@lavalogic/scoria 0.37.31 → 0.37.33

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.
@@ -179,29 +179,10 @@
179
179
  // svelecte always emits the full option object regardless of
180
180
  // `valueAsObject`. Map to the shape the discriminated variant
181
181
  // promised: primitive for AsPrimitive, object for AsObject.
182
- if (typeof window !== 'undefined') {
183
- // eslint-disable-next-line no-console
184
- console.debug('%c[scoria/SingleSelect] svelecte onChange fired', 'color:#a0a', {
185
- inputId,
186
- name,
187
- newValue,
188
- newValueType: typeof newValue,
189
- valueAsObject,
190
- valueProp,
191
- });
192
- }
193
182
  if (valueAsObject) {
194
183
  void (onchange as (v: V | null) => void | Promise<void>)(newValue);
195
184
  } else {
196
185
  const primitive = newValue == null ? null : newValue[valueProp];
197
- if (typeof window !== 'undefined') {
198
- // eslint-disable-next-line no-console
199
- console.debug(
200
- '%c[scoria/SingleSelect] calling outer onchange with primitive',
201
- 'color:#a0a',
202
- { primitive }
203
- );
204
- }
205
186
  // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
206
187
  void (onchange as (v: V[VP] | null) => void | Promise<void>)(primitive);
207
188
  }
@@ -145,32 +145,8 @@
145
145
  // V (from `value: T`) clashes with the option type
146
146
  // (`SelectOption<T>`); the casts let `V` be inferred
147
147
  // from the explicit `onchange` annotation.
148
- if (dev) {
149
- console.debug(
150
- '%c[scoria/TableSelect] onchange fired',
151
- 'color:#0a0',
152
- {
153
- columnId: cell.column.columnDef.id,
154
- newValue,
155
- newValueType: typeof newValue,
156
- oldValue: value,
157
- optional,
158
- willEdit: newValue !== value && (optional || newValue != null),
159
- }
160
- );
161
- }
162
148
  if (newValue !== value) {
163
149
  if (optional || newValue != null) {
164
- if (dev) {
165
- console.debug(
166
- '%c[scoria/TableSelect] calling tableContext.onEditCell',
167
- 'color:#0a0',
168
- {
169
- coords: $state.snapshot(editable.focusDetails.coordinates),
170
- value: newValue,
171
- }
172
- );
173
- }
174
150
  tableContext
175
151
  .onEditCell(cell.row.original, editable.focusDetails.coordinates, newValue ?? null)
176
152
  .catch(devCatch);
@@ -376,6 +376,16 @@ export function createColumnFactory() {
376
376
  filterValueType: opts.filterValueType ?? FilterValueType.String,
377
377
  getSortingFn: opts.getSortingFn,
378
378
  getOptions: normalisedOptions,
379
+ // Forward reactiveDependencies onto the def so
380
+ // `makeRow.getValue` can read them synchronously before
381
+ // calling the (possibly async) accessor. Without this,
382
+ // reactive reads that happen only after `await` inside
383
+ // the consumer's `value` callback are not tracked by the
384
+ // cell's `$derived`, and downstream display cells never
385
+ // refresh when a sibling field is edited (visible on the
386
+ // Receipt pod table: Title / UoM did not reload after the
387
+ // SKU select changed `item.receiptLineId`).
388
+ reactiveDependencies: opts.reactiveDependencies,
379
389
  });
380
390
  return makeSpec('display', def);
381
391
  },
@@ -92,6 +92,17 @@ export interface ColumnDefProps<T extends object, in FilterFnType, out OptionsTy
92
92
  * advanced filter panel. See `BaseColumnOptions.filterGroup` for
93
93
  * the consumer-facing description. */
94
94
  filterGroup?: string;
95
+ /**
96
+ * Optional thunk returning a list of additional reactive values the
97
+ * cell renderer should depend on. Read synchronously from
98
+ * `cell.getValue` before the (possibly async) `accessorFn` runs, so
99
+ * Svelte's `$derived` tracking captures these row-level reads even
100
+ * when the user's accessor reads them only after an `await` (where
101
+ * tracking is no longer active). Use when a column's displayed
102
+ * value depends on a sibling field that the accessor reads after a
103
+ * `Promise.then` / `await` boundary.
104
+ */
105
+ reactiveDependencies?: (row: T) => ReadonlyArray<unknown>;
95
106
  }
96
107
  /**
97
108
  * `ColumnDef<T, FilterFnType, OptionsType, TValue>` is the abstract base
@@ -137,6 +148,13 @@ export declare abstract class ColumnDef<T extends object, in FilterFnType = neve
137
148
  readonly isValid: ValidationFn<T> | undefined;
138
149
  /** Reads the row value. Required at the `AccessorDef` subclass layer. */
139
150
  readonly accessorFn: AccessorFn<T, TValue> | undefined;
151
+ /**
152
+ * Optional thunk returning a list of additional reactive values the
153
+ * cell should depend on. Read synchronously by `cell.getValue` so
154
+ * Svelte's `$derived` tracking picks them up even when the user's
155
+ * `accessorFn` accesses them only after an `await` boundary.
156
+ */
157
+ readonly reactiveDependencies: ((row: T) => ReadonlyArray<unknown>) | undefined;
140
158
  /** Filter predicate. Each subclass narrows the generic parameter. */
141
159
  abstract filterFn: FilterFn<T, FilterFnType> | undefined;
142
160
  private _minSize;
@@ -63,6 +63,7 @@ export class ColumnDef {
63
63
  // user function's synchronous portion). The per-Def WeakMap was
64
64
  // an extra correctness-eating layer.
65
65
  this.getDisplayValue = $derived(props.getDisplayValue);
66
+ this.reactiveDependencies = $derived(props.reactiveDependencies);
66
67
  this.debug = $derived(props.debug ?? false);
67
68
  this.getSortingFn = $derived(props.getSortingFn);
68
69
  this.getOptions = $derived(props.getOptions);
@@ -90,6 +91,13 @@ export class ColumnDef {
90
91
  isValid;
91
92
  /** Reads the row value. Required at the `AccessorDef` subclass layer. */
92
93
  accessorFn;
94
+ /**
95
+ * Optional thunk returning a list of additional reactive values the
96
+ * cell should depend on. Read synchronously by `cell.getValue` so
97
+ * Svelte's `$derived` tracking picks them up even when the user's
98
+ * `accessorFn` accesses them only after an `await` boundary.
99
+ */
100
+ reactiveDependencies;
93
101
  _minSize;
94
102
  /** Minimum width in pixels. */
95
103
  get minSize() {
@@ -1833,6 +1833,19 @@ export class TableContext {
1833
1833
  id: `${index}_${def.id}`,
1834
1834
  row: row,
1835
1835
  getValue: () => {
1836
+ // Read declared reactive dependencies synchronously so the
1837
+ // caller's `$derived` registers them even when the
1838
+ // (possibly async) accessor reads them only after an
1839
+ // `await` (where Svelte's tracking context is no longer
1840
+ // active). The returned tuple is read for its side-effect
1841
+ // of touching each entry through the row proxy; the
1842
+ // values themselves are discarded.
1843
+ const deps = def.reactiveDependencies?.(item);
1844
+ if (deps) {
1845
+ for (let i = 0; i < deps.length; i++) {
1846
+ void deps[i];
1847
+ }
1848
+ }
1836
1849
  const gotValue = def.accessorFn?.(item, index);
1837
1850
  if (gotValue !== undefined) {
1838
1851
  return gotValue;
@@ -184,6 +184,18 @@ export interface DisplayColumnOptions<TRow extends object, TValue = unknown> ext
184
184
  * types.
185
185
  */
186
186
  getOptions?: () => ReadonlyArray<SelectOption<TValue>> | Promise<ReadonlyArray<SelectOption<TValue>>>;
187
+ /**
188
+ * Optional thunk returning a list of additional reactive values
189
+ * this column's `value` callback depends on. Read synchronously
190
+ * by the cell renderer before invoking `value` so Svelte's
191
+ * `$derived` tracking captures those row-level reads, even when
192
+ * the `value` callback reads them only after an `await` boundary
193
+ * (where reactive tracking is no longer active). Use when the
194
+ * computed value depends on a sibling field that is read inside
195
+ * the async portion of `value` (e.g. `(row) => [row.fkId]` for a
196
+ * display column that fetches a foreign-key entity by id).
197
+ */
198
+ reactiveDependencies?: (row: TRow) => ReadonlyArray<unknown>;
187
199
  }
188
200
  /** Options for `col.bubble({ header, value, colour, ... })`. Renders one or
189
201
  * more coloured pill bubbles per row. */
@@ -255,24 +255,9 @@ function makeOnEditCell(editHandlers, onAnyEdit) {
255
255
  }
256
256
  return async (item, coords, value) => {
257
257
  const handler = editHandlers.get(coords.id);
258
- if (typeof window !== 'undefined') {
259
- // eslint-disable-next-line no-console
260
- console.debug('%c[scoria/makeOnEditCell] dispatching', 'color:#08f', {
261
- coordsId: coords.id,
262
- value,
263
- handlerFound: !!handler,
264
- registeredHandlerKeys: Array.from(editHandlers.keys()),
265
- });
266
- }
267
258
  if (handler) {
268
259
  try {
269
260
  await handler(item, value, coords);
270
- if (typeof window !== 'undefined') {
271
- // eslint-disable-next-line no-console
272
- console.debug('%c[scoria/makeOnEditCell] handler resolved', 'color:#08f', {
273
- coordsId: coords.id,
274
- });
275
- }
276
261
  }
277
262
  catch (e) {
278
263
  devCatch(e);
@@ -90,8 +90,6 @@ export function attachDropdownPortal(options) {
90
90
  /* nothing wired up */
91
91
  };
92
92
  }
93
- let originalParent = null;
94
- let originalNextSibling = null;
95
93
  let isPortalled = false;
96
94
  const reposition = () => {
97
95
  if (!isPortalled) {
@@ -144,32 +142,31 @@ export function attachDropdownPortal(options) {
144
142
  if (isPortalled) {
145
143
  return;
146
144
  }
147
- originalParent = dropdown.parentNode;
148
- originalNextSibling = dropdown.nextSibling;
149
- // Clear any `display: none` left over from a previous portalIn
150
- // (see the defensive hide there) so the dropdown is visible
151
- // once teleported into the new host.
145
+ // PREVIOUSLY: this helper moved the dropdown to `document.body`
146
+ // (or to the open `<dialog>` ancestor) via `appendChild` so it
147
+ // would escape ancestor `overflow: hidden` / `z-index` clipping.
148
+ // That hop broke Svelte 5's per-component event delegation:
149
+ // inline `onmousedown` / `onclick` handlers in Svelecte's
150
+ // template (specifically the `.sv_dropdown` element) are wired
151
+ // through a delegated dispatch rooted at the component's
152
+ // original mount point, and the dispatch's parent-chain walk
153
+ // never reaches the moved element. The visible symptom: mouse
154
+ // clicks on dropdown options did not select (keyboard Enter
155
+ // did, because that path bypasses the dropdown's click
156
+ // handler).
157
+ //
158
+ // We now leave the dropdown in its original DOM location and
159
+ // only flip it to `position: fixed`. `position: fixed` escapes
160
+ // ancestor `overflow` clipping (the original motivation for
161
+ // portaling) without removing the element from its component
162
+ // tree, so Svelte's delegated dispatch continues to find it.
163
+ // The reposition logic below tracks the trigger via
164
+ // `getBoundingClientRect` exactly as before.
152
165
  dropdown.style.display = '';
153
- // Append the dropdown to the nearest open native `<dialog>` ancestor
154
- // when one exists, otherwise to `document.body`. A `<dialog>` opened
155
- // via `showModal()` renders in the browser's top layer, which sits
156
- // above every z-indexed element in the regular stacking context.
157
- // Without this hop the dropdown's `z-index: 10000` was still painted
158
- // behind the dialog backdrop and its content (the symptom that
159
- // surfaced as "select options disappear behind the allocate modal").
160
- const dialogHost = trigger.closest('dialog');
161
- const portalTarget = dialogHost && dialogHost.open ? dialogHost : document.body;
162
- portalTarget.appendChild(dropdown);
163
- // Tag the teleported dropdown so any "click outside the
164
- // panel" / "click outside the trigger" handler elsewhere in
165
- // the codebase can recognise the dropdown as a logical
166
- // extension of its trigger and ignore clicks landing inside
167
- // it. The `TableSidebar` side-panel uses this marker (along
168
- // with `closest('.sv_dropdown')`) so clicking an option in a
169
- // teleported filter-row dropdown does not close the
170
- // side-panel under it. Components that want to ignore
171
- // portal clicks should check
172
- // `el.closest('[data-scoria-portal-dropdown]')`.
166
+ // Marker preserved for any consumer that still uses it (e.g.
167
+ // the TableSidebar "click-outside" handler). The dropdown is
168
+ // no longer literally portaled, but the marker means "treat
169
+ // clicks landing here as part of the trigger".
173
170
  dropdown.setAttribute('data-scoria-portal-dropdown', '');
174
171
  isPortalled = true;
175
172
  // First-frame measurement: lay out, then reposition again on
@@ -208,24 +205,14 @@ export function attachDropdownPortal(options) {
208
205
  // `display: none` (e.g. on certain transition-cancel race paths
209
206
  // the open/close class flip and the inline style update arrive
210
207
  // out of order), the dropdown would now be `position: static;
211
- // display: block` in `document.body` and contribute its option
212
- // list's height to the body scroll size - which is what surfaces
213
- // as a stray page-level vertical scrollbar after toggling a
214
- // select. Force `display: none` here; Svelecte will restore the
215
- // inline style on next open via its own class-driven render.
208
+ // display: block` and contribute its option list's height to
209
+ // the page scroll size, which is what surfaces as a stray
210
+ // page-level vertical scrollbar after toggling a select. Force
211
+ // `display: none` here; Svelecte will restore the inline style
212
+ // on next open via its own class-driven render.
216
213
  if (!dropdown.classList.contains('is-open')) {
217
214
  dropdown.style.display = 'none';
218
215
  }
219
- if (originalParent != null) {
220
- if (originalNextSibling != null && originalNextSibling.parentNode === originalParent) {
221
- originalParent.insertBefore(dropdown, originalNextSibling);
222
- }
223
- else {
224
- originalParent.appendChild(dropdown);
225
- }
226
- }
227
- originalParent = null;
228
- originalNextSibling = null;
229
216
  isPortalled = false;
230
217
  };
231
218
  // Apply / unapply the portal whenever Svelecte toggles the
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.31",
4
+ "version": "0.37.33",
5
5
  "publishConfig": {
6
6
  "access": "restricted"
7
7
  },