summaryrefslogtreecommitdiffhomepage
path: root/src/features/surface-host/logic/table.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-10 16:29:01 +0900
committerAdam Malczewski <[email protected]>2026-06-10 16:29:01 +0900
commit871957b930203c019e631c4606cfdf8266d222fa (patch)
tree50c522018c3ce4127ffa76f4b3b6c7843e90db43 /src/features/surface-host/logic/table.ts
parent7b345f132763fa6405ae858b74e46229629c19d9 (diff)
downloaddispatch-web-871957b930203c019e631c4606cfdf8266d222fa.tar.gz
dispatch-web-871957b930203c019e631c4606cfdf8266d222fa.zip
feat(views,surface-host): Extensions sidebar view — auto-expanded surfaces + tables
views (new feature): - pure panel-stack reducer + thin generic ViewSidebar (dropdown picker + add/remove), switches on view KIND, never a surface id Extensions view (composition root): - folds frontend modules + backend surfaces into one "Extensions" view - frontend module list AGGREGATED from each feature's public `manifest` export (can't drift); no per-module version (FE features are internal to dispatch-web) - surfaces are AUTO-SUBSCRIBED on catalog + rendered expanded (no catalog buttons) surface-host: - consecutive `stat` fields coalesce into one aligned label/value table (StatTable) - generic custom-field renderer: dispatch on rendererId === "table" → SurfaceTable (pure parseTablePayload), so a backend `custom`/table field renders generically - shared presentational components/Table.svelte (used by both, neither feature depends on the other) store: - auto-subscribe every catalog entry, unsubscribe vanished ones, re-subscribe all on reconnect; expose all received specs via `surfaces` (drops single-selection) backend-handoff: CR-1 — emit Loaded Extensions as a custom/table field; notes what's already covered FE-side (renderer shipped, stat-table fallback works).
Diffstat (limited to 'src/features/surface-host/logic/table.ts')
-rw-r--r--src/features/surface-host/logic/table.ts54
1 files changed, 54 insertions, 0 deletions
diff --git a/src/features/surface-host/logic/table.ts b/src/features/surface-host/logic/table.ts
new file mode 100644
index 0000000..027553c
--- /dev/null
+++ b/src/features/surface-host/logic/table.ts
@@ -0,0 +1,54 @@
+/**
+ * Pure parser for the `rendererId: "table"` custom-field payload.
+ *
+ * This is the FRONTEND-side renderer contract for tabular custom fields: a
+ * backend that wants a table emits a `custom` field with `rendererId: "table"`
+ * and a payload of `{ columns: string[]; rows: (string|number)[][] }`. Cells are
+ * coerced to strings. Anything that does not match the shape returns `null`, so
+ * the renderer gracefully skips it (never throws on hostile/partial data).
+ */
+
+export interface TableData {
+ readonly columns: readonly string[];
+ readonly rows: readonly (readonly string[])[];
+}
+
+function isStringArray(v: unknown): v is unknown[] {
+ return Array.isArray(v);
+}
+
+function coerceCell(v: unknown): string | null {
+ if (typeof v === "string") return v;
+ if (typeof v === "number" && Number.isFinite(v)) return String(v);
+ if (typeof v === "boolean") return String(v);
+ return null;
+}
+
+export function parseTablePayload(payload: unknown): TableData | null {
+ if (typeof payload !== "object" || payload === null) return null;
+ const obj = payload as Record<string, unknown>;
+
+ const rawColumns = obj.columns;
+ const rawRows = obj.rows;
+ if (!isStringArray(rawColumns) || !isStringArray(rawRows)) return null;
+
+ const columns: string[] = [];
+ for (const col of rawColumns) {
+ if (typeof col !== "string") return null;
+ columns.push(col);
+ }
+
+ const rows: string[][] = [];
+ for (const row of rawRows) {
+ if (!Array.isArray(row)) return null;
+ const cells: string[] = [];
+ for (const cell of row) {
+ const c = coerceCell(cell);
+ if (c === null) return null;
+ cells.push(c);
+ }
+ rows.push(cells);
+ }
+
+ return { columns, rows };
+}