@r2digisolutions/components 0.3.4 → 0.3.6

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.
@@ -180,13 +180,11 @@
180
180
 
181
181
  /** Dock sticks to the viewport bottom; portal avoids parent transform/overflow traps. */
182
182
  function portalDock(node: HTMLElement) {
183
- if (!(placement === 'dock' && sticky)) return {};
184
- document.body.appendChild(node);
185
- return {
186
- destroy() {
187
- node.remove();
188
- }
189
- };
183
+ $effect(() => {
184
+ if (!(placement === 'dock' && sticky)) return;
185
+ document.body.appendChild(node);
186
+ return () => node.remove();
187
+ });
190
188
  }
191
189
 
192
190
  const dockFixed = $derived(placement === 'dock' && sticky);
@@ -196,7 +194,7 @@
196
194
 
197
195
  {#if count > 0}
198
196
  <div
199
- use:portalDock
197
+ {@attach portalDock}
200
198
  class={[
201
199
  placement === 'dock' && 'pointer-events-none z-30 flex justify-center',
202
200
  dockFixed && 'fixed inset-x-0 bottom-3 z-50 px-3',
@@ -102,21 +102,24 @@
102
102
  import { MARK_COLORS, markTint, markSwatch } from './marks.js';
103
103
  import { isColumnVisibleAtWidth } from './breakpoints.js';
104
104
  import { filterRows, cellText, uniqueColumnValues } from './filterRows.js';
105
+ import { resolveAccessor, accessorPatchKey, resolveRowKey } from '../../../utils/columnAccessor.js';
106
+ import type { RowKey } from '../../../utils/columnAccessor.js';
105
107
 
108
+ /**
109
+ * Portal via attachment + $effect so the node moves after mount/hydration
110
+ * (same pattern as Checkbox `{@attach setIndeterminate}`).
111
+ */
106
112
  function portalToBody(node: HTMLElement) {
107
- if (typeof document === 'undefined') return {};
108
- document.body.appendChild(node);
109
- return {
110
- destroy() {
111
- node.remove();
112
- }
113
- };
113
+ $effect(() => {
114
+ document.body.appendChild(node);
115
+ return () => node.remove();
116
+ });
114
117
  }
115
118
 
116
119
  interface DataGridProps {
117
120
  columns?: DataGridColumn<T>[];
118
121
  rows?: T[];
119
- rowKey?: keyof T | ((row: T, index: number) => string);
122
+ rowKey?: RowKey<T>;
120
123
  selection?: GridSelection;
121
124
  notes?: CellNote[];
122
125
  marks?: GridMark[];
@@ -257,14 +260,19 @@
257
260
  let contextTarget = $state<CellRef | null>(null);
258
261
  let contextOpen = $state(false);
259
262
  let contextAnchor = $state<ContextMenuAnchor | null>(null);
260
- /** Same on SSR + first client paint to avoid hydration mismatch; real width set in $effect. */
261
- let viewportWidth = $state(1280);
263
+ /**
264
+ * SSR + first client paint: do not apply `hideBelow` yet (show full table).
265
+ * After mount, measure window and refine columns — no hydration mismatch.
266
+ */
267
+ let viewportMeasured = $state(false);
268
+ let viewportWidth = $state(Number.POSITIVE_INFINITY);
262
269
  let filterColumnDraft = $state('');
263
270
  let filterValueDraft = $state('');
264
271
 
265
272
  $effect(() => {
266
273
  const onResize = () => {
267
274
  viewportWidth = window.innerWidth;
275
+ viewportMeasured = true;
268
276
  };
269
277
  window.addEventListener('resize', onResize);
270
278
  onResize();
@@ -272,12 +280,14 @@
272
280
  });
273
281
 
274
282
  const autoHiddenColumns = $derived(
275
- columns.filter(
276
- (c) =>
277
- !c.hidden &&
278
- c.hideBelow &&
279
- !isColumnVisibleAtWidth(c.hideBelow, viewportWidth)
280
- )
283
+ viewportMeasured
284
+ ? columns.filter(
285
+ (c) =>
286
+ !c.hidden &&
287
+ c.hideBelow &&
288
+ !isColumnVisibleAtWidth(c.hideBelow, viewportWidth)
289
+ )
290
+ : []
281
291
  );
282
292
 
283
293
  /** Expand controls only when there is content to reveal. */
@@ -294,7 +304,13 @@
294
304
  const visibleColumns = $derived(
295
305
  columns.filter((c) => {
296
306
  if (c.hidden) return false;
297
- if (c.hideBelow && !isColumnVisibleAtWidth(c.hideBelow, viewportWidth)) return false;
307
+ if (
308
+ viewportMeasured &&
309
+ c.hideBelow &&
310
+ !isColumnVisibleAtWidth(c.hideBelow, viewportWidth)
311
+ ) {
312
+ return false;
313
+ }
298
314
  return true;
299
315
  })
300
316
  );
@@ -389,20 +405,20 @@
389
405
  );
390
406
 
391
407
  function getKey(row: T, index: number): string {
392
- if (typeof rowKey === 'function') return rowKey(row, index);
393
- if (rowKey && row[rowKey] != null) return String(row[rowKey]);
394
- if ('id' in row && row.id != null) return String(row.id);
395
- return String(index);
408
+ try {
409
+ return resolveRowKey(row, rowKey, index);
410
+ } catch {
411
+ // Bad accessor/rowKey must not take down SSR (e.g. null profile).
412
+ return String(index);
413
+ }
396
414
  }
397
415
 
398
416
  function getValue(row: T, column: DataGridColumn<T>): unknown {
399
- const key = (column.accessor ?? column.id) as string;
400
- return key.split('.').reduce<unknown>((acc, part) => {
401
- if (acc && typeof acc === 'object' && part in (acc as object)) {
402
- return (acc as Record<string, unknown>)[part];
403
- }
417
+ try {
418
+ return resolveAccessor(row, column.accessor, column.id);
419
+ } catch {
404
420
  return undefined;
405
- }, row);
421
+ }
406
422
  }
407
423
 
408
424
  function formatCell(value: unknown): string {
@@ -749,7 +765,7 @@
749
765
  if (!editing || !activeEditor) return;
750
766
  const { rowId, columnId } = editing;
751
767
  const col = columns.find((c) => c.id === columnId);
752
- const accessor = (col?.accessor ?? columnId) as string;
768
+ const accessor = accessorPatchKey(col?.accessor, columnId);
753
769
  const draft = activeEditor.type === 'boolean' ? editBool : editValue;
754
770
  const nextValue = coerceEditValue(activeEditor, draft);
755
771
  editing = null;
@@ -2081,7 +2097,7 @@
2081
2097
  {#if noteTargets || markPickerOpen || (showDock && dockCount > 0)}
2082
2098
  <!-- Fixed to the viewport (portaled) so parent relative/overflow don't shift the dock. -->
2083
2099
  <div
2084
- use:portalToBody
2100
+ {@attach portalToBody}
2085
2101
  class="pointer-events-none fixed inset-x-0 bottom-3 z-[60] flex justify-center px-3"
2086
2102
  >
2087
2103
  <div class="relative w-full max-w-2xl">
@@ -2218,7 +2234,7 @@
2218
2234
  {@const ay = contextAnchor.y}
2219
2235
  <!-- svelte-ignore a11y_no_static_element_interactions -->
2220
2236
  <div
2221
- use:portalToBody
2237
+ {@attach portalToBody}
2222
2238
  role="menu"
2223
2239
  tabindex={-1}
2224
2240
  aria-label="Context menu"
@@ -8,11 +8,12 @@ export { filterRows, cellText, uniqueColumnValues } from './filterRows.js';
8
8
  import type { Snippet } from 'svelte';
9
9
  import type { BulkAction } from '../../molecules/BulkActionBar/BulkActionBar.svelte';
10
10
  import { type DataGridColumn, type DataGridViewMode, type DataGridFilter, type GridSelection, type CellNote, type GridMark, type ConditionalFormatRule, type SortDir } from './types.js';
11
+ import type { RowKey } from '../../../utils/columnAccessor.js';
11
12
  declare function $$render<T extends Record<string, unknown> = Record<string, unknown>>(): {
12
13
  props: {
13
14
  columns?: DataGridColumn<T>[];
14
15
  rows?: T[];
15
- rowKey?: keyof T | ((row: T, index: number) => string);
16
+ rowKey?: RowKey<T>;
16
17
  selection?: GridSelection;
17
18
  notes?: CellNote[];
18
19
  marks?: GridMark[];
@@ -1,19 +1,16 @@
1
- function getByPath(row, path) {
2
- return path.split('.').reduce((acc, part) => {
3
- if (acc && typeof acc === 'object' && part in acc) {
4
- return acc[part];
5
- }
6
- return undefined;
7
- }, row);
8
- }
1
+ import { resolveAccessor } from '../../../utils/columnAccessor.js';
9
2
  export function cellText(row, column) {
10
- const key = String(column.accessor ?? column.id);
11
- const value = getByPath(row, key);
12
- if (value == null)
3
+ try {
4
+ const value = resolveAccessor(row, column.accessor, column.id);
5
+ if (value == null)
6
+ return '';
7
+ if (typeof value === 'boolean')
8
+ return value ? 'Yes' : 'No';
9
+ return String(value);
10
+ }
11
+ catch {
13
12
  return '';
14
- if (typeof value === 'boolean')
15
- return value ? 'Yes' : 'No';
16
- return String(value);
13
+ }
17
14
  }
18
15
  /** Client-side search + equality/contains filters. */
19
16
  export function filterRows(rows, columns, query, filters) {
@@ -1,8 +1,12 @@
1
1
  export interface DataGridColumn<Row = Record<string, unknown>> {
2
2
  id: string;
3
3
  header: string;
4
- /** Dot-path key on the row, or custom render via `cell` snippet. */
5
- accessor?: keyof Row | string;
4
+ /**
5
+ * How to read the cell value:
6
+ * - `keyof Row` / string (supports dot-path: `'profile.displayName'`)
7
+ * - function: `(row) => row.profile.displayName` (preferred for nested/typed access)
8
+ */
9
+ accessor?: keyof Row | string | ((row: Row) => unknown);
6
10
  align?: 'left' | 'center' | 'right';
7
11
  sortable?: boolean;
8
12
  width?: string;
@@ -2,8 +2,13 @@
2
2
  export interface DataTableColumn<Row = Record<string, unknown>> {
3
3
  id: string;
4
4
  header: string;
5
- /** Dot-path key on the row, or custom render via `cell` snippet map in parent. */
6
- accessor?: keyof Row | string;
5
+ /**
6
+ * How to read the cell value:
7
+ * - `keyof Row` / string (supports dot-path: `'profile.displayName'`)
8
+ * - function: `(row) => row.profile.displayName` (preferred for nested/typed access)
9
+ * Custom render via `cell` snippet still overrides display.
10
+ */
11
+ accessor?: keyof Row | string | ((row: Row) => unknown);
7
12
  align?: 'left' | 'center' | 'right';
8
13
  sortable?: boolean;
9
14
  width?: string;
@@ -13,13 +18,15 @@
13
18
 
14
19
  <script lang="ts" generics="T extends Record<string, unknown> = Record<string, unknown>">
15
20
  import type { Snippet } from 'svelte';
21
+ import { resolveAccessor, resolveRowKey } from '../../../utils/columnAccessor.js';
22
+ import type { RowKey } from '../../../utils/columnAccessor.js';
16
23
 
17
24
  type SortDir = 'asc' | 'desc' | null;
18
25
 
19
26
  interface DataTableProps {
20
27
  columns?: DataTableColumn<T>[];
21
28
  rows?: T[];
22
- rowKey?: keyof T | ((row: T, index: number) => string);
29
+ rowKey?: RowKey<T>;
23
30
  sortable?: boolean;
24
31
  striped?: boolean;
25
32
  hoverable?: boolean;
@@ -53,20 +60,19 @@
53
60
  let sortDir = $state<SortDir>(null);
54
61
 
55
62
  function getKey(row: T, index: number): string {
56
- if (typeof rowKey === 'function') return rowKey(row, index);
57
- if (rowKey && row[rowKey] != null) return String(row[rowKey]);
58
- if ('id' in row && row.id != null) return String(row.id);
59
- return String(index);
63
+ try {
64
+ return resolveRowKey(row, rowKey, index);
65
+ } catch {
66
+ return String(index);
67
+ }
60
68
  }
61
69
 
62
70
  function getValue(row: T, column: DataTableColumn<T>): unknown {
63
- const key = (column.accessor ?? column.id) as string;
64
- return key.split('.').reduce<unknown>((acc, part) => {
65
- if (acc && typeof acc === 'object' && part in (acc as object)) {
66
- return (acc as Record<string, unknown>)[part];
67
- }
71
+ try {
72
+ return resolveAccessor(row, column.accessor, column.id);
73
+ } catch {
68
74
  return undefined;
69
- }, row);
75
+ }
70
76
  }
71
77
 
72
78
  function formatCell(value: unknown): string {
@@ -1,19 +1,25 @@
1
1
  export interface DataTableColumn<Row = Record<string, unknown>> {
2
2
  id: string;
3
3
  header: string;
4
- /** Dot-path key on the row, or custom render via `cell` snippet map in parent. */
5
- accessor?: keyof Row | string;
4
+ /**
5
+ * How to read the cell value:
6
+ * - `keyof Row` / string (supports dot-path: `'profile.displayName'`)
7
+ * - function: `(row) => row.profile.displayName` (preferred for nested/typed access)
8
+ * Custom render via `cell` snippet still overrides display.
9
+ */
10
+ accessor?: keyof Row | string | ((row: Row) => unknown);
6
11
  align?: 'left' | 'center' | 'right';
7
12
  sortable?: boolean;
8
13
  width?: string;
9
14
  class?: string;
10
15
  }
11
16
  import type { Snippet } from 'svelte';
17
+ import type { RowKey } from '../../../utils/columnAccessor.js';
12
18
  declare function $$render<T extends Record<string, unknown> = Record<string, unknown>>(): {
13
19
  props: {
14
20
  columns?: DataTableColumn<T>[];
15
21
  rows?: T[];
16
- rowKey?: keyof T | ((row: T, index: number) => string);
22
+ rowKey?: RowKey<T>;
17
23
  sortable?: boolean;
18
24
  striped?: boolean;
19
25
  hoverable?: boolean;
package/dist/index.d.ts CHANGED
@@ -813,6 +813,8 @@ export { default as AudioEditorTemplate } from './components/templates/AudioEdit
813
813
  export { themeStore } from './utils/theme.svelte.js';
814
814
  export type { Theme } from './utils/theme.svelte.js';
815
815
  export { createId } from './utils/id.js';
816
+ export { resolveAccessor, accessorPatchKey, resolveRowKey } from './utils/columnAccessor.js';
817
+ export type { ColumnAccessor, RowKey } from './utils/columnAccessor.js';
816
818
  export { pageVisibility } from './utils/pageVisibility.svelte.js';
817
819
  export type { VisibilityState } from './utils/pageVisibility.svelte.js';
818
820
  export { network } from './utils/network.svelte.js';
package/dist/index.js CHANGED
@@ -554,6 +554,7 @@ export { default as AudioEditorTemplate } from './components/templates/AudioEdit
554
554
  // ── Utils ────────────────────────────────────────────────────────────────────
555
555
  export { themeStore } from './utils/theme.svelte.js';
556
556
  export { createId } from './utils/id.js';
557
+ export { resolveAccessor, accessorPatchKey, resolveRowKey } from './utils/columnAccessor.js';
557
558
  export { pageVisibility } from './utils/pageVisibility.svelte.js';
558
559
  export { network } from './utils/network.svelte.js';
559
560
  export { DEFAULT_COLS, DEFAULT_ROW_HEIGHT, DEFAULT_GAP, GRID_DENSITY, clampItem, rectsOverlap, findCollisions, resolveCollisions, compactLayout, updateItem, addItem, removeItem, serializeLayout, parseLayout, layoutBounds, rescaleLayout, resizeItemByEdge } from './utils/layoutGrid.js';
@@ -0,0 +1,8 @@
1
+ export type ColumnAccessor<Row> = keyof Row | string | ((row: Row) => unknown);
2
+ export type RowKey<Row> = keyof Row | string | ((row: Row, index: number) => string);
3
+ /** Resolve a column value from a function, keyof, or dot-path accessor. */
4
+ export declare function resolveAccessor<Row>(row: Row, accessor: ColumnAccessor<Row> | undefined, fallbackId: string): unknown;
5
+ /** Resolve a stable row id (supports function, keyof, and dotted paths like `profile.id`). */
6
+ export declare function resolveRowKey<Row>(row: Row, rowKey: RowKey<Row> | undefined, index: number): string;
7
+ /** Field key for patches — functions can't be keys, so fall back to column id. */
8
+ export declare function accessorPatchKey(accessor: unknown, fallbackId: string): string;
@@ -0,0 +1,35 @@
1
+ function getByPath(row, path) {
2
+ return path.split('.').reduce((acc, part) => {
3
+ if (acc && typeof acc === 'object' && part in acc) {
4
+ return acc[part];
5
+ }
6
+ return undefined;
7
+ }, row);
8
+ }
9
+ /** Resolve a column value from a function, keyof, or dot-path accessor. */
10
+ export function resolveAccessor(row, accessor, fallbackId) {
11
+ if (typeof accessor === 'function')
12
+ return accessor(row);
13
+ const key = String(accessor ?? fallbackId);
14
+ return getByPath(row, key);
15
+ }
16
+ /** Resolve a stable row id (supports function, keyof, and dotted paths like `profile.id`). */
17
+ export function resolveRowKey(row, rowKey, index) {
18
+ if (typeof rowKey === 'function')
19
+ return rowKey(row, index);
20
+ if (rowKey != null && rowKey !== '') {
21
+ const val = getByPath(row, String(rowKey));
22
+ if (val != null)
23
+ return String(val);
24
+ }
25
+ if (row && typeof row === 'object' && 'id' in row && row.id != null) {
26
+ return String(row.id);
27
+ }
28
+ return String(index);
29
+ }
30
+ /** Field key for patches — functions can't be keys, so fall back to column id. */
31
+ export function accessorPatchKey(accessor, fallbackId) {
32
+ if (typeof accessor === 'function' || accessor == null)
33
+ return fallbackId;
34
+ return String(accessor);
35
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r2digisolutions/components",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "private": false,
5
5
  "description": "R2DigiSolutions Svelte 5 component library — Atomic Design, Tailwind 4, Light/Dark mode",
6
6
  "license": "MIT",