@lavalogic/scoria 0.40.0 → 0.40.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +58 -58
  2. package/dist/Components/AccordionGroup.svelte +46 -46
  3. package/dist/Components/AccordionGroupButton.svelte +76 -76
  4. package/dist/Components/Action.svelte +136 -136
  5. package/dist/Components/AlertModal.svelte +195 -195
  6. package/dist/Components/AppointmentCalendar/AppointmentCalendar.svelte +729 -729
  7. package/dist/Components/Base/BaseModal.svelte +125 -125
  8. package/dist/Components/Base/ContextWrapper.svelte +72 -72
  9. package/dist/Components/Base/Pagination.svelte +198 -198
  10. package/dist/Components/Base/Snippets.svelte +36 -36
  11. package/dist/Components/BigRadioSet.svelte +38 -38
  12. package/dist/Components/Bubble.svelte +101 -101
  13. package/dist/Components/Button.svelte +209 -209
  14. package/dist/Components/Checkbox.svelte +86 -86
  15. package/dist/Components/CollapsibleCard.svelte +78 -78
  16. package/dist/Components/Contexts/ContextMenu.svelte +164 -164
  17. package/dist/Components/Contexts/ContextMenuDivider.svelte +15 -15
  18. package/dist/Components/Contexts/ContextMenuOption.svelte +60 -60
  19. package/dist/Components/Contexts/Toast.svelte +86 -86
  20. package/dist/Components/Contexts/ToastContainer.svelte +65 -65
  21. package/dist/Components/Contexts/Tooltip.svelte +37 -37
  22. package/dist/Components/DataMatrixIcon.svelte +112 -112
  23. package/dist/Components/DateInput.svelte +148 -148
  24. package/dist/Components/DatePicker.svelte +300 -300
  25. package/dist/Components/DesktopModal.svelte +90 -90
  26. package/dist/Components/Dial.svelte +204 -204
  27. package/dist/Components/DimensionInput.svelte +170 -170
  28. package/dist/Components/DragMenu/DragMenuHolder.svelte +119 -119
  29. package/dist/Components/DragMenu/DraggableCard.svelte +77 -77
  30. package/dist/Components/FileCard.svelte +24 -24
  31. package/dist/Components/FileUpload.svelte +236 -236
  32. package/dist/Components/GridInput.svelte +196 -196
  33. package/dist/Components/GridInputSet.svelte +29 -29
  34. package/dist/Components/HorizontalTabGroup.svelte +82 -82
  35. package/dist/Components/HorizontalTabGroupButton.svelte +44 -44
  36. package/dist/Components/Icon.svelte +128 -128
  37. package/dist/Components/Icons.js +357 -357
  38. package/dist/Components/InfoAlert.svelte +35 -35
  39. package/dist/Components/LoadingAnimation.svelte +29 -29
  40. package/dist/Components/LoadingModal.svelte +45 -45
  41. package/dist/Components/LoadingOverlay.svelte +25 -25
  42. package/dist/Components/Modal.svelte +258 -258
  43. package/dist/Components/MultiSelect.svelte +220 -220
  44. package/dist/Components/Nav/Nav.svelte +110 -110
  45. package/dist/Components/Nav/NavServiceMenuList.svelte +43 -43
  46. package/dist/Components/Nav/NavTab.svelte +62 -62
  47. package/dist/Components/Nav/ServiceMenuItem.svelte +112 -112
  48. package/dist/Components/Nav/ServicesNav.svelte +80 -80
  49. package/dist/Components/NumberInput.svelte +157 -157
  50. package/dist/Components/PageHeading.svelte +25 -25
  51. package/dist/Components/PasswordInput.svelte +88 -88
  52. package/dist/Components/Portal.svelte +36 -36
  53. package/dist/Components/ProgressBubble.svelte +107 -107
  54. package/dist/Components/QuerySelect.svelte +366 -366
  55. package/dist/Components/SingleSelect.svelte +250 -250
  56. package/dist/Components/StepButton.svelte +70 -70
  57. package/dist/Components/StepForm.svelte +140 -140
  58. package/dist/Components/Switch.svelte +69 -69
  59. package/dist/Components/Table/ARCHITECTURE.md +298 -298
  60. package/dist/Components/Table/Body/QuickSearchCell.svelte +679 -679
  61. package/dist/Components/Table/Body/Rows/ColumnHighlight.svelte +51 -51
  62. package/dist/Components/Table/Body/Rows/ColumnList.svelte +59 -59
  63. package/dist/Components/Table/Body/Rows/ColumnListRow.svelte +454 -454
  64. package/dist/Components/Table/Body/Rows/Columns/AccessorComponent.svelte +79 -79
  65. package/dist/Components/Table/Body/Rows/Columns/ActionsCell.svelte +185 -185
  66. package/dist/Components/Table/Body/Rows/Columns/BubbleCell.svelte +124 -124
  67. package/dist/Components/Table/Body/Rows/Columns/ExpandCell.svelte +131 -120
  68. package/dist/Components/Table/Body/Rows/Columns/FocusableImmutableCell.svelte +234 -234
  69. package/dist/Components/Table/Body/Rows/Columns/ProgressCell.svelte +105 -105
  70. package/dist/Components/Table/Body/Rows/Columns/TableAction.svelte +87 -87
  71. package/dist/Components/Table/Body/Rows/Columns/TableCheckbox.svelte +126 -126
  72. package/dist/Components/Table/Body/Rows/Columns/TableColumn.svelte +159 -159
  73. package/dist/Components/Table/Body/Rows/Columns/TableDatePicker.svelte +246 -246
  74. package/dist/Components/Table/Body/Rows/Columns/TableNumberInput.svelte +216 -216
  75. package/dist/Components/Table/Body/Rows/Columns/TableQuerySelect.svelte +252 -252
  76. package/dist/Components/Table/Body/Rows/Columns/TableRowSelectionCheckbox.svelte +179 -179
  77. package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte +221 -221
  78. package/dist/Components/Table/Body/Rows/Columns/TableTextInput.svelte +197 -197
  79. package/dist/Components/Table/Body/Rows/Columns/ValidityCell.svelte +190 -190
  80. package/dist/Components/Table/Body/Rows/TableRow.svelte +290 -290
  81. package/dist/Components/Table/Body/TableBody.svelte +155 -155
  82. package/dist/Components/Table/ColumnFactory.svelte.js +1 -0
  83. package/dist/Components/Table/Headers/HighlightAllHeader.svelte +41 -41
  84. package/dist/Components/Table/Headers/SelectAllHeader.svelte +30 -30
  85. package/dist/Components/Table/Headers/TableHead.svelte +415 -415
  86. package/dist/Components/Table/Misc/ColumnPanel.svelte +39 -39
  87. package/dist/Components/Table/Misc/ColumnPanelModal.svelte +39 -39
  88. package/dist/Components/Table/Misc/FilterInput.svelte +641 -641
  89. package/dist/Components/Table/Misc/FilterPanel.svelte +190 -190
  90. package/dist/Components/Table/Misc/FilterPanelModal.svelte +31 -31
  91. package/dist/Components/Table/Misc/ShareViewModal.svelte +291 -291
  92. package/dist/Components/Table/Misc/SidePanel.svelte +35 -35
  93. package/dist/Components/Table/Misc/TableConfigurationModal.svelte +644 -644
  94. package/dist/Components/Table/Misc/TableFooter.svelte +340 -340
  95. package/dist/Components/Table/Misc/TableHorizontalBar.svelte +140 -140
  96. package/dist/Components/Table/Misc/TableSidebar.svelte +200 -200
  97. package/dist/Components/Table/Misc/TableViewDropdown.svelte +105 -105
  98. package/dist/Components/Table/Misc/ToolboxPanel.svelte +100 -100
  99. package/dist/Components/Table/Misc/ToolboxPanelModal.svelte +107 -107
  100. package/dist/Components/Table/Misc/UpdateViewOptionsModal.svelte +192 -192
  101. package/dist/Components/Table/NestedTable.svelte +130 -130
  102. package/dist/Components/Table/Table.svelte +692 -692
  103. package/dist/Components/Table/Types/Columns/Definitions/Expand/ExpandDef.svelte.d.ts +5 -0
  104. package/dist/Components/Table/Types/Columns/Definitions/Expand/ExpandDef.svelte.js +3 -0
  105. package/dist/Components/Table/Types/Public/ColumnOptions.d.ts +10 -0
  106. package/dist/Components/Test/TestContextConsumer.svelte +13 -13
  107. package/dist/Components/TextArea.svelte +130 -130
  108. package/dist/Components/TextInput.svelte +176 -176
  109. package/dist/Components/ThreeStateRadio.svelte +131 -131
  110. package/dist/Components/Touch/BoolCard.svelte +45 -45
  111. package/dist/Components/Touch/DateModal.svelte +209 -209
  112. package/dist/Components/Touch/EditCard.svelte +59 -59
  113. package/dist/Components/Touch/InfoCard.svelte +31 -31
  114. package/dist/Components/Touch/InfoTextCard.svelte +28 -28
  115. package/dist/Components/Touch/ModalExplanation.svelte +17 -17
  116. package/dist/Components/Touch/NumberKeyboard.svelte +225 -225
  117. package/dist/Components/Touch/NumberModal.svelte +79 -79
  118. package/dist/Components/Touch/PlusMinusCard.svelte +46 -46
  119. package/dist/Components/Touch/SelectModal.svelte +127 -127
  120. package/dist/Components/Touch/SummaryCard.svelte +93 -93
  121. package/dist/Components/Touch/TextAreaModal.svelte +95 -95
  122. package/dist/Components/Touch/TextModal.svelte +106 -106
  123. package/dist/Components/Touch/TouchCard.svelte +33 -33
  124. package/dist/Components/Touch/TouchModal.svelte +274 -274
  125. package/dist/Components/Touch/UtilityButton.svelte +109 -109
  126. package/dist/Components/VerticalTabGroup.svelte +79 -79
  127. package/dist/Components/VerticalTabGroupButton.svelte +58 -58
  128. package/dist/Helpers/Datamatrix.d.ts +19 -19
  129. package/dist/Helpers/Datamatrix.js +291 -291
  130. package/dist/Types/Examples/ExampleModal.svelte +35 -35
  131. package/dist/scss/_basics.scss +12 -12
  132. package/dist/scss/_colours.scss +134 -134
  133. package/dist/scss/_mixins.scss +3392 -3392
  134. package/dist/scss/_motion.scss +57 -57
  135. package/dist/scss/_sizing.scss +85 -85
  136. package/dist/scss/_transitions.scss +8 -8
  137. package/package.json +1 -1
@@ -1,298 +1,298 @@
1
- # `<Table>` architecture
2
-
3
- Orientation for a new developer opening `src/lib/Components/Table/`. Read this once before changing anything substantive in this subtree. Read it again before reviewing a PR.
4
-
5
- For the cross-cutting story of how the current shape came to be, see
6
- `agent-review/dev-changelog.md` at repo root. For the consumer-facing
7
- public API, see the Storybook stories under `Table/Table API` and
8
- `src/stories/Table/*`.
9
-
10
- ---
11
-
12
- ## What ships out of the front door
13
-
14
- Everything in the public surface is re-exported from
15
- `src/lib/index.ts`. The Table-side of that file is small on purpose:
16
-
17
- ```ts
18
- import {
19
- createTable,
20
- localRows,
21
- remoteRows,
22
- customRows,
23
- Table,
24
- type CreateTableOptions,
25
- type DisplayModalProps,
26
- // …per-kind option types: TextColumnOptions, NumberColumnOptions, …
27
- } from '@lavalogic/scoria';
28
- ```
29
-
30
- Three entry points cover every legitimate use:
31
-
32
- 1. `createTable<TRow, IdType>({...})` builds a structured `Table`
33
- instance. The returned value carries the typed sub-APIs (`selection`,
34
- `filtering`, `sorting`, `clipboard`, `columns`, `pagination`,
35
- `preferences`, `focus`, `modal`).
36
- 2. `<Table {table} />` mounts the instance.
37
- 3. The column builder closure passed under `columns:` is given a
38
- `ColumnFactory<TRow>` parameter; consumers reach for the matching
39
- `col.text(...)`, `col.number(...)`, `col.select(...)` and so on.
40
-
41
- The Def classes (`TextInputDef`, `DisplayDef`, …) are NOT exported. They
42
- are constructed inside `ColumnFactory.svelte.ts` and never appear on a
43
- consumer's import surface. If you need to alter Def behaviour, do it
44
- through the factory.
45
-
46
- ---
47
-
48
- ## Folder tour
49
-
50
- ```
51
- Table/
52
- ├── Table.svelte — top-level component (mounts the grid + chrome)
53
- ├── createTable.svelte.ts — the consumer-facing entry; builds the TableContext + public Table instance
54
- ├── ColumnFactory.svelte.ts — closure-based column builder; wraps each Def
55
- ├── ColumnKindRegistry.ts — kind→category map used by setupDefaultColumnDefs
56
- ├── ColumnKindRegistry.test.ts — registry contract tests
57
- ├── SubApis.svelte.ts — facade constructors for each typed sub-API on the public Table instance
58
- ├── NestedTable.svelte — internal Expand-column body
59
- ├── TableHelpers.svelte.ts — co-located helpers (column-id mint, etc.)
60
- ├── clipboardEscape.ts — escapeForSpreadsheet / encapsulate
61
-
62
- ├── Body/ — body-grid layer
63
- │ ├── TableBody.svelte
64
- │ ├── QuickSearchCell.svelte — filter row inputs
65
- │ └── Rows/Columns/ — per-kind cell components
66
-
67
- ├── Headers/ — header-row layer
68
- │ └── TableHead.svelte — column headers, sort buttons, resize handles
69
-
70
- ├── Misc/ — toolbars, side-panels, footer, filter panels
71
- │ ├── TableFooter.svelte
72
- │ ├── TableSidebar.svelte
73
- │ ├── TableHorizontalBar.svelte
74
- │ ├── FilterPanel.svelte
75
- │ ├── FilterInput.svelte — advanced-filter panel inputs
76
- │ ├── ColumnPanel.svelte — column visibility & order panel
77
- │ └── ToolboxPanel.svelte — extra tool buttons
78
-
79
- └── Types/ — all type declarations + state classes
80
- ├── Public/ — every consumer-facing type
81
- ├── Context/ — TableContext (god) + the Phase 6 sub-contexts
82
- ├── Columns/ — Def classes (internal) + Column shape types
83
- ├── DataRepository/ — TableDataRepository abstract + BuiltInRemoteRepository
84
- ├── Filtering/ — FilterFn, ColumnFiltersState, FilterMode enums
85
- ├── Pagination/ — PaginationState, PaginationType enum
86
- ├── Focus/ — focus-coordinate helpers
87
- ├── Coordinates/ — CellCoordinates, ColumnIndices brands
88
- ├── Layout/ — layout-related helpers
89
- └── Toolbar/ — ToolbarPosition enum
90
- ```
91
-
92
- ---
93
-
94
- ## The state graph
95
-
96
- `TableContext` is the root state object. It owns the data repo and seven
97
- sub-contexts:
98
-
99
- | Sub-context | Owns |
100
- | ----------------- | ------------------------------------------------------------------------- |
101
- | `_sorting` | Sort comparators + the `sortedData` derived chain |
102
- | `_filtering` | Filter-fn relays + the `filteredData` derived chain + quick-filter toggle |
103
- | `_columnLayout` | Column widths / pinning / visibility / id-index maps |
104
- | `_selection` | Selected & highlighted maps, key cache, page-wide select helpers |
105
- | `_preferences` | Table settings + layout / filter / view localStorage round-trip |
106
- | `_focus` | Focus-coordinate state + multi-cell selection |
107
- | `_virtualisation` | Row virtualisation window |
108
- | `_queryValues` | Typeahead-echo cache for QuerySelect filter inputs |
109
-
110
- Each sub-context is a class under `Types/Context/<Name>State.svelte.ts`
111
- that takes a `deps` object of thunk-returning fields the parent reads
112
- on demand. The thunks keep reactivity flowing without the sub-context
113
- holding a parent reference — re-binding `TableContext._sorting` (or
114
- swapping its parent) is impossible by construction.
115
-
116
- The pattern when adding a new sub-context:
117
-
118
- 1. Drop a `Types/Context/<Name>State.svelte.ts` file using
119
- `FilteringState` as the structural template (`<Name>StateDeps`
120
- interface, class with `$state` / `$derived` slots, `deps` thunks).
121
- 2. Add a `_name: <Name>State` field on `TableContext` constructed with
122
- the deps thunks closed over `this`.
123
- 3. Add public passthrough getters/setters on `TableContext` for any
124
- slot consumers reached for through the legacy field name.
125
- 4. Write a `<Name>State.svelte.test.ts` test next to the new class.
126
- Cover construction, each writer, and each derived read.
127
- 5. If the sub-context surfaces a typed sub-API on the public `Table`
128
- instance, wire a builder in `SubApis.svelte.ts` returning a frozen
129
- object literal.
130
-
131
- ---
132
-
133
- ## The data repo
134
-
135
- `TableContext.dataRepo` is the storage layer. Three concrete shapes:
136
-
137
- - `LocalDataRepository` — handed a plain array; sorts/filters/paginates
138
- in process.
139
- - `BuiltInRemoteRepository` — fetches pages from a REST endpoint with
140
- scoria's built-in query-string format.
141
- - `CustomDataRepository` — built by `customRows({ fetch, … })` for
142
- consumer-shaped query strings. flowms wraps this in
143
- `createRemoteRowSource.ts` to map flowms's filter format.
144
-
145
- The repo abstract base (`TableDataRepository.svelte.ts`) owns:
146
-
147
- - `paginationState` (page index + size, persisted)
148
- - the per-column `_filterModes` SvelteMap
149
- - the `forceRefresh` callback hook (use `registerRefreshMethod` to
150
- assign without firing; the setter form fires immediately and is
151
- `@deprecated`)
152
-
153
- If you add a new repo subclass, also extend the runtime discriminator
154
- on `IDataRepository.paginationType` so existing callers can branch.
155
-
156
- ---
157
-
158
- ## Adding a new column kind
159
-
160
- This is intentionally not a one-file change; the surface area below is
161
- the irreducible minimum. Pick the four (sometimes five) sites carefully:
162
-
163
- 1. **Def class** — `Types/Columns/Definitions/{Accessors,Display,…}/<Kind>Def.svelte.ts`.
164
- Inherits from the matching category base (`AccessorDef` / `DisplayDef` / etc.).
165
- 2. **Cell renderer** — `Body/Rows/Columns/<Kind>Cell.svelte`. Picked
166
- by the dispatch chain in `TableColumn.svelte` (currently an
167
- `instanceof` if/else; intentionally not registry-driven — see
168
- follow-up F3 in `PROJECT_CONTEXT.md`).
169
- 3. **Public option type** — add to `Types/Public/ColumnOptions.ts`.
170
- Discriminated by the kind literal.
171
- 4. **Factory method** — add a method on `ColumnFactory<TRow>` in
172
- `ColumnFactory.svelte.ts`. The factory's `makeSpec(kind, def, …)`
173
- tags the def with `def.kind = kind` automatically.
174
- 5. **Registry entry** — add to `COLUMN_KIND_REGISTRY` in
175
- `ColumnKindRegistry.ts`. Mark `category` and (if applicable) a
176
- `defaultFilter` key. The companion test file enforces "every kind
177
- has an entry".
178
-
179
- A `setupDefaultColumnDefs` change is no longer needed; it now reads
180
- from the registry.
181
-
182
- ---
183
-
184
- ## Load-bearing invariants
185
-
186
- These each cost real time to discover the first time. Tagged for future
187
- greppability.
188
-
189
- ### `def.kind` is the canonical sort key
190
-
191
- Every `ColumnDef` produced by the factory carries `def.kind`. Filter
192
- wiring (`setupDefaultColumnDefs`), debug logs, and the public spec all
193
- derive from it. A hand-constructed `ColumnDef` (test stub, legacy code)
194
- without `kind` is tolerated but skips registry-driven defaults — the
195
- consumer must assign `filterFn` themselves.
196
-
197
- ### Filter-input `__queryValues` is gone; use `tableContext.queryValues`
198
-
199
- The legacy `dataRepo.__queryValues: Map<string, unknown>` shared cache
200
- has been replaced by the typed `QueryValuesState` sub-context. New
201
- filter widgets should call `tableContext.queryValues.get(def)` /
202
- `set(def, value)`. The sub-context keys by `ColumnDef` so the
203
- historical `def.id` vs `label` keying bug class is structurally
204
- prevented.
205
-
206
- ### `reactiveDependencies` for async accessors
207
-
208
- `col.display({ value: async (row) => row.foo.value })` will only
209
- re-render when row identity changes — not when `row.foo.value` mutates,
210
- because the reactive read happens after the `await` boundary. Pass a
211
- `reactiveDependencies: (row) => [row.foo.value]` thunk on the column
212
- options so the cell's `$derived` registers the dep synchronously.
213
-
214
- ### Dropdowns do NOT move in the DOM
215
-
216
- `Helpers/dropdownPortal.ts` flips a Svelecte dropdown to
217
- `position: fixed` and lifts its stacking-context ancestor; it does NOT
218
- re-parent the dropdown to `document.body`. Svelte 5's root-level event
219
- delegation walks the live `parentNode` chain at dispatch time — moving
220
- the element out of the mount target silently breaks mouse-click option
221
- selection while keyboard Enter keeps working. See the header comment in
222
- `dropdownPortal.ts` for the full reasoning.
223
-
224
- ### Pinned cells skip horizontal scrollIntoView
225
-
226
- In `Table.svelte`'s focus-restore effects, the scroll-into-view call
227
- omits `inline: 'nearest'` for pinned cells. Sticky positioning makes
228
- them visually always-on-screen, but their layout position is at the
229
- real (often off-screen) column track; an unconditional inline scroll
230
- would surface the underlying position and the table would "jump" when
231
- focus lands on a pinned Actions button.
232
-
233
- ### Clipboard copy must pass through `escapeForSpreadsheet`
234
-
235
- Every path that writes to `navigator.clipboard.writeText` is required
236
- to neutralise formula-injection prefixes via
237
- `escapeForSpreadsheet` (or `encapsulate`, which calls it). The single-
238
- cell `clipboard.copyCell` route and the bulk
239
- `copyDataToClipboard` / `copyHighlightedRowsToClipboard` paths both
240
- already use it; new copy paths must too.
241
-
242
- ### `createTable` is async-effective; consume via reactive readers
243
-
244
- `createTable({...})` returns the structured `Table` instance synchronously, but the underlying `TableContext` finishes initialising asynchronously: `setupDefaultColumnDefs` runs unawaited in the constructor, and `_selection`'s pending-changes buffer flushes on `tick()`. Two practical consequences:
245
-
246
- - `table.columns.specs` and `table._context.columnDefs` are empty for the first microtask after `createTable` returns. Reactive readers (a `$derived` or `$effect`) re-evaluate when the columns land, so consumer code reads through these without thinking about it.
247
- - `table.selection.set(rowId, row)` writes into the pending-changes buffer and flushes on `tick()`. `table.selection.size` returns the post-flush value the next time a reactive consumer reads it; a synchronous read on the same call frame returns the pre-flush value.
248
-
249
- Direct synchronous read-back of state is therefore only safe inside an `$effect` or after `await tick()`. Unit tests at the public API surface need either a reactive harness or explicit `await tick()` between writes and assertions; that's why scoria's table tests use the `.svelte.test.ts` (browser-project) shape rather than plain server-mode unit tests.
250
-
251
- ### Custom-row sources must defensively resolve their accessor thunks
252
-
253
- A `customRows({ uri, defaultFilters, remoteFilters })` source can have
254
- its accessor thunks called after the surrounding Svelte component has
255
- torn down (a debounced fetch landing across a route change). Wrap the
256
- thunk reads in try/catch and validate the returned shape — see
257
- `flowms-svelte-web/src/lib/components/base/Table/createRemoteRowSource.ts`
258
- for the canonical pattern.
259
-
260
- ---
261
-
262
- ## Testing model
263
-
264
- | File pattern | Project | What it covers |
265
- | --------------------------- | -------- | ------------------------------------------------------------------------------------- |
266
- | `*.test.ts` (no `.svelte.`) | `server` | Pure-node unit tests (helpers, type contracts). Runs via `pnpm test`. |
267
- | `*.svelte.test.ts` | `client` | Tests using `$state` / `$effect` from Svelte 5. Browser mode via Vitest + Playwright. |
268
-
269
- The browser-mode runner has a Windows-specific chromium launch issue;
270
- set `SCORIA_CHROMIUM_EXECUTABLE` to the headless-shell binary to work
271
- around it (follow-up F1 in `PROJECT_CONTEXT.md`). The server project
272
- covers the kind-registry contract, clipboard escape behaviour, and the
273
- co-located decoupling tests.
274
-
275
- Add new tests next to the code they cover, not in a global `__tests__`
276
- folder.
277
-
278
- ---
279
-
280
- ## Storybook
281
-
282
- Stories live under `src/stories/Table/`. Mount via the shared host
283
- `_helpers/TableHost.svelte` which accepts a `CreateTableOptions` payload
284
- and runs `createTable` inside instance init (module-scope `createTable`
285
- would trip `effect_orphan`). The `Table/Table API` MDX page is the
286
- companion reference; keep it in sync with significant public-API
287
- changes.
288
-
289
- ---
290
-
291
- ## Cross-references
292
-
293
- - `PROJECT_CONTEXT.md` (repo root) — project state, known issues and
294
- follow-ups (Part A), plus the consumer migration guide (Part B).
295
- - `agent-review/dev-changelog.md` — long-form record of the
296
- decomposition phases.
297
- - `src/lib/Components/Table/Types/Public/index.ts` — barrel for every
298
- public type re-exported from `src/lib/index.ts`.
1
+ # `<Table>` architecture
2
+
3
+ Orientation for a new developer opening `src/lib/Components/Table/`. Read this once before changing anything substantive in this subtree. Read it again before reviewing a PR.
4
+
5
+ For the cross-cutting story of how the current shape came to be, see
6
+ `agent-review/dev-changelog.md` at repo root. For the consumer-facing
7
+ public API, see the Storybook stories under `Table/Table API` and
8
+ `src/stories/Table/*`.
9
+
10
+ ---
11
+
12
+ ## What ships out of the front door
13
+
14
+ Everything in the public surface is re-exported from
15
+ `src/lib/index.ts`. The Table-side of that file is small on purpose:
16
+
17
+ ```ts
18
+ import {
19
+ createTable,
20
+ localRows,
21
+ remoteRows,
22
+ customRows,
23
+ Table,
24
+ type CreateTableOptions,
25
+ type DisplayModalProps,
26
+ // …per-kind option types: TextColumnOptions, NumberColumnOptions, …
27
+ } from '@lavalogic/scoria';
28
+ ```
29
+
30
+ Three entry points cover every legitimate use:
31
+
32
+ 1. `createTable<TRow, IdType>({...})` builds a structured `Table`
33
+ instance. The returned value carries the typed sub-APIs (`selection`,
34
+ `filtering`, `sorting`, `clipboard`, `columns`, `pagination`,
35
+ `preferences`, `focus`, `modal`).
36
+ 2. `<Table {table} />` mounts the instance.
37
+ 3. The column builder closure passed under `columns:` is given a
38
+ `ColumnFactory<TRow>` parameter; consumers reach for the matching
39
+ `col.text(...)`, `col.number(...)`, `col.select(...)` and so on.
40
+
41
+ The Def classes (`TextInputDef`, `DisplayDef`, …) are NOT exported. They
42
+ are constructed inside `ColumnFactory.svelte.ts` and never appear on a
43
+ consumer's import surface. If you need to alter Def behaviour, do it
44
+ through the factory.
45
+
46
+ ---
47
+
48
+ ## Folder tour
49
+
50
+ ```
51
+ Table/
52
+ ├── Table.svelte — top-level component (mounts the grid + chrome)
53
+ ├── createTable.svelte.ts — the consumer-facing entry; builds the TableContext + public Table instance
54
+ ├── ColumnFactory.svelte.ts — closure-based column builder; wraps each Def
55
+ ├── ColumnKindRegistry.ts — kind→category map used by setupDefaultColumnDefs
56
+ ├── ColumnKindRegistry.test.ts — registry contract tests
57
+ ├── SubApis.svelte.ts — facade constructors for each typed sub-API on the public Table instance
58
+ ├── NestedTable.svelte — internal Expand-column body
59
+ ├── TableHelpers.svelte.ts — co-located helpers (column-id mint, etc.)
60
+ ├── clipboardEscape.ts — escapeForSpreadsheet / encapsulate
61
+
62
+ ├── Body/ — body-grid layer
63
+ │ ├── TableBody.svelte
64
+ │ ├── QuickSearchCell.svelte — filter row inputs
65
+ │ └── Rows/Columns/ — per-kind cell components
66
+
67
+ ├── Headers/ — header-row layer
68
+ │ └── TableHead.svelte — column headers, sort buttons, resize handles
69
+
70
+ ├── Misc/ — toolbars, side-panels, footer, filter panels
71
+ │ ├── TableFooter.svelte
72
+ │ ├── TableSidebar.svelte
73
+ │ ├── TableHorizontalBar.svelte
74
+ │ ├── FilterPanel.svelte
75
+ │ ├── FilterInput.svelte — advanced-filter panel inputs
76
+ │ ├── ColumnPanel.svelte — column visibility & order panel
77
+ │ └── ToolboxPanel.svelte — extra tool buttons
78
+
79
+ └── Types/ — all type declarations + state classes
80
+ ├── Public/ — every consumer-facing type
81
+ ├── Context/ — TableContext (god) + the Phase 6 sub-contexts
82
+ ├── Columns/ — Def classes (internal) + Column shape types
83
+ ├── DataRepository/ — TableDataRepository abstract + BuiltInRemoteRepository
84
+ ├── Filtering/ — FilterFn, ColumnFiltersState, FilterMode enums
85
+ ├── Pagination/ — PaginationState, PaginationType enum
86
+ ├── Focus/ — focus-coordinate helpers
87
+ ├── Coordinates/ — CellCoordinates, ColumnIndices brands
88
+ ├── Layout/ — layout-related helpers
89
+ └── Toolbar/ — ToolbarPosition enum
90
+ ```
91
+
92
+ ---
93
+
94
+ ## The state graph
95
+
96
+ `TableContext` is the root state object. It owns the data repo and seven
97
+ sub-contexts:
98
+
99
+ | Sub-context | Owns |
100
+ | ----------------- | ------------------------------------------------------------------------- |
101
+ | `_sorting` | Sort comparators + the `sortedData` derived chain |
102
+ | `_filtering` | Filter-fn relays + the `filteredData` derived chain + quick-filter toggle |
103
+ | `_columnLayout` | Column widths / pinning / visibility / id-index maps |
104
+ | `_selection` | Selected & highlighted maps, key cache, page-wide select helpers |
105
+ | `_preferences` | Table settings + layout / filter / view localStorage round-trip |
106
+ | `_focus` | Focus-coordinate state + multi-cell selection |
107
+ | `_virtualisation` | Row virtualisation window |
108
+ | `_queryValues` | Typeahead-echo cache for QuerySelect filter inputs |
109
+
110
+ Each sub-context is a class under `Types/Context/<Name>State.svelte.ts`
111
+ that takes a `deps` object of thunk-returning fields the parent reads
112
+ on demand. The thunks keep reactivity flowing without the sub-context
113
+ holding a parent reference — re-binding `TableContext._sorting` (or
114
+ swapping its parent) is impossible by construction.
115
+
116
+ The pattern when adding a new sub-context:
117
+
118
+ 1. Drop a `Types/Context/<Name>State.svelte.ts` file using
119
+ `FilteringState` as the structural template (`<Name>StateDeps`
120
+ interface, class with `$state` / `$derived` slots, `deps` thunks).
121
+ 2. Add a `_name: <Name>State` field on `TableContext` constructed with
122
+ the deps thunks closed over `this`.
123
+ 3. Add public passthrough getters/setters on `TableContext` for any
124
+ slot consumers reached for through the legacy field name.
125
+ 4. Write a `<Name>State.svelte.test.ts` test next to the new class.
126
+ Cover construction, each writer, and each derived read.
127
+ 5. If the sub-context surfaces a typed sub-API on the public `Table`
128
+ instance, wire a builder in `SubApis.svelte.ts` returning a frozen
129
+ object literal.
130
+
131
+ ---
132
+
133
+ ## The data repo
134
+
135
+ `TableContext.dataRepo` is the storage layer. Three concrete shapes:
136
+
137
+ - `LocalDataRepository` — handed a plain array; sorts/filters/paginates
138
+ in process.
139
+ - `BuiltInRemoteRepository` — fetches pages from a REST endpoint with
140
+ scoria's built-in query-string format.
141
+ - `CustomDataRepository` — built by `customRows({ fetch, … })` for
142
+ consumer-shaped query strings. flowms wraps this in
143
+ `createRemoteRowSource.ts` to map flowms's filter format.
144
+
145
+ The repo abstract base (`TableDataRepository.svelte.ts`) owns:
146
+
147
+ - `paginationState` (page index + size, persisted)
148
+ - the per-column `_filterModes` SvelteMap
149
+ - the `forceRefresh` callback hook (use `registerRefreshMethod` to
150
+ assign without firing; the setter form fires immediately and is
151
+ `@deprecated`)
152
+
153
+ If you add a new repo subclass, also extend the runtime discriminator
154
+ on `IDataRepository.paginationType` so existing callers can branch.
155
+
156
+ ---
157
+
158
+ ## Adding a new column kind
159
+
160
+ This is intentionally not a one-file change; the surface area below is
161
+ the irreducible minimum. Pick the four (sometimes five) sites carefully:
162
+
163
+ 1. **Def class** — `Types/Columns/Definitions/{Accessors,Display,…}/<Kind>Def.svelte.ts`.
164
+ Inherits from the matching category base (`AccessorDef` / `DisplayDef` / etc.).
165
+ 2. **Cell renderer** — `Body/Rows/Columns/<Kind>Cell.svelte`. Picked
166
+ by the dispatch chain in `TableColumn.svelte` (currently an
167
+ `instanceof` if/else; intentionally not registry-driven — see
168
+ follow-up F3 in `PROJECT_CONTEXT.md`).
169
+ 3. **Public option type** — add to `Types/Public/ColumnOptions.ts`.
170
+ Discriminated by the kind literal.
171
+ 4. **Factory method** — add a method on `ColumnFactory<TRow>` in
172
+ `ColumnFactory.svelte.ts`. The factory's `makeSpec(kind, def, …)`
173
+ tags the def with `def.kind = kind` automatically.
174
+ 5. **Registry entry** — add to `COLUMN_KIND_REGISTRY` in
175
+ `ColumnKindRegistry.ts`. Mark `category` and (if applicable) a
176
+ `defaultFilter` key. The companion test file enforces "every kind
177
+ has an entry".
178
+
179
+ A `setupDefaultColumnDefs` change is no longer needed; it now reads
180
+ from the registry.
181
+
182
+ ---
183
+
184
+ ## Load-bearing invariants
185
+
186
+ These each cost real time to discover the first time. Tagged for future
187
+ greppability.
188
+
189
+ ### `def.kind` is the canonical sort key
190
+
191
+ Every `ColumnDef` produced by the factory carries `def.kind`. Filter
192
+ wiring (`setupDefaultColumnDefs`), debug logs, and the public spec all
193
+ derive from it. A hand-constructed `ColumnDef` (test stub, legacy code)
194
+ without `kind` is tolerated but skips registry-driven defaults — the
195
+ consumer must assign `filterFn` themselves.
196
+
197
+ ### Filter-input `__queryValues` is gone; use `tableContext.queryValues`
198
+
199
+ The legacy `dataRepo.__queryValues: Map<string, unknown>` shared cache
200
+ has been replaced by the typed `QueryValuesState` sub-context. New
201
+ filter widgets should call `tableContext.queryValues.get(def)` /
202
+ `set(def, value)`. The sub-context keys by `ColumnDef` so the
203
+ historical `def.id` vs `label` keying bug class is structurally
204
+ prevented.
205
+
206
+ ### `reactiveDependencies` for async accessors
207
+
208
+ `col.display({ value: async (row) => row.foo.value })` will only
209
+ re-render when row identity changes — not when `row.foo.value` mutates,
210
+ because the reactive read happens after the `await` boundary. Pass a
211
+ `reactiveDependencies: (row) => [row.foo.value]` thunk on the column
212
+ options so the cell's `$derived` registers the dep synchronously.
213
+
214
+ ### Dropdowns do NOT move in the DOM
215
+
216
+ `Helpers/dropdownPortal.ts` flips a Svelecte dropdown to
217
+ `position: fixed` and lifts its stacking-context ancestor; it does NOT
218
+ re-parent the dropdown to `document.body`. Svelte 5's root-level event
219
+ delegation walks the live `parentNode` chain at dispatch time — moving
220
+ the element out of the mount target silently breaks mouse-click option
221
+ selection while keyboard Enter keeps working. See the header comment in
222
+ `dropdownPortal.ts` for the full reasoning.
223
+
224
+ ### Pinned cells skip horizontal scrollIntoView
225
+
226
+ In `Table.svelte`'s focus-restore effects, the scroll-into-view call
227
+ omits `inline: 'nearest'` for pinned cells. Sticky positioning makes
228
+ them visually always-on-screen, but their layout position is at the
229
+ real (often off-screen) column track; an unconditional inline scroll
230
+ would surface the underlying position and the table would "jump" when
231
+ focus lands on a pinned Actions button.
232
+
233
+ ### Clipboard copy must pass through `escapeForSpreadsheet`
234
+
235
+ Every path that writes to `navigator.clipboard.writeText` is required
236
+ to neutralise formula-injection prefixes via
237
+ `escapeForSpreadsheet` (or `encapsulate`, which calls it). The single-
238
+ cell `clipboard.copyCell` route and the bulk
239
+ `copyDataToClipboard` / `copyHighlightedRowsToClipboard` paths both
240
+ already use it; new copy paths must too.
241
+
242
+ ### `createTable` is async-effective; consume via reactive readers
243
+
244
+ `createTable({...})` returns the structured `Table` instance synchronously, but the underlying `TableContext` finishes initialising asynchronously: `setupDefaultColumnDefs` runs unawaited in the constructor, and `_selection`'s pending-changes buffer flushes on `tick()`. Two practical consequences:
245
+
246
+ - `table.columns.specs` and `table._context.columnDefs` are empty for the first microtask after `createTable` returns. Reactive readers (a `$derived` or `$effect`) re-evaluate when the columns land, so consumer code reads through these without thinking about it.
247
+ - `table.selection.set(rowId, row)` writes into the pending-changes buffer and flushes on `tick()`. `table.selection.size` returns the post-flush value the next time a reactive consumer reads it; a synchronous read on the same call frame returns the pre-flush value.
248
+
249
+ Direct synchronous read-back of state is therefore only safe inside an `$effect` or after `await tick()`. Unit tests at the public API surface need either a reactive harness or explicit `await tick()` between writes and assertions; that's why scoria's table tests use the `.svelte.test.ts` (browser-project) shape rather than plain server-mode unit tests.
250
+
251
+ ### Custom-row sources must defensively resolve their accessor thunks
252
+
253
+ A `customRows({ uri, defaultFilters, remoteFilters })` source can have
254
+ its accessor thunks called after the surrounding Svelte component has
255
+ torn down (a debounced fetch landing across a route change). Wrap the
256
+ thunk reads in try/catch and validate the returned shape — see
257
+ `flowms-svelte-web/src/lib/components/base/Table/createRemoteRowSource.ts`
258
+ for the canonical pattern.
259
+
260
+ ---
261
+
262
+ ## Testing model
263
+
264
+ | File pattern | Project | What it covers |
265
+ | --------------------------- | -------- | ------------------------------------------------------------------------------------- |
266
+ | `*.test.ts` (no `.svelte.`) | `server` | Pure-node unit tests (helpers, type contracts). Runs via `pnpm test`. |
267
+ | `*.svelte.test.ts` | `client` | Tests using `$state` / `$effect` from Svelte 5. Browser mode via Vitest + Playwright. |
268
+
269
+ The browser-mode runner has a Windows-specific chromium launch issue;
270
+ set `SCORIA_CHROMIUM_EXECUTABLE` to the headless-shell binary to work
271
+ around it (follow-up F1 in `PROJECT_CONTEXT.md`). The server project
272
+ covers the kind-registry contract, clipboard escape behaviour, and the
273
+ co-located decoupling tests.
274
+
275
+ Add new tests next to the code they cover, not in a global `__tests__`
276
+ folder.
277
+
278
+ ---
279
+
280
+ ## Storybook
281
+
282
+ Stories live under `src/stories/Table/`. Mount via the shared host
283
+ `_helpers/TableHost.svelte` which accepts a `CreateTableOptions` payload
284
+ and runs `createTable` inside instance init (module-scope `createTable`
285
+ would trip `effect_orphan`). The `Table/Table API` MDX page is the
286
+ companion reference; keep it in sync with significant public-API
287
+ changes.
288
+
289
+ ---
290
+
291
+ ## Cross-references
292
+
293
+ - `PROJECT_CONTEXT.md` (repo root) — project state, known issues and
294
+ follow-ups (Part A), plus the consumer migration guide (Part B).
295
+ - `agent-review/dev-changelog.md` — long-form record of the
296
+ decomposition phases.
297
+ - `src/lib/Components/Table/Types/Public/index.ts` — barrel for every
298
+ public type re-exported from `src/lib/index.ts`.