@lavalogic/scoria 0.37.0 → 0.37.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.
@@ -309,6 +309,19 @@
309
309
 
310
310
  {#if columnDef}
311
311
  {@const reorderLabel = typeof columnDef.header === 'string' ? columnDef.header : columnDef.id}
312
+ <!--
313
+ The `<li>` is a structural container for the eye, pin selector,
314
+ label, and reorder buttons - none of which the user activates by
315
+ clicking the `<li>` itself. The `onkeydown` here only captures
316
+ Alt+ArrowUp / Alt+ArrowDown / Alt+Home / Alt+End bubbling up from
317
+ whichever inner control has focus, to drive the reorder shortcut;
318
+ it never makes the `<li>` itself a focusable interactive widget.
319
+ Giving the row a `role` like `treeitem` or `option` would
320
+ mis-describe the structure (this is a free-form list of cards,
321
+ not a tree or listbox), so the right call is to silence the
322
+ warning here rather than fake the semantics.
323
+ -->
324
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
312
325
  <li
313
326
  bind:this={rowEl}
314
327
  class="single-column"
@@ -4,13 +4,15 @@
4
4
  lang="ts"
5
5
  generics="T extends object, FilterValueType, RowIdType extends Primitive"
6
6
  >
7
+ import Portal from '../../../../Portal.svelte';
8
+ import type { AbstractDisplayDef } from '../../../Types/Columns/Definitions/Display/AbstractDisplayDef.svelte.js';
7
9
  import { TableContext } from '../../../Types/Context/TableContext.svelte.js';
8
10
  import { type Primitive } from '../../../Types/Context/TableInitOptions.js';
9
11
  import type { TableFocusDetails } from '../../../Types/Focus/TableFocusDetails.svelte.js';
10
12
  import { devCatch, isValidityTuple, loadingText } from '../../../../../Helpers/Helpers.svelte.js';
11
13
  import type { SelectOption } from '../../../../../Types/Internal/SelectOption.js';
12
14
  import { Validity } from '../../../../../Types/Internal/Validity.js';
13
- import { getContext } from 'svelte';
15
+ import { getContext, tick } from 'svelte';
14
16
  import type { FocusableImmutableCellProps } from './FocusableImmutableCellProps.js';
15
17
 
16
18
  const { cell, children }: FocusableImmutableCellProps<T, FilterValueType> = $props();
@@ -35,6 +37,46 @@
35
37
  tableContext._focus.getFocusDetailsFor(cell)
36
38
  );
37
39
 
40
+ // `modal` lives on every `AbstractDisplayDef` subclass (Display,
41
+ // Bubble, Progress, Validity), so a duck-typed read against the base
42
+ // `ColumnDef` is sufficient and avoids importing the entire
43
+ // AbstractDisplayDef class chain just to satisfy the type checker.
44
+ const modalComponent = $derived(
45
+ (cell.column.columnDef as AbstractDisplayDef<T, FilterValueType>).modal
46
+ );
47
+
48
+ let previousFocusElement: HTMLElement | undefined = $state();
49
+ let showModal = $state(false);
50
+
51
+ const thisId = $derived(
52
+ `${tableContext.tableName}-cell-${cell.row.visibleIndex}-${cell.column.id}`
53
+ );
54
+
55
+ // Match the open-modal state by row-object identity and column id so
56
+ // pagination / sort / filter does not silently re-target the modal at
57
+ // a different row that happens to occupy the same visible index.
58
+ $effect(() => {
59
+ const target = tableContext.openModalCell;
60
+ if (target !== null && target.row === cell.row.original && target.columnId === cell.column.id) {
61
+ previousFocusElement = document.activeElement as HTMLElement | undefined;
62
+ showModal = true;
63
+ } else {
64
+ showModal = false;
65
+ }
66
+ });
67
+
68
+ function onclose() {
69
+ tableContext.setOpenModalCell(null);
70
+
71
+ if (previousFocusElement?.id != thisId) {
72
+ tick()
73
+ .then(() => {
74
+ previousFocusElement?.focus();
75
+ })
76
+ .catch(devCatch);
77
+ }
78
+ }
79
+
38
80
  let validity: Validity = $state(Validity.Valid);
39
81
  $effect(() => {
40
82
  const validityPromise = focusDetails.validity;
@@ -56,11 +98,12 @@
56
98
  <!-- svelte-ignore a11y_mouse_events_have_key_events -->
57
99
  <!-- svelte-ignore a11y_no_static_element_interactions -->
58
100
  <div
59
- id={`${tableContext.tableName}-cell-${cell.row.visibleIndex}-${cell.column.id}`}
101
+ id={thisId}
60
102
  class:info={validity == Validity.Info}
61
103
  class:success={validity == Validity.Valid}
62
104
  class:warning={validity == Validity.Warning}
63
105
  class:error={validity == Validity.Invalid}
106
+ class:has-modal={!!modalComponent}
64
107
  tabIndex={hasValue ? 0 : undefined}
65
108
  class="focusable"
66
109
  style={focusDetails.outline}
@@ -85,6 +128,31 @@
85
128
  onmouseup={() => {
86
129
  tableContext.endFocus(focusDetails.coordinates);
87
130
  }}
131
+ onclick={() => {
132
+ // Open the per-cell modal if the column declares one. Click is
133
+ // the right trigger here (focus already started on mousedown);
134
+ // modal-less display cells fall through to the default focus
135
+ // behaviour with no extra handler.
136
+ if (modalComponent) {
137
+ tableContext.setOpenModalCell({
138
+ row: cell.row.original,
139
+ columnId: cell.column.id,
140
+ });
141
+ }
142
+ }}
143
+ onkeydown={(e) => {
144
+ // Keyboard parity with `onclick` above: when the cell has DOM
145
+ // focus (via `tabIndex`), Enter / Space opens the modal. Only
146
+ // fires for columns that declare a `modal`; non-modal display
147
+ // cells defer to the table's global keyboard navigation.
148
+ if (modalComponent && (e.key === 'Enter' || e.key === ' ')) {
149
+ e.preventDefault();
150
+ tableContext.setOpenModalCell({
151
+ row: cell.row.original,
152
+ columnId: cell.column.id,
153
+ });
154
+ }
155
+ }}
88
156
  >
89
157
  {#if hasValue}
90
158
  {#await visibleValue}
@@ -108,6 +176,22 @@
108
176
  </div>
109
177
  {/if}
110
178
  </div>
179
+ {#if showModal && modalComponent}
180
+ <!--
181
+ Mirrors the `ValidityCell` pattern: portal the modal to
182
+ `document.body` so the surrounding sticky / overflow:hidden
183
+ ancestors (header, pinned columns, virtualised viewport) never
184
+ clip it. The component receives the public
185
+ `DisplayModalProps<T>` shape `{ row, close }`.
186
+ -->
187
+ <Portal target={document.body}>
188
+ {@const ModalComponent = modalComponent}
189
+ <ModalComponent
190
+ row={cell.row.original}
191
+ close={onclose}
192
+ />
193
+ </Portal>
194
+ {/if}
111
195
 
112
196
  <style>.focusable,
113
197
  .checkbox-cell,
@@ -57,12 +57,13 @@
57
57
  let previousFocusElement: HTMLElement | undefined = $state();
58
58
  let showModal = $state(false);
59
59
 
60
+ // Match the open-modal state by row-object identity and column id.
61
+ // Identity-based comparison keeps the modal locked to the row it was
62
+ // opened on even when pagination / sort / filter changes the visible
63
+ // index of that row.
60
64
  $effect(() => {
61
- if (
62
- !!tableContext.openModalCoordinates &&
63
- focusDetails.coordinates.row === tableContext.openModalCoordinates.row &&
64
- focusDetails.coordinates.id === tableContext.openModalCoordinates.id
65
- ) {
65
+ const target = tableContext.openModalCell;
66
+ if (target !== null && target.row === cell.row.original && target.columnId === cell.column.id) {
66
67
  previousFocusElement = document.activeElement as HTMLElement | undefined;
67
68
  showModal = true;
68
69
  } else {
@@ -98,7 +99,7 @@
98
99
  );
99
100
 
100
101
  function onclose() {
101
- tableContext.setOpenModalCoordinates(null);
102
+ tableContext.setOpenModalCell(null);
102
103
 
103
104
  if (previousFocusElement?.id != thisId) {
104
105
  tick()
@@ -151,17 +152,29 @@
151
152
  {icon}
152
153
  onclick={() => {
153
154
  if (ColumnDef.modal) {
154
- tableContext.setOpenModalCoordinates(focusDetails.coordinates);
155
+ tableContext.setOpenModalCell({
156
+ row: cell.row.original,
157
+ columnId: cell.column.id,
158
+ });
155
159
  }
156
160
  }}
157
161
  />
158
162
  </div>
159
163
  {#if showModal}
160
164
  <Portal target={document.body}>
165
+ <!--
166
+ `ColumnDef.modal` is declared as
167
+ `Component<DisplayModalProps<TRow>>` on the public column
168
+ options, which exposes a stable `{ row, close }` prop
169
+ shape. Cell renderers used to hand a legacy
170
+ `{ cell, onclose, onsubmit }` triple here, which made the
171
+ public type lie about the runtime shape - migrated to
172
+ `{ row, close }` so consumer-authored modals can rely on
173
+ the documented type.
174
+ -->
161
175
  <ColumnDef.modal
162
- {cell}
163
- {onclose}
164
- onsubmit={onclose}
176
+ row={cell.row.original}
177
+ close={onclose}
165
178
  />
166
179
  </Portal>
167
180
  {/if}
@@ -1,5 +1,5 @@
1
1
  import type { ExampleItem } from '../../../Types/Examples/ExampleItem.js';
2
2
  import { type Component } from 'svelte';
3
- import type { DisplayDefModalProps } from '../Types/Columns/Definitions/Display/AbstractDisplayDef.svelte.js';
3
+ import type { DisplayModalProps } from '../Types/Public/ColumnOptions.js';
4
4
  import type { DefsWrapper } from '../Types/Columns/DefsWrapper.js';
5
- export declare function generateExampleItemColumnDefs(modal: Component<DisplayDefModalProps<ExampleItem>, object, ''>): DefsWrapper<ExampleItem>;
5
+ export declare function generateExampleItemColumnDefs(modal: Component<DisplayModalProps<ExampleItem>, object, ''>): DefsWrapper<ExampleItem>;
@@ -215,6 +215,11 @@ export function createColumnFactory() {
215
215
  ...commonProps(opts),
216
216
  accessorFn: opts.value,
217
217
  modal: opts.modal,
218
+ // Display columns have no implicit filter type (unlike the
219
+ // typed kinds where the factory pins one based on the
220
+ // column kind). Consumers opt in by passing the type they
221
+ // want the filter input rendered as.
222
+ filterValueType: opts.filterValueType,
218
223
  });
219
224
  return makeSpec('display', def);
220
225
  },
@@ -225,6 +230,7 @@ export function createColumnFactory() {
225
230
  ...commonProps(opts),
226
231
  accessorFn: opts.value,
227
232
  getColour: opts.colour,
233
+ filterValueType: opts.filterValueType,
228
234
  });
229
235
  return makeSpec('bubble', def);
230
236
  },
@@ -250,6 +256,7 @@ export function createColumnFactory() {
250
256
  ...commonProps(opts),
251
257
  accessorFn: tupleAccessor,
252
258
  getColour,
259
+ filterValueType: opts.filterValueType,
253
260
  });
254
261
  return makeSpec('progress', def);
255
262
  },
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import type { TableContext } from './Types/Context/TableContext.svelte.js';
13
13
  import type { Primitive } from './Types/Context/TableInitOptions.js';
14
- import type { ClipboardApi, ColumnsApi, FilteringApi, FocusApi, PaginationApi, PreferencesApi, SelectionApi, SortingApi } from './Types/Public/TableSubApis.js';
14
+ import type { ClipboardApi, ColumnsApi, FilteringApi, FocusApi, ModalApi, PaginationApi, PreferencesApi, SelectionApi, SortingApi } from './Types/Public/TableSubApis.js';
15
15
  import type { ColumnSpec } from './Types/Public/ColumnSpec.js';
16
16
  export declare function makeSelectionApi<TRow extends object, TRowId extends Primitive>(ctx: TableContext<TRow, TRowId>): SelectionApi<TRow, TRowId>;
17
17
  export declare function makeFilteringApi<TRow extends object, TRowId extends Primitive>(ctx: TableContext<TRow, TRowId>): FilteringApi<TRow>;
@@ -20,4 +20,5 @@ export declare function makePaginationApi<TRow extends object, TRowId extends Pr
20
20
  export declare function makeColumnsApi<TRow extends object, TRowId extends Primitive>(ctx: TableContext<TRow, TRowId>, specsById: ReadonlyMap<string, ColumnSpec<TRow>>): ColumnsApi<TRow>;
21
21
  export declare function makeFocusApi<TRow extends object, TRowId extends Primitive>(ctx: TableContext<TRow, TRowId>): FocusApi<TRow>;
22
22
  export declare function makePreferencesApi<TRow extends object, TRowId extends Primitive>(ctx: TableContext<TRow, TRowId>): PreferencesApi<TRow>;
23
+ export declare function makeModalApi<TRow extends object, TRowId extends Primitive>(ctx: TableContext<TRow, TRowId>): ModalApi<TRow>;
23
24
  export declare function makeClipboardApi<TRow extends object, TRowId extends Primitive>(ctx: TableContext<TRow, TRowId>): ClipboardApi<TRow>;
@@ -325,6 +325,27 @@ export function makePreferencesApi(ctx) {
325
325
  });
326
326
  }
327
327
  // ─────────────────────────────────────────────────────────────────────────
328
+ // Modal
329
+ // ─────────────────────────────────────────────────────────────────────────
330
+ export function makeModalApi(ctx) {
331
+ return Object.freeze({
332
+ get openCell() {
333
+ return ctx.openModalCell;
334
+ },
335
+ open(row, columnId) {
336
+ // The underlying setter takes the row by reference. Cells
337
+ // compare via object identity, so passing the same instance
338
+ // the table holds for the row is load-bearing - callers
339
+ // typically have that instance in hand from `table.rows`,
340
+ // `table.selection.map`, or a `col.actions` callback.
341
+ ctx.setOpenModalCell({ row, columnId });
342
+ },
343
+ close() {
344
+ ctx.setOpenModalCell(null);
345
+ },
346
+ });
347
+ }
348
+ // ─────────────────────────────────────────────────────────────────────────
328
349
  // Clipboard
329
350
  // ─────────────────────────────────────────────────────────────────────────
330
351
  export function makeClipboardApi(ctx) {
@@ -334,8 +334,6 @@
334
334
  const centreColumns = $derived(tableContext.table.getCenterVisibleLeafColumns());
335
335
  const rightColumns = $derived(tableContext.table.getRightVisibleLeafColumns());
336
336
 
337
- const visibleColumns = $derived([...leftColumns, ...centreColumns, ...rightColumns]);
338
-
339
337
  // CSS Grid template for the table.
340
338
  //
341
339
  // - First track is the highlight column (always present, fixed at
@@ -1,37 +1,20 @@
1
1
  import type { Component } from 'svelte';
2
+ import type { DisplayModalProps } from '../../../Public/ColumnOptions.js';
2
3
  import type { DisplayType } from '../../DisplayType.js';
3
- import type { TableCell } from '../../TableCell.js';
4
4
  import { ColumnDef, type ColumnDefProps } from '../ColumnDef.svelte.js';
5
- /**
6
- * Props passed to the modal a display column may open.
7
- *
8
- * - `cell`: the source cell, used to read row context and the row value.
9
- * - `onclose`: invoked when the user dismisses without committing.
10
- * - `onsubmit`: invoked when the user commits.
11
- *
12
- * Note: neither callback carries a payload today (engineering F-secondary);
13
- * callers persist any state through the cell's row reference.
14
- */
15
- export interface DisplayDefModalProps<T extends object, in FilterValueType = never, out FilterOptionsType = unknown> {
16
- /** The source cell. */
17
- cell: TableCell<T, FilterValueType, FilterOptionsType>;
18
- /** Invoked on dismissal without commit. */
19
- onclose: () => void;
20
- /** Invoked on commit. */
21
- onsubmit: () => void;
22
- }
23
5
  /**
24
6
  * Construction-time props for every `AbstractDisplayDef` subclass.
25
7
  *
26
- * Note: `modal` is typed with `Component<any, any, any>` because the
27
- * dynamic-dispatch site cannot statically know the consumer's full
28
- * exports / events / slots triple. The narrowing is delegated to the
29
- * caller via `DisplayDefModalProps`.
8
+ * `modal` is typed against the public `DisplayModalProps<T>` shape - the
9
+ * cell renderer hands the modal component `{ row, close }` at runtime, and
10
+ * the public type advertises that same shape via `col.display({ modal })`
11
+ * and `col.validity({ modal })`. The dynamic-dispatch slot uses
12
+ * `Component<DisplayModalProps<T>>` with `any` filling the events / slots
13
+ * channels because the consumer-supplied component declares those itself.
30
14
  */
31
15
  export interface AbstractDisplayDefProps<T extends object, in FilterFnType, out FilterOptionsType> extends ColumnDefProps<T, FilterFnType, FilterOptionsType> {
32
- /** Optional Svelte component opened as a modal from the cell. The
33
- * `any` triple is intentional: see class JSDoc. */
34
- modal?: Component<any, any, any>;
16
+ /** Optional Svelte component opened as a modal from the cell. */
17
+ modal?: Component<DisplayModalProps<T>, any, any>;
35
18
  }
36
19
  /**
37
20
  * `AbstractDisplayDef<T, FilterValueType, FilterOptionsType>` is the
@@ -41,11 +24,9 @@ export interface AbstractDisplayDefProps<T extends object, in FilterFnType, out
41
24
  *
42
25
  * @remarks
43
26
  *
44
- * `modal` is `Component<any, any, any>` rather than a narrower generic
45
- * because the consumer-supplied modal component declares its own
46
- * exports / events / slots and the cell dispatch site cannot statically
47
- * know the full triple. Callers should accept `DisplayDefModalProps`
48
- * as props and ignore the slot / event channels.
27
+ * `modal` accepts any Svelte component whose props match the public
28
+ * `DisplayModalProps<T>` shape. The cell renderers hand it
29
+ * `{ row: T; close: () => void }` at runtime.
49
30
  */
50
31
  export declare abstract class AbstractDisplayDef<T extends object, in FilterValueType = never, out FilterOptionsType = unknown> extends ColumnDef<T, FilterValueType, FilterOptionsType> {
51
32
  /** Inner discriminator literal narrowed by each leaf subclass. */
@@ -55,5 +36,5 @@ export declare abstract class AbstractDisplayDef<T extends object, in FilterValu
55
36
  readonly columnType: "Display";
56
37
  /** Optional Svelte component opened as a modal from the cell. */
57
38
  readonly modal: // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
- Component<DisplayDefModalProps<T>, any, any> | undefined;
39
+ Component<DisplayModalProps<T>, any, any> | undefined;
59
40
  }
@@ -8,11 +8,9 @@ import { ColumnDef } from '../ColumnDef.svelte.js';
8
8
  *
9
9
  * @remarks
10
10
  *
11
- * `modal` is `Component<any, any, any>` rather than a narrower generic
12
- * because the consumer-supplied modal component declares its own
13
- * exports / events / slots and the cell dispatch site cannot statically
14
- * know the full triple. Callers should accept `DisplayDefModalProps`
15
- * as props and ignore the slot / event channels.
11
+ * `modal` accepts any Svelte component whose props match the public
12
+ * `DisplayModalProps<T>` shape. The cell renderers hand it
13
+ * `{ row: T; close: () => void }` at runtime.
16
14
  */
17
15
  export class AbstractDisplayDef extends ColumnDef {
18
16
  constructor(props) {
@@ -9,7 +9,6 @@ import type { ColumnDefSet } from '../Columns/Definitions/ColumnDefSet.js';
9
9
  import type { DefsWrapper } from '../Columns/DefsWrapper.js';
10
10
  import type { VisibilityState } from '../Columns/VisibilityState.js';
11
11
  import type { CellCoordinates } from '../Coordinates/CellCoordinates.js';
12
- import type { CellCoordinatesWithoutColumn } from '../Coordinates/CellCoordinatesWithoutColumn.js';
13
12
  import type { VisualColumnIndex } from '../Coordinates/ColumnIndices.js';
14
13
  import type { IDataRepository } from '../DataRepository/IDataRepository.js';
15
14
  import { SortingState } from './SortingState.svelte.js';
@@ -230,9 +229,28 @@ export declare class TableContext<T extends object, RowIdType extends Primitive>
230
229
  */
231
230
  get columnVisibility(): VisibilityState;
232
231
  set columnVisibility(v: VisibilityState);
233
- private _openModalCoordinates;
234
- get openModalCoordinates(): CellCoordinatesWithoutColumn | null;
235
- set openModalCoordinates(v: CellCoordinatesWithoutColumn | null);
232
+ /**
233
+ * Identifies the cell whose `col.validity({ modal })` /
234
+ * `col.display({ modal })` modal is currently open, or `null` when no
235
+ * modal is open.
236
+ *
237
+ * Keyed on the **row object reference**, not on visible index, so the
238
+ * open-modal state is stable across pagination, sort, and filter
239
+ * changes - the cell renderer matches via `cell.row.original ===
240
+ * openModalCell.row`. Visible-index-based keying (the old
241
+ * `CellCoordinatesWithoutColumn` shape) silently re-targeted the modal
242
+ * to whichever row happened to occupy the same index after a page
243
+ * change.
244
+ */
245
+ private _openModalCell;
246
+ get openModalCell(): {
247
+ row: T;
248
+ columnId: string;
249
+ } | null;
250
+ set openModalCell(v: {
251
+ row: T;
252
+ columnId: string;
253
+ } | null);
236
254
  private _shownActionsPopup;
237
255
  get shownActionsPopup(): symbol | undefined;
238
256
  set shownActionsPopup(v: symbol | undefined);
@@ -486,8 +504,18 @@ export declare class TableContext<T extends object, RowIdType extends Primitive>
486
504
  readonly addInvalidValue: (item: T) => void | Promise<void>;
487
505
  /** Delegating accessor preserved for backwards compatibility. */
488
506
  readonly removeInvalidValue: (item: T) => void | Promise<void>;
489
- /** Stores or clears the coordinates of the open per-cell modal. */
490
- readonly setOpenModalCoordinates: (coordinates: CellCoordinatesWithoutColumn | null) => void;
507
+ /**
508
+ * Stores or clears the cell whose per-cell modal is currently open.
509
+ *
510
+ * Pass `{ row, columnId }` to open; pass `null` to close. The `row`
511
+ * value must be the same object reference the table holds for that row
512
+ * (`cell.row.original`) - identity comparison is what the cell
513
+ * renderers use to decide which one of them should render the modal.
514
+ */
515
+ readonly setOpenModalCell: (cell: {
516
+ row: T;
517
+ columnId: string;
518
+ } | null) => void;
491
519
  /** Delegating accessor preserved for backwards compatibility. */
492
520
  readonly toggleHighlightedRow: (item: T) => void | Promise<void>;
493
521
  /** Delegating accessor preserved for backwards compatibility. */
@@ -673,12 +673,25 @@ export class TableContext {
673
673
  set columnVisibility(v) {
674
674
  this._columnLayout.columnVisibility = v;
675
675
  }
676
- _openModalCoordinates = $state(null);
677
- get openModalCoordinates() {
678
- return this._openModalCoordinates;
676
+ /**
677
+ * Identifies the cell whose `col.validity({ modal })` /
678
+ * `col.display({ modal })` modal is currently open, or `null` when no
679
+ * modal is open.
680
+ *
681
+ * Keyed on the **row object reference**, not on visible index, so the
682
+ * open-modal state is stable across pagination, sort, and filter
683
+ * changes - the cell renderer matches via `cell.row.original ===
684
+ * openModalCell.row`. Visible-index-based keying (the old
685
+ * `CellCoordinatesWithoutColumn` shape) silently re-targeted the modal
686
+ * to whichever row happened to occupy the same index after a page
687
+ * change.
688
+ */
689
+ _openModalCell = $state(null);
690
+ get openModalCell() {
691
+ return this._openModalCell;
679
692
  }
680
- set openModalCoordinates(v) {
681
- this._openModalCoordinates = v;
693
+ set openModalCell(v) {
694
+ this._openModalCell = v;
682
695
  }
683
696
  // Per-table identifier of the ActionsCell that currently has its popup open.
684
697
  // Lives on the context rather than at module scope so multiple tables on the
@@ -1166,13 +1179,6 @@ export class TableContext {
1166
1179
  },
1167
1180
  };
1168
1181
  };
1169
- const makeCellFromId = (columnId) => {
1170
- const def = this.columnDefsById.get(columnId);
1171
- if (!def) {
1172
- throw new Error(`Could not find column with id ${columnId}`);
1173
- }
1174
- return makeRow(def);
1175
- };
1176
1182
  // Iterating `this.columnDefs.filter(...)` instead of
1177
1183
  // `this.columnPinning.left?.keys()` puts pinned-left body
1178
1184
  // cells in declaration order (matching the header rendering
@@ -1235,9 +1241,16 @@ export class TableContext {
1235
1241
  removeInvalidValue = (item) => {
1236
1242
  return this._selection.removeInvalidValue(item);
1237
1243
  };
1238
- /** Stores or clears the coordinates of the open per-cell modal. */
1239
- setOpenModalCoordinates = (coordinates) => {
1240
- this.openModalCoordinates = coordinates ? { ...coordinates } : null;
1244
+ /**
1245
+ * Stores or clears the cell whose per-cell modal is currently open.
1246
+ *
1247
+ * Pass `{ row, columnId }` to open; pass `null` to close. The `row`
1248
+ * value must be the same object reference the table holds for that row
1249
+ * (`cell.row.original`) - identity comparison is what the cell
1250
+ * renderers use to decide which one of them should render the modal.
1251
+ */
1252
+ setOpenModalCell = (cell) => {
1253
+ this.openModalCell = cell ? { row: cell.row, columnId: cell.columnId } : null;
1241
1254
  };
1242
1255
  /** Delegating accessor preserved for backwards compatibility. */
1243
1256
  toggleHighlightedRow = (item) => {
@@ -1813,13 +1826,6 @@ export class TableContext {
1813
1826
  },
1814
1827
  };
1815
1828
  };
1816
- const makeCellFromId = (columnId) => {
1817
- const def = this.columnDefsById.get(columnId);
1818
- if (!def) {
1819
- throw new Error(`Could not make cell from id ${columnId}: Coloumn Def was not found`);
1820
- }
1821
- return makeRow(def);
1822
- };
1823
1829
  // Visibility filter applied to every cell collection so the cell
1824
1830
  // count in TableRow stays in sync with the visible-column count
1825
1831
  // that drives `--cols` in Table.svelte. Without this, a hidden
@@ -132,6 +132,21 @@ export interface DisplayColumnOptions<TRow extends object, TValue = unknown> ext
132
132
  /** Override the auto-generated id. Rare; only useful when the consumer
133
133
  * needs a stable id for preset persistence or test selectors. */
134
134
  idHint?: string;
135
+ /**
136
+ * Filter-input shape for this column. Display columns have no
137
+ * implicit filter type (unlike `col.number` / `col.date` etc.
138
+ * which derive theirs from the column kind). Set this when the
139
+ * displayed `value` is a computed string but the underlying field
140
+ * the server filters on is a number / date / select - this keeps
141
+ * the typed filter UI (e.g. number ranges, date pickers) for
142
+ * foreign-key columns that show a human-readable name.
143
+ *
144
+ * When omitted, the filter row renders a generic string input.
145
+ *
146
+ * The filter targets the column id (use `idHint` to set a stable
147
+ * id matching the server field name).
148
+ */
149
+ filterValueType?: FilterValueType;
135
150
  }
136
151
  /** Options for `col.bubble({ header, value, colour, ... })`. Renders one or
137
152
  * more coloured pill bubbles per row. */
@@ -143,6 +158,12 @@ export interface BubbleColumnOptions<TRow extends object, TValue = unknown> exte
143
158
  * so multi-bubble cells can colour each independently. */
144
159
  colour: (row: TRow, index: number, value: TValue) => BubbleColour | Promise<BubbleColour>;
145
160
  idHint?: string;
161
+ /**
162
+ * Filter-input shape for this column. See `DisplayColumnOptions.filterValueType`
163
+ * - same semantics, applied to bubble cells. Common case: a tag-bubble column
164
+ * where the displayed values are strings but the server filter is a select.
165
+ */
166
+ filterValueType?: FilterValueType;
146
167
  }
147
168
  /** Options for `col.progress({ header, value, ... })`. Renders a horizontal
148
169
  * progress bar from a numeric ratio. */
@@ -152,6 +173,12 @@ export interface ProgressColumnOptions<TRow extends object> extends BaseColumnOp
152
173
  /** Optional colour override. Defaults to scoria's progress colour ramp. */
153
174
  colour?: (row: TRow, index: number, fraction: number) => ColourSet | Promise<ColourSet>;
154
175
  idHint?: string;
176
+ /**
177
+ * Filter-input shape for this column. See `DisplayColumnOptions.filterValueType`
178
+ * - same semantics. Progress columns most commonly filter on a numeric
179
+ * "percent complete" field, in which case set this to `'Number'`.
180
+ */
181
+ filterValueType?: FilterValueType;
155
182
  }
156
183
  /** Options for `col.validity({ header, ... })`. Renders the row's overall
157
184
  * validity indicator and (optionally) opens a modal with the validity
@@ -228,9 +255,15 @@ export interface FilterOnlyColumnOptions<TRow extends object, K extends keyof TR
228
255
  * omit this and rely on the backend instead. */
229
256
  customFilter?: CustomFilter<TValue>;
230
257
  }
231
- /** Props passed to a `DisplayColumnOptions.modal` / `ValidityColumnOptions.modal`
232
- * Component. Mirrors the existing `DisplayDefModalProps` shape so consumers
233
- * can reuse their existing modal components unchanged. */
258
+ /**
259
+ * Props passed to a `DisplayColumnOptions.modal` / `ValidityColumnOptions.modal`
260
+ * Component when the table opens it (either via the column's built-in click
261
+ * handler or imperatively through `table.modal.open(row, columnId)`).
262
+ *
263
+ * Consumer-authored modals should declare their props as
264
+ * `DisplayModalProps<MyRow>` and read `row` for the row context and `close()`
265
+ * to dismiss themselves.
266
+ */
234
267
  export interface DisplayModalProps<TRow extends object> {
235
268
  row: TRow;
236
269
  /** Closes the modal. */
@@ -1,5 +1,5 @@
1
1
  import type { Primitive } from '../Context/TableInitOptions.js';
2
- import type { ClipboardApi, ColumnsApi, FilteringApi, FocusApi, PaginationApi, PreferencesApi, SelectionApi, SortingApi } from './TableSubApis.js';
2
+ import type { ClipboardApi, ColumnsApi, FilteringApi, FocusApi, ModalApi, PaginationApi, PreferencesApi, SelectionApi, SortingApi } from './TableSubApis.js';
3
3
  /**
4
4
  * Public Table instance returned by `createTable(...)`. Mount the underlying
5
5
  * Table component via `<Table {table} />`; the structured object below is
@@ -32,6 +32,7 @@ export interface Table<TRow extends object, TRowId extends Primitive> {
32
32
  readonly focus: FocusApi<TRow>;
33
33
  readonly preferences: PreferencesApi<TRow>;
34
34
  readonly clipboard: ClipboardApi<TRow>;
35
+ readonly modal: ModalApi<TRow>;
35
36
  /** Trigger a refetch (remote / custom) or a recompute (local). */
36
37
  reload(): Promise<void>;
37
38
  /** Tear down any subscriptions and abort in-flight requests. */
@@ -155,3 +155,36 @@ export interface ClipboardApi<TRow extends object> {
155
155
  /** Copy a single cell. */
156
156
  copyCell(row: TRow, columnId: string): Promise<void>;
157
157
  }
158
+ /**
159
+ * Imperative control of the per-cell modal that display / validity columns
160
+ * may declare. Use cases:
161
+ *
162
+ * - Programmatically open a row's validity modal from a callback (e.g. when
163
+ * toggling `held=true` without a reason on a hold-and-release editor).
164
+ * - Close any open modal as part of a higher-level cancellation flow.
165
+ * - Read the current open-modal state to decide whether a pending action
166
+ * should defer until the user dismisses the modal.
167
+ *
168
+ * The open-modal state is keyed on the row's object identity, so it
169
+ * survives pagination, sort, and filter changes - if the row is currently
170
+ * off-page, the modal stays "queued" and renders as soon as the cell
171
+ * mounts (typically when the user navigates back to that page). To open
172
+ * the modal at a stable rowId rather than at a row reference you already
173
+ * have, look the row up via `table.rows` / `table.selection.map` first.
174
+ *
175
+ * Modal-less display cells silently ignore `open()` (no error, no state
176
+ * change) so consumers do not need to guard the call site against
177
+ * column-shape mismatches.
178
+ */
179
+ export interface ModalApi<TRow extends object> {
180
+ /** The cell whose modal is currently open, or `null` when none is. Reactive. */
181
+ readonly openCell: {
182
+ readonly row: TRow;
183
+ readonly columnId: string;
184
+ } | null;
185
+ /** Open the per-cell modal at the given row + column. No-op when the
186
+ * column does not declare a `modal` component. */
187
+ open(row: TRow, columnId: string): void;
188
+ /** Close any open modal. No-op when none is open. */
189
+ close(): void;
190
+ }
@@ -17,4 +17,4 @@ export type { SelectionOptions } from './SelectionOptions.js';
17
17
  export type { Table } from './Table.js';
18
18
  export type { TableInitialState } from './TableInitialState.js';
19
19
  export type { TablePersistence, TablePersistenceObject, TableStorageAdapter, } from './TablePersistence.js';
20
- export type { ClipboardApi, ColumnsApi, FilteringApi, FocusApi, PaginationApi, PreferencesApi, SavedPreset, SelectionApi, SortingApi, } from './TableSubApis.js';
20
+ export type { ClipboardApi, ColumnsApi, FilteringApi, FocusApi, ModalApi, PaginationApi, PreferencesApi, SavedPreset, SelectionApi, SortingApi, } from './TableSubApis.js';
@@ -23,7 +23,7 @@ import { LocalTableDataRepository } from './Types/DataRepository/LocalTableDataR
23
23
  import { TableContext } from './Types/Context/TableContext.svelte.js';
24
24
  import { ToolbarPosition } from './Types/Toolbar/ToolbarPosition.js';
25
25
  import { createColumnFactory, unwrapColumnSpec } from './ColumnFactory.svelte.js';
26
- import { makeClipboardApi, makeColumnsApi, makeFilteringApi, makeFocusApi, makePaginationApi, makePreferencesApi, makeSelectionApi, makeSortingApi, } from './SubApis.svelte.js';
26
+ import { makeClipboardApi, makeColumnsApi, makeFilteringApi, makeFocusApi, makeModalApi, makePaginationApi, makePreferencesApi, makeSelectionApi, makeSortingApi, } from './SubApis.svelte.js';
27
27
  // ─────────────────────────────────────────────────────────────────────────
28
28
  // Public entry point
29
29
  // ─────────────────────────────────────────────────────────────────────────
@@ -121,6 +121,7 @@ export function createTable(options) {
121
121
  focus: makeFocusApi(ctx),
122
122
  preferences: makePreferencesApi(ctx),
123
123
  clipboard: makeClipboardApi(ctx),
124
+ modal: makeModalApi(ctx),
124
125
  reload() {
125
126
  return ctx.dataRepo.reload?.() ?? Promise.resolve();
126
127
  },
@@ -4,28 +4,28 @@
4
4
  import DesktopModal from '../../Components/DesktopModal.svelte';
5
5
  import { cssLength } from '../../Helpers/Helpers.svelte.js';
6
6
  import Switch from '../../Components/Switch.svelte';
7
- import type { DisplayDefModalProps } from '../../Components/Table/Types/Columns/Definitions/Display/AbstractDisplayDef.svelte.js';
7
+ import type { DisplayModalProps } from '../../Components/Table/Types/Public/ColumnOptions.js';
8
8
  import type { ExampleItem } from './ExampleItem.js';
9
9
 
10
- const { cell, onclose, onsubmit }: DisplayDefModalProps<ExampleItem> = $props();
10
+ const { row, close }: DisplayModalProps<ExampleItem> = $props();
11
11
  </script>
12
12
 
13
13
  <DesktopModal
14
14
  width={cssLength('1220px')}
15
15
  headerLabel="Hello World"
16
- {onclose}
17
- {onsubmit}
18
- buttons={[{ label: 'Close', callback: onclose }]}
16
+ onclose={close}
17
+ onsubmit={close}
18
+ buttons={[{ label: 'Close', callback: close }]}
19
19
  >
20
20
  <div class="wrapper">
21
- This is an Example modal where you can do additional work through cell.row.original or a custom
22
- context
21
+ This is an Example modal where you can do additional work through the `row` prop or a custom
22
+ context.
23
23
  <div>
24
24
  <Switch
25
- label="{cell.row.original.id} checked?"
26
- value={cell.row.original.isChecked}
25
+ label="{row.id} checked?"
26
+ value={row.isChecked}
27
27
  onchange={() => {
28
- cell.row.original.isChecked = !cell.row.original.isChecked;
28
+ row.isChecked = !row.isChecked;
29
29
  }}
30
30
  />
31
31
  </div>
@@ -1,5 +1,5 @@
1
- import type { DisplayDefModalProps } from '../../Components/Table/Types/Columns/Definitions/Display/AbstractDisplayDef.svelte.js';
1
+ import type { DisplayModalProps } from '../../Components/Table/Types/Public/ColumnOptions.js';
2
2
  import type { ExampleItem } from './ExampleItem.js';
3
- declare const ExampleModal: import("svelte").Component<DisplayDefModalProps<ExampleItem, never, unknown>, {}, "">;
3
+ declare const ExampleModal: import("svelte").Component<DisplayModalProps<ExampleItem>, {}, "">;
4
4
  type ExampleModal = ReturnType<typeof ExampleModal>;
5
5
  export default ExampleModal;
package/dist/index.d.ts CHANGED
@@ -104,7 +104,7 @@ export type { TabGroupButtonProps } from './Components/TabGroupButtonProps.js';
104
104
  export { default as Table, type TableProps } from './Components/Table/Table.svelte';
105
105
  export { createTable, customRows, localRows, remoteRows, } from './Components/Table/createTable.svelte.js';
106
106
  export type { ColumnSpecInternals } from './Components/Table/ColumnFactory.svelte.js';
107
- export type { ActionsColumnOptions, AnyEditEvent, BaseColumnOptions, BooleanKeyOf, BubbleColumnOptions, CheckboxColumnOptions, ClipboardApi, ColumnFactory as TableColumnFactory, ColumnKind, ColumnSpec, ColumnsApi, CreateTableOptions, CustomRowSource, DateColumnOptions, DateKeyOf, DisplayColumnOptions, DisplayModalProps, ExpandColumnOptions, FilterOnlyColumnOptions, FilteringApi, FocusApi, LiteralRowIdKey, LocalRowSource, NumberColumnOptions, NumberKeyOf, PaginationApi, PreferencesApi, ProgressColumnOptions, QueryColumnOptions, RemoteRowSource, RowAction, RowFetchRequest, RowFetchResponse, RowSelectionColumnOptions, RowSource, SavedPreset, SelectColumnOptions, SelectionApi, SelectionOptions, SortingApi, StringKeyOf, Table as TableInstance, TableInitialState, TablePersistence, TablePersistenceObject, TableStorageAdapter, TextColumnOptions, ValidityColumnOptions, } from './Components/Table/Types/Public/index.js';
107
+ export type { ActionsColumnOptions, AnyEditEvent, BaseColumnOptions, BooleanKeyOf, BubbleColumnOptions, CheckboxColumnOptions, ClipboardApi, ColumnFactory as TableColumnFactory, ColumnKind, ColumnSpec, ColumnsApi, CreateTableOptions, CustomRowSource, DateColumnOptions, DateKeyOf, DisplayColumnOptions, DisplayModalProps, ExpandColumnOptions, FilterOnlyColumnOptions, FilteringApi, FocusApi, LiteralRowIdKey, LocalRowSource, ModalApi, NumberColumnOptions, NumberKeyOf, PaginationApi, PreferencesApi, ProgressColumnOptions, QueryColumnOptions, RemoteRowSource, RowAction, RowFetchRequest, RowFetchResponse, RowSelectionColumnOptions, RowSource, SavedPreset, SelectColumnOptions, SelectionApi, SelectionOptions, SortingApi, StringKeyOf, Table as TableInstance, TableInitialState, TablePersistence, TablePersistenceObject, TableStorageAdapter, TextColumnOptions, ValidityColumnOptions, } from './Components/Table/Types/Public/index.js';
108
108
  export { type CellCoordinates } from './Components/Table/Types/Coordinates/CellCoordinates.js';
109
109
  export { type ColumnPinningState } from './Components/Table/Types/Columns/ColumnPinningState.js';
110
110
  export { type ColumnSizingState } from './Components/Table/Types/Columns/ColumnSizingState.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lavalogic/scoria",
3
3
  "description": "Svelte components used for the FloWMS Web Frontend",
4
- "version": "0.37.0",
4
+ "version": "0.37.2",
5
5
  "publishConfig": {
6
6
  "access": "restricted"
7
7
  },