diff --git a/client/main.ts b/client/main.ts
index e5d6c0a..4888113 100644
--- a/client/main.ts
+++ b/client/main.ts
@@ -18,6 +18,10 @@
// (voice/STT plumbing)
// ============================================================
+// Migrated modules — each side-effect import installs its window-global
+// shim for legacy consumers and self-registers any DOM listeners.
+import './ui-state';
+
console.log('[client] TypeScript bundle loaded.');
// Marker so anything else that wants to know if the bundle reached
diff --git a/client/ui-state.ts b/client/ui-state.ts
new file mode 100644
index 0000000..5f01cf1
--- /dev/null
+++ b/client/ui-state.ts
@@ -0,0 +1,37 @@
+// ============================================================
+// client/ui-state.ts — sub-pill / sub-tab persistence across browser restart.
+//
+// First file migrated out of public/js/ as ES module + TypeScript.
+// Tab-level selection (ped_last_tab) is handled in app.js (still legacy).
+// This file persists finer-grained choices that would otherwise reset
+// to defaults when the user closes the browser, signs out / back in,
+// or reloads the page — because _componentCache is in-memory only.
+//
+// All keys live under "ped_ui/" in localStorage. Reads and writes are
+// wrapped in try/catch — localStorage can throw in some contexts
+// (Safari private mode, storage quotas), and a persistence failure
+// must never break the UI.
+//
+// Compat: window.UIState is still assigned for the 10+ legacy consumers
+// (app.js, sub-nav.js, several IIFE modules). Once those are migrated
+// to ES module imports, the window assignment can be removed.
+// ============================================================
+
+const PREFIX = 'ped_ui/';
+
+export function get(key: string): string | null {
+ try { return localStorage.getItem(PREFIX + key); } catch (e) { return null; }
+}
+
+export function set(key: string, value: string): void {
+ try { localStorage.setItem(PREFIX + key, value); } catch (e) { /* swallow — see header */ }
+}
+
+export function del(key: string): void {
+ try { localStorage.removeItem(PREFIX + key); } catch (e) { /* swallow */ }
+}
+
+export const UIState = { get, set, del };
+
+// Legacy bridge — kept until every consumer is migrated to ES imports.
+(window as any).UIState = UIState;
diff --git a/public/index.html b/public/index.html
index 78e51d6..c34906a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -452,7 +452,7 @@
-
+
diff --git a/public/js/ui-state.js b/public/js/ui-state.js
deleted file mode 100644
index 9dc9d40..0000000
--- a/public/js/ui-state.js
+++ /dev/null
@@ -1,35 +0,0 @@
-// ============================================================
-// UI STATE — sub-pill / sub-tab persistence across browser restart
-// ============================================================
-// Tab-level selection (ped_last_tab) is handled in app.js. This file
-// persists the finer-grained choices that would otherwise reset to
-// defaults when the user closes the browser, signs out / back in, or
-// reloads the page — because _componentCache is in-memory and cleared
-// on reload.
-//
-// All keys live under the "ped_ui/" namespace in localStorage so they
-// are easy to enumerate and clear. Reads and writes are wrapped in
-// try/catch — localStorage can throw in some contexts (Safari private
-// mode, storage quotas), and a persistence failure must never break
-// the UI.
-// ============================================================
-
-(function () {
- var PREFIX = 'ped_ui/';
-
- function get(key) {
- try { return localStorage.getItem(PREFIX + key); } catch (e) { return null; }
- }
- function set(key, value) {
- try { localStorage.setItem(PREFIX + key, value); } catch (e) {}
- }
- function del(key) {
- try { localStorage.removeItem(PREFIX + key); } catch (e) {}
- }
-
- window.UIState = {
- get: get,
- set: set,
- del: del,
- };
-})();
diff --git a/scripts/build-client.mjs b/scripts/build-client.mjs
index 1ac54c2..4c2ebb4 100644
--- a/scripts/build-client.mjs
+++ b/scripts/build-client.mjs
@@ -31,17 +31,22 @@ if (!existsSync(SRC)) {
}
mkdirSync(OUT, { recursive: true });
-// Each top-level .ts/.tsx file in client/ is a separate entry point.
-// Sub-directories are imported by the entries via TS imports — they
-// are NOT separate entries, they get bundled into their parent.
-const entryFiles = readdirSync(SRC, { withFileTypes: true })
- .filter(d => d.isFile() && /\.tsx?$/.test(d.name))
- .map(d => resolve(SRC, d.name));
+// Explicit entry list — every other .ts file in client/ is a sub-module
+// imported by an entry. Add new entries here when a feature truly needs
+// its own separate bundle (different DOM lifecycle, lazy-loaded route,
+// etc.). For most modernization work, just import into main.ts.
+const ENTRIES = [
+ 'main.ts',
+];
-if (entryFiles.length === 0) {
- console.error(`[build-client] No .ts entry points found in ${SRC}`);
- process.exit(1);
-}
+const entryFiles = ENTRIES.map(name => {
+ const p = resolve(SRC, name);
+ if (!existsSync(p)) {
+ console.error(`[build-client] Entry file does not exist: ${p}`);
+ process.exit(1);
+ }
+ return p;
+});
const buildOpts = {
entryPoints: entryFiles,