@lavalogic/scoria 0.40.13 → 0.40.15

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 (143) hide show
  1. package/README.md +58 -58
  2. package/dist/Components/AccordionGroup.svelte +35 -46
  3. package/dist/Components/AccordionGroupButton.svelte +68 -76
  4. package/dist/Components/Action.svelte +103 -135
  5. package/dist/Components/AlertModal.svelte +171 -190
  6. package/dist/Components/AppointmentCalendar/AppointmentCalendar.svelte +614 -697
  7. package/dist/Components/Base/BaseModal.svelte +90 -116
  8. package/dist/Components/Base/ContextWrapper.svelte +67 -72
  9. package/dist/Components/Base/Pagination.svelte +173 -196
  10. package/dist/Components/Base/Snippets.svelte +34 -36
  11. package/dist/Components/BigRadioSet.svelte +36 -38
  12. package/dist/Components/Bubble.svelte +78 -101
  13. package/dist/Components/Button.svelte +175 -208
  14. package/dist/Components/Checkbox.svelte +75 -86
  15. package/dist/Components/CollapsibleCard.svelte +72 -87
  16. package/dist/Components/CollapsibleMenuGroup.svelte +53 -57
  17. package/dist/Components/ConditionBuilder/ConditionBuilder.svelte +39 -64
  18. package/dist/Components/Contexts/ContextMenu.svelte +141 -156
  19. package/dist/Components/Contexts/ContextMenuDivider.svelte +15 -15
  20. package/dist/Components/Contexts/ContextMenuOption.svelte +53 -60
  21. package/dist/Components/Contexts/Toast.svelte +81 -86
  22. package/dist/Components/Contexts/ToastContainer.svelte +58 -62
  23. package/dist/Components/Contexts/Tooltip.svelte +36 -37
  24. package/dist/Components/DataMatrixIcon.svelte +95 -108
  25. package/dist/Components/DateInput.svelte +118 -146
  26. package/dist/Components/DatePicker.svelte +255 -294
  27. package/dist/Components/DesktopModal.svelte +70 -90
  28. package/dist/Components/Dial.svelte +167 -197
  29. package/dist/Components/DimensionInput.svelte +124 -169
  30. package/dist/Components/DragMenu/DragMenuHolder.svelte +98 -114
  31. package/dist/Components/DragMenu/DraggableCard.svelte +64 -76
  32. package/dist/Components/FileCard.svelte +21 -24
  33. package/dist/Components/FileUpload.svelte +190 -225
  34. package/dist/Components/GridInput.svelte +159 -196
  35. package/dist/Components/GridInputSet.svelte +24 -29
  36. package/dist/Components/HorizontalTabGroup.svelte +73 -89
  37. package/dist/Components/HorizontalTabGroupButton.svelte +40 -44
  38. package/dist/Components/Icon.svelte +99 -128
  39. package/dist/Components/Icons.js +357 -357
  40. package/dist/Components/InfoAlert.svelte +35 -38
  41. package/dist/Components/LoadingAnimation.svelte +691 -755
  42. package/dist/Components/LoadingModal.svelte +36 -44
  43. package/dist/Components/LoadingOverlay.svelte +22 -26
  44. package/dist/Components/Modal.svelte +204 -254
  45. package/dist/Components/MultiSelect.svelte +186 -216
  46. package/dist/Components/Nav/Nav.svelte +94 -110
  47. package/dist/Components/Nav/NavServiceMenuList.svelte +57 -61
  48. package/dist/Components/Nav/NavTab.svelte +58 -62
  49. package/dist/Components/Nav/ServiceMenuItem.svelte +103 -111
  50. package/dist/Components/Nav/ServicesNav.svelte +72 -79
  51. package/dist/Components/NumberInput.svelte +121 -155
  52. package/dist/Components/OptionCards.svelte +62 -73
  53. package/dist/Components/PageHeading.svelte +23 -25
  54. package/dist/Components/PasswordInput.svelte +69 -88
  55. package/dist/Components/PopoverButton.svelte +151 -0
  56. package/dist/Components/PopoverButton.svelte.d.ts +9 -0
  57. package/dist/Components/PopoverButtonProps.d.ts +21 -0
  58. package/dist/Components/PopoverButtonProps.js +1 -0
  59. package/dist/Components/Portal.svelte +29 -35
  60. package/dist/Components/ProgressBubble.svelte +91 -106
  61. package/dist/Components/QuerySelect.svelte +260 -346
  62. package/dist/Components/SidebarPanel.svelte +34 -36
  63. package/dist/Components/SingleSelect.svelte +196 -244
  64. package/dist/Components/StepButton.svelte +65 -70
  65. package/dist/Components/StepForm.svelte +115 -135
  66. package/dist/Components/Switch.svelte +57 -69
  67. package/dist/Components/Table/ARCHITECTURE.md +298 -298
  68. package/dist/Components/Table/Body/QuickSearchCell.svelte +578 -655
  69. package/dist/Components/Table/Body/Rows/ColumnHighlight.svelte +42 -51
  70. package/dist/Components/Table/Body/Rows/ColumnList.svelte +42 -58
  71. package/dist/Components/Table/Body/Rows/ColumnListRow.svelte +403 -442
  72. package/dist/Components/Table/Body/Rows/Columns/AccessorComponent.svelte +51 -78
  73. package/dist/Components/Table/Body/Rows/Columns/ActionsCell.svelte +165 -184
  74. package/dist/Components/Table/Body/Rows/Columns/BubbleCell.svelte +110 -123
  75. package/dist/Components/Table/Body/Rows/Columns/ExpandCell.svelte +134 -160
  76. package/dist/Components/Table/Body/Rows/Columns/FocusableImmutableCell.svelte +190 -231
  77. package/dist/Components/Table/Body/Rows/Columns/ProgressCell.svelte +82 -104
  78. package/dist/Components/Table/Body/Rows/Columns/TableAction.svelte +52 -87
  79. package/dist/Components/Table/Body/Rows/Columns/TableCheckbox.svelte +122 -150
  80. package/dist/Components/Table/Body/Rows/Columns/TableColumn.svelte +132 -159
  81. package/dist/Components/Table/Body/Rows/Columns/TableDatePicker.svelte +190 -236
  82. package/dist/Components/Table/Body/Rows/Columns/TableNumberInput.svelte +173 -211
  83. package/dist/Components/Table/Body/Rows/Columns/TableQuerySelect.svelte +204 -244
  84. package/dist/Components/Table/Body/Rows/Columns/TableRowSelectionCheckbox.svelte +140 -177
  85. package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte +188 -217
  86. package/dist/Components/Table/Body/Rows/Columns/TableTextInput.svelte +173 -193
  87. package/dist/Components/Table/Body/Rows/Columns/ValidityCell.svelte +166 -187
  88. package/dist/Components/Table/Body/Rows/TableRow.svelte +245 -288
  89. package/dist/Components/Table/Body/TableBody.svelte +141 -154
  90. package/dist/Components/Table/Headers/HighlightAllHeader.svelte +35 -41
  91. package/dist/Components/Table/Headers/SelectAllHeader.svelte +24 -30
  92. package/dist/Components/Table/Headers/TableHead.svelte +348 -402
  93. package/dist/Components/Table/Misc/ColumnPanel.svelte +37 -39
  94. package/dist/Components/Table/Misc/ColumnPanelModal.svelte +35 -39
  95. package/dist/Components/Table/Misc/FilterInput.svelte +556 -619
  96. package/dist/Components/Table/Misc/FilterPanel.svelte +170 -183
  97. package/dist/Components/Table/Misc/FilterPanelModal.svelte +25 -31
  98. package/dist/Components/Table/Misc/ShareViewModal.svelte +252 -281
  99. package/dist/Components/Table/Misc/SidePanel.svelte +32 -35
  100. package/dist/Components/Table/Misc/TableConfigurationModal.svelte +574 -629
  101. package/dist/Components/Table/Misc/TableFooter.svelte +326 -332
  102. package/dist/Components/Table/Misc/TableHorizontalBar.svelte +134 -140
  103. package/dist/Components/Table/Misc/TableSidebar.svelte +183 -198
  104. package/dist/Components/Table/Misc/TableViewDropdown.svelte +83 -103
  105. package/dist/Components/Table/Misc/ToolboxPanel.svelte +83 -100
  106. package/dist/Components/Table/Misc/ToolboxPanelModal.svelte +88 -107
  107. package/dist/Components/Table/Misc/UpdateViewOptionsModal.svelte +163 -190
  108. package/dist/Components/Table/NestedTable.svelte +102 -128
  109. package/dist/Components/Table/Table.svelte +531 -663
  110. package/dist/Components/Test/TestContextConsumer.svelte +11 -13
  111. package/dist/Components/TextArea.svelte +95 -130
  112. package/dist/Components/TextInput.svelte +125 -176
  113. package/dist/Components/ThreeStateRadio.svelte +111 -130
  114. package/dist/Components/Touch/BoolCard.svelte +33 -45
  115. package/dist/Components/Touch/DateModal.svelte +164 -204
  116. package/dist/Components/Touch/EditCard.svelte +48 -59
  117. package/dist/Components/Touch/InfoCard.svelte +28 -31
  118. package/dist/Components/Touch/InfoTextCard.svelte +26 -28
  119. package/dist/Components/Touch/ModalExplanation.svelte +15 -17
  120. package/dist/Components/Touch/NumberKeyboard.svelte +181 -218
  121. package/dist/Components/Touch/NumberModal.svelte +59 -79
  122. package/dist/Components/Touch/PlusMinusCard.svelte +43 -46
  123. package/dist/Components/Touch/SelectModal.svelte +111 -126
  124. package/dist/Components/Touch/SummaryCard.svelte +80 -93
  125. package/dist/Components/Touch/TextAreaModal.svelte +72 -94
  126. package/dist/Components/Touch/TextModal.svelte +77 -105
  127. package/dist/Components/Touch/TouchCard.svelte +31 -33
  128. package/dist/Components/Touch/TouchModal.svelte +214 -270
  129. package/dist/Components/Touch/UtilityButton.svelte +88 -109
  130. package/dist/Components/VerticalTabGroup.svelte +60 -78
  131. package/dist/Components/VerticalTabGroupButton.svelte +54 -58
  132. package/dist/Helpers/Datamatrix.d.ts +19 -19
  133. package/dist/Helpers/Datamatrix.js +291 -291
  134. package/dist/Types/Examples/ExampleModal.svelte +31 -35
  135. package/dist/index.d.ts +2 -0
  136. package/dist/index.js +1 -0
  137. package/dist/scss/_basics.scss +12 -12
  138. package/dist/scss/_colours.scss +134 -134
  139. package/dist/scss/_mixins.scss +3392 -3392
  140. package/dist/scss/_motion.scss +57 -57
  141. package/dist/scss/_sizing.scss +85 -85
  142. package/dist/scss/_transitions.scss +8 -8
  143. 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`.