@warkypublic/svelix 0.1.41 → 0.1.43

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.
@@ -147,33 +147,18 @@
147
147
 
148
148
  const contentPaddingClass = $derived(hideHeader ? 'p-0' : 'p-4');
149
149
 
150
- type TipexControlEditor = {
151
- isActive: (name: string, attrs?: Record<string, unknown>) => boolean;
152
- chain: () => {
153
- focus: () => {
154
- toggleHeading: (opts: { level: number }) => { run: () => void };
155
- setParagraph: () => { run: () => void };
156
- toggleBold: () => { run: () => void };
157
- toggleItalic: () => { run: () => void };
158
- toggleUnderline: () => { run: () => void };
159
- toggleBulletList: () => { run: () => void };
160
- toggleOrderedList: () => { run: () => void };
161
- toggleTaskList: () => { run: () => void };
162
- toggleBlockquote: () => { run: () => void };
163
- toggleCodeBlock: () => { run: () => void };
164
- };
165
- };
166
- };
150
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
151
+ function echain(editor: TipexEditor): any { return echain(editor) ?? {}; }
167
152
  </script>
168
153
 
169
- {#snippet controlComponent(editor: TipexControlEditor)}
154
+ {#snippet controlComponent(editor: TipexEditor)}
170
155
  <div class="tipex-controller">
171
156
  <div class="tipex-basic-controller-wrapper">
172
157
  <button
173
158
  type="button"
174
159
  class="tipex-edit-button tipex-button-extra tipex-button-rigid"
175
160
  class:active={editor?.isActive('heading', { level: 1 })}
176
- onclick={() => editor?.chain().focus().toggleHeading({ level: 1 }).run()}
161
+ onclick={() => echain(editor).toggleHeading({ level: 1 }).run()}
177
162
  aria-label="Heading 1"
178
163
  title="Heading 1"
179
164
  >
@@ -183,7 +168,7 @@ type TipexControlEditor = {
183
168
  type="button"
184
169
  class="tipex-edit-button tipex-button-extra tipex-button-rigid"
185
170
  class:active={editor?.isActive('heading', { level: 2 })}
186
- onclick={() => editor?.chain().focus().toggleHeading({ level: 2 }).run()}
171
+ onclick={() => echain(editor).toggleHeading({ level: 2 }).run()}
187
172
  aria-label="Heading 2"
188
173
  title="Heading 2"
189
174
  >
@@ -193,7 +178,7 @@ type TipexControlEditor = {
193
178
  type="button"
194
179
  class="tipex-edit-button tipex-button-extra tipex-button-rigid"
195
180
  class:active={editor?.isActive('paragraph')}
196
- onclick={() => editor?.chain().focus().setParagraph().run()}
181
+ onclick={() => echain(editor).setParagraph().run()}
197
182
  aria-label="Paragraph"
198
183
  title="Paragraph"
199
184
  >
@@ -206,7 +191,7 @@ type TipexControlEditor = {
206
191
  type="button"
207
192
  class="tipex-edit-button tipex-button-extra tipex-button-rigid"
208
193
  class:active={editor?.isActive('bold')}
209
- onclick={() => editor?.chain().focus().toggleBold().run()}
194
+ onclick={() => echain(editor).toggleBold().run()}
210
195
  aria-label="Bold"
211
196
  title="Bold"
212
197
  >
@@ -216,7 +201,7 @@ type TipexControlEditor = {
216
201
  type="button"
217
202
  class="tipex-edit-button tipex-button-extra tipex-button-rigid"
218
203
  class:active={editor?.isActive('italic')}
219
- onclick={() => editor?.chain().focus().toggleItalic().run()}
204
+ onclick={() => echain(editor).toggleItalic().run()}
220
205
  aria-label="Italic"
221
206
  title="Italic"
222
207
  >
@@ -226,7 +211,7 @@ type TipexControlEditor = {
226
211
  type="button"
227
212
  class="tipex-edit-button tipex-button-extra tipex-button-rigid"
228
213
  class:active={editor?.isActive('underline')}
229
- onclick={() => editor?.chain().focus().toggleUnderline().run()}
214
+ onclick={() => echain(editor).toggleUnderline().run()}
230
215
  aria-label="Underline"
231
216
  title="Underline"
232
217
  >
@@ -239,7 +224,7 @@ type TipexControlEditor = {
239
224
  type="button"
240
225
  class="tipex-edit-button tipex-button-extra tipex-button-rigid"
241
226
  class:active={editor?.isActive('bulletList')}
242
- onclick={() => editor?.chain().focus().toggleBulletList().run()}
227
+ onclick={() => echain(editor).toggleBulletList().run()}
243
228
  aria-label="Bullet list"
244
229
  title="Bullet list"
245
230
  >
@@ -251,7 +236,7 @@ type TipexControlEditor = {
251
236
  type="button"
252
237
  class="tipex-edit-button tipex-button-extra tipex-button-rigid"
253
238
  class:active={editor?.isActive('orderedList')}
254
- onclick={() => editor?.chain().focus().toggleOrderedList().run()}
239
+ onclick={() => echain(editor).toggleOrderedList().run()}
255
240
  aria-label="Ordered list"
256
241
  title="Ordered list"
257
242
  >
@@ -263,7 +248,7 @@ type TipexControlEditor = {
263
248
  type="button"
264
249
  class="tipex-edit-button tipex-button-extra tipex-button-rigid"
265
250
  class:active={editor?.isActive('taskList')}
266
- onclick={() => editor?.chain().focus().toggleTaskList().run()}
251
+ onclick={() => echain(editor).toggleTaskList().run()}
267
252
  aria-label="Task list"
268
253
  title="Task list"
269
254
  >
@@ -279,7 +264,7 @@ type TipexControlEditor = {
279
264
  type="button"
280
265
  class="tipex-edit-button tipex-button-extra tipex-button-rigid"
281
266
  class:active={editor?.isActive('blockquote')}
282
- onclick={() => editor?.chain().focus().toggleBlockquote().run()}
267
+ onclick={() => echain(editor).toggleBlockquote().run()}
283
268
  aria-label="Blockquote"
284
269
  title="Blockquote"
285
270
  >
@@ -291,7 +276,7 @@ type TipexControlEditor = {
291
276
  type="button"
292
277
  class="tipex-edit-button tipex-button-extra tipex-button-rigid"
293
278
  class:active={editor?.isActive('codeBlock')}
294
- onclick={() => editor?.chain().focus().toggleCodeBlock().run()}
279
+ onclick={() => echain(editor).toggleCodeBlock().run()}
295
280
  aria-label="Code block"
296
281
  title="Code block"
297
282
  >
@@ -28,7 +28,7 @@
28
28
  column?: GridlerColumn;
29
29
  }) => void;
30
30
  onRowClick?: (row: number) => void;
31
- onRowContextMenu?: (row: number) => void;
31
+ onRowContextMenu?: (row: number, x: number, y: number) => void;
32
32
  /** Resolve raw row data by row index for populating onCellEvent. */
33
33
  getRowData?: (row: number) => Record<string, unknown> | undefined;
34
34
  children?: Snippet;
@@ -157,6 +157,29 @@
157
157
  cancelEdit();
158
158
  }
159
159
 
160
+ function getSelectionRowData(sel: Selection): Record<string, unknown>[] | undefined {
161
+ if (!getRowData) return undefined;
162
+ if (sel.type === "cell") {
163
+ const d = getRowData(sel.item[1]);
164
+ return d ? [d] : undefined;
165
+ }
166
+ if (sel.type === "row") {
167
+ const items = sel.rows.map(getRowData).filter((r): r is Record<string, unknown> => r !== undefined);
168
+ return items.length > 0 ? items : undefined;
169
+ }
170
+ if (sel.type === "range") {
171
+ const minRow = Math.min(sel.start[1], sel.end[1]);
172
+ const maxRow = Math.max(sel.start[1], sel.end[1]);
173
+ const items: Record<string, unknown>[] = [];
174
+ for (let i = minRow; i <= maxRow; i++) {
175
+ const d = getRowData(i);
176
+ if (d) items.push(d);
177
+ }
178
+ return items.length > 0 ? items : undefined;
179
+ }
180
+ return undefined;
181
+ }
182
+
160
183
  // ── Row toggle (checkbox / number markers) ────────────────────────────────────
161
184
 
162
185
  function handleRowToggle(row: number) {
@@ -173,7 +196,7 @@
173
196
  }
174
197
  currentSelection = newSel;
175
198
  canvasComponent?.setCurrentSelection(newSel);
176
- onSelectionChange?.(newSel);
199
+ onSelectionChange?.(newSel, getSelectionRowData(newSel));
177
200
  }
178
201
 
179
202
  // ── Clipboard ────────────────────────────────────────────────────────────────
@@ -295,7 +318,7 @@
295
318
  };
296
319
  currentSelection = newSel;
297
320
  canvasComponent?.setCurrentSelection(newSel);
298
- onSelectionChange?.(newSel);
321
+ onSelectionChange?.(newSel, getSelectionRowData(newSel));
299
322
  canvasComponent?.setAnnouncement("All cells selected");
300
323
  }
301
324
  e.preventDefault();
@@ -386,7 +409,7 @@
386
409
  const newSel: Selection = { type: "cell", item: newFocusedCell };
387
410
  currentSelection = newSel;
388
411
  canvasComponent?.setCurrentSelection(newSel);
389
- onSelectionChange?.(newSel);
412
+ onSelectionChange?.(newSel, getSelectionRowData(newSel));
390
413
  canvasComponent?.setAnnouncement(
391
414
  `Row ${newRow + 1}, column ${columns[newCol]?.title ?? newCol + 1}`,
392
415
  );
@@ -445,7 +468,7 @@
445
468
  onVisibleRangeChange={handleVisibleRangeChange}
446
469
  onSelectionChange={(sel) => {
447
470
  currentSelection = sel;
448
- onSelectionChange?.(sel);
471
+ onSelectionChange?.(sel, getSelectionRowData(sel));
449
472
  onGridEvent?.("selection_changed");
450
473
  }}
451
474
  onCellDblClick={(item, cell) => {
@@ -472,8 +495,8 @@
472
495
  onCellEvent?.("contextmenu", getRowData?.(item[1]) ?? {}, columns[item[0]] ?? { id: "", title: "" }, { x, y });
473
496
  onGridEvent?.("contextmenu", getRowData?.(item[1]) ?? {}, columns[item[0]] ?? { id: "", title: "" }, { x, y });
474
497
  }}
475
- onRowContextMenu={(row) => {
476
- onRowContextMenu?.(row);
498
+ onRowContextMenu={(row, x, y) => {
499
+ onRowContextMenu?.(row, x, y);
477
500
  }}
478
501
  onColumnResized={(col, newWidth) => {
479
502
  columns[col].width = newWidth;
@@ -14,7 +14,7 @@ export interface Props extends Partial<GridlerProps> {
14
14
  column?: GridlerColumn;
15
15
  }) => void;
16
16
  onRowClick?: (row: number) => void;
17
- onRowContextMenu?: (row: number) => void;
17
+ onRowContextMenu?: (row: number, x: number, y: number) => void;
18
18
  /** Resolve raw row data by row index for populating onCellEvent. */
19
19
  getRowData?: (row: number) => Record<string, unknown> | undefined;
20
20
  children?: Snippet;
@@ -58,7 +58,7 @@
58
58
  onSelectionChange?: (sel: Selection) => void;
59
59
  onCellDblClick?: (item: Item, cell: GridlerCell) => void;
60
60
  onRowClick?: (row: number) => void;
61
- onRowContextMenu?: (row: number) => void;
61
+ onRowContextMenu?: (row: number, x: number, y: number) => void;
62
62
  onColumnMoved?: (from: number, to: number) => void;
63
63
  onCellEdited?: (item: Item, cell: GridlerCell) => void;
64
64
  onDelete?: (sel: Selection) => void;
@@ -480,7 +480,7 @@
480
480
  const row = getRowFromOffsetY(e.offsetY);
481
481
  if (row !== null) {
482
482
  e.preventDefault();
483
- onRowContextMenu?.(row);
483
+ onRowContextMenu?.(row, e.clientX, e.clientY);
484
484
  }
485
485
 
486
486
  if (e.offsetX < rowMarkerWidth) return;
@@ -27,7 +27,7 @@ interface Props {
27
27
  onSelectionChange?: (sel: Selection) => void;
28
28
  onCellDblClick?: (item: Item, cell: GridlerCell) => void;
29
29
  onRowClick?: (row: number) => void;
30
- onRowContextMenu?: (row: number) => void;
30
+ onRowContextMenu?: (row: number, x: number, y: number) => void;
31
31
  onColumnMoved?: (from: number, to: number) => void;
32
32
  onCellEdited?: (item: Item, cell: GridlerCell) => void;
33
33
  onDelete?: (sel: Selection) => void;
@@ -88,10 +88,12 @@
88
88
  row: number,
89
89
  rowData: Record<string, unknown> | undefined,
90
90
  ) => void;
91
- /** Called on row context-menu with the row index and resolved row data. */
91
+ /** Called on row context-menu with the row index, resolved row data, and click coordinates. */
92
92
  onRowContextMenu?: (
93
93
  row: number,
94
94
  rowData: Record<string, unknown> | undefined,
95
+ x: number,
96
+ y: number,
95
97
  ) => void;
96
98
 
97
99
  // ── Server-side fetching ──────────────────────────────────────────────────
@@ -120,13 +122,25 @@
120
122
  /** Called when a server fetch fails. */
121
123
  onLoadError?: (error: string) => void;
122
124
 
125
+ /**
126
+ * Bindable. Reflects the server-reported total row count.
127
+ * Updated after every successful page fetch; 0 until the first response.
128
+ * Read-only from the parent — do not write this value.
129
+ */
130
+ total?: number;
131
+ /**
132
+ * Bindable. `true` while any server fetch is in flight (initial load or next-page).
133
+ * Read-only from the parent — do not write this value.
134
+ */
135
+ loading?: boolean;
136
+
123
137
  /**
124
138
  * Extra items appended to the built-in context menu (Refresh, Clear Search, Clear Filters, Clear Sort).
125
139
  * Pass a separator item `{ id: 'sep', kind: 'separator', label: '' }` to group your items.
126
140
  */
127
141
  menuItems?: GridlerContextMenuItem[];
128
142
  /** Called when any context menu item is selected (built-in or custom). */
129
- onMenuItemSelect?: (item: GridlerContextMenuItem) => void;
143
+ onMenuItemSelect?: (item: GridlerContextMenuItem, rowData?: Record<string, unknown>) => void;
130
144
 
131
145
  children?: Snippet;
132
146
  }
@@ -150,6 +164,8 @@
150
164
  serverSideSearch = false,
151
165
  searchColumns,
152
166
  onLoadError,
167
+ total = $bindable(0),
168
+ loading = $bindable(false),
153
169
  // GridCommonProps filters/sort (generic types)
154
170
  filters,
155
171
  onFilterChange,
@@ -285,6 +301,7 @@
285
301
  // ── Context menu ───────────────────────────────────────────────────────────
286
302
 
287
303
  let refreshTrigger = $state(0);
304
+ let contextMenuRowData = $state<Record<string, unknown> | undefined>(undefined);
288
305
  let menu = $state<{
289
306
  show: (
290
307
  id: string,
@@ -347,8 +364,8 @@
347
364
  label: item.label,
348
365
  icon: item.icon,
349
366
  onClick: () => {
350
- item.onselect?.();
351
- onMenuItemSelect?.(item);
367
+ item.onselect?.(contextMenuRowData);
368
+ onMenuItemSelect?.(item, contextMenuRowData);
352
369
  },
353
370
  });
354
371
  }
@@ -523,6 +540,9 @@
523
540
  let serverFetchVersion = 0;
524
541
  let serverLoading = $state(false);
525
542
  let serverAllLoaded = $state(false);
543
+
544
+ $effect(() => { total = serverTotal; });
545
+ $effect(() => { loading = serverLoading; });
526
546
  let serverError = $state<string | null>(null);
527
547
 
528
548
  type Adapter = GridlerResolveSpecAdapter | GridlerRestHeaderSpecAdapter;
@@ -704,8 +724,10 @@
704
724
  onRowClick?.(row, resolveRowData(row));
705
725
  }
706
726
 
707
- function handleRowContextMenu(row: number) {
708
- onRowContextMenu?.(row, resolveRowData(row));
727
+ function handleRowContextMenu(row: number, x: number, y: number) {
728
+ contextMenuRowData = resolveRowData(row);
729
+ menu?.show("gridler-menu", { x, y, items: buildMenuItems() });
730
+ onRowContextMenu?.(row, contextMenuRowData, x, y);
709
731
  }
710
732
 
711
733
  // ── Selection → selectedItems bridge ──────────────────────────────────────
@@ -736,7 +758,7 @@
736
758
  }
737
759
  internalSelectedItems = items;
738
760
  if (items.length > 0) onSelectedItemsChange?.(items);
739
- onSelectionChange?.(sel);
761
+ onSelectionChange?.(sel, items.length > 0 ? items : undefined);
740
762
  }
741
763
 
742
764
  // ── Settings change event ─────────────────────────────────────────────────
@@ -795,6 +817,7 @@
795
817
  item?: Record<string, unknown>;
796
818
  column?: GridlerColumn;
797
819
  }) {
820
+ contextMenuRowData = undefined;
798
821
  if (onMenuClick) {
799
822
  await onMenuClick(
800
823
  d?.item,
@@ -806,6 +829,16 @@
806
829
  if (!d) return;
807
830
  menu?.show("gridler-menu", { x: d.x, y: d.y, items: buildMenuItems() });
808
831
  }
832
+
833
+ // ── Public API ────────────────────────────────────────────────────────────
834
+
835
+ export function getSelectedRows(): Record<string, unknown>[] {
836
+ return internalSelectedItems;
837
+ }
838
+
839
+ export function getLoadedData(): Record<string, unknown>[] {
840
+ return isServerMode ? serverData : filteredDataState;
841
+ }
809
842
  </script>
810
843
 
811
844
  <BetterMenu bind:this={menu}>
@@ -30,8 +30,8 @@ export interface Props extends Omit<Partial<GridlerProps>, "columns" | "rows" |
30
30
  onRowDblClick?: (row: number, rowData: Record<string, unknown> | undefined) => void;
31
31
  /** Called on row click with the row index and resolved row data. */
32
32
  onRowClick?: (row: number, rowData: Record<string, unknown> | undefined) => void;
33
- /** Called on row context-menu with the row index and resolved row data. */
34
- onRowContextMenu?: (row: number, rowData: Record<string, unknown> | undefined) => void;
33
+ /** Called on row context-menu with the row index, resolved row data, and click coordinates. */
34
+ onRowContextMenu?: (row: number, rowData: Record<string, unknown> | undefined, x: number, y: number) => void;
35
35
  /** Rows per cursor-forward page. Defaults to 200. */
36
36
  pageSize?: number;
37
37
  /**
@@ -52,15 +52,29 @@ export interface Props extends Omit<Partial<GridlerProps>, "columns" | "rows" |
52
52
  searchColumns?: string[];
53
53
  /** Called when a server fetch fails. */
54
54
  onLoadError?: (error: string) => void;
55
+ /**
56
+ * Bindable. Reflects the server-reported total row count.
57
+ * Updated after every successful page fetch; 0 until the first response.
58
+ * Read-only from the parent — do not write this value.
59
+ */
60
+ total?: number;
61
+ /**
62
+ * Bindable. `true` while any server fetch is in flight (initial load or next-page).
63
+ * Read-only from the parent — do not write this value.
64
+ */
65
+ loading?: boolean;
55
66
  /**
56
67
  * Extra items appended to the built-in context menu (Refresh, Clear Search, Clear Filters, Clear Sort).
57
68
  * Pass a separator item `{ id: 'sep', kind: 'separator', label: '' }` to group your items.
58
69
  */
59
70
  menuItems?: GridlerContextMenuItem[];
60
71
  /** Called when any context menu item is selected (built-in or custom). */
61
- onMenuItemSelect?: (item: GridlerContextMenuItem) => void;
72
+ onMenuItemSelect?: (item: GridlerContextMenuItem, rowData?: Record<string, unknown>) => void;
62
73
  children?: Snippet;
63
74
  }
64
- declare const GridlerFull: import("svelte").Component<Props, {}, "">;
75
+ declare const GridlerFull: import("svelte").Component<Props, {
76
+ getSelectedRows: () => Record<string, unknown>[];
77
+ getLoadedData: () => Record<string, unknown>[];
78
+ }, "loading" | "total">;
65
79
  type GridlerFull = ReturnType<typeof GridlerFull>;
66
80
  export default GridlerFull;
@@ -155,8 +155,8 @@ export interface GridlerProps extends GridCommonProps<Record<string, unknown>> {
155
155
  theme?: Partial<GridlerTheme>;
156
156
  /** Controlled selection state. */
157
157
  selection?: Selection;
158
- /** Called when the internal selection changes. */
159
- onSelectionChange?: (selection: Selection) => void;
158
+ /** Called when the internal selection changes. Row data for all selected rows is provided where available. */
159
+ onSelectionChange?: (selection: Selection, rowData?: Record<string, unknown>[]) => void;
160
160
  /** Called when the visible row/column range changes (e.g. on scroll). */
161
161
  onVisibleRangeChange?: (range: VisibleRange) => void;
162
162
  /** When true, all cells are read-only. Also settable via settings.readonly. */
@@ -173,7 +173,7 @@ export interface GridlerContextMenuItem {
173
173
  icon?: string;
174
174
  /** Use 'separator' to render a divider line. */
175
175
  kind?: 'item' | 'separator';
176
- onselect?: () => void;
176
+ onselect?: (rowData?: Record<string, unknown>) => void;
177
177
  }
178
178
  export declare const DEFAULT_THEME: GridlerTheme;
179
179
  export declare const DEFAULT_THEME_DARK: GridlerTheme;
@@ -376,12 +376,10 @@
376
376
  contextMenuY = ev.clientY;
377
377
 
378
378
  const ctxRowId = (resolved?.id ?? selectedIds[0] ?? null) as string | number | null;
379
- const ctxRow =
380
- resolved?.row ??
381
- (ctxRowId != null
379
+ const ctxRow: Record<string, unknown> | null =
380
+ ctxRowId != null
382
381
  ? data.find((item) => String(item?.[uniqueID]) === String(ctxRowId)) ?? null
383
- : null) ??
384
- null;
382
+ : null;
385
383
 
386
384
  contextMenuRowId = ctxRowId;
387
385
  contextMenuRow = ctxRow;
@@ -570,6 +570,78 @@ These are `Partial<Options>` from `@warkypublic/resolvespec-js`. Grid-controlled
570
570
  | `where` | Raw SQL WHERE fragment for the relation. |
571
571
  | `updatable` | Marks the preloaded relation as updatable. |
572
572
 
573
+ #### Bindable state props
574
+
575
+ | Prop | Type | Description |
576
+ |-----------|-----------|-----------------------------------------------------------------------------|
577
+ | `total` | `number` | Server-reported total row count. Updated after every successful page fetch. |
578
+ | `loading` | `boolean` | `true` while any fetch is in flight (initial load or next-page append). |
579
+
580
+ Both are read-only from the parent's perspective — bind them to a variable, do not pass a value in:
581
+
582
+ ```svelte
583
+ <script lang="ts">
584
+ let total = $state(0);
585
+ let loading = $state(false);
586
+ </script>
587
+
588
+ <GridlerFull bind:total bind:loading {columns} dataSource="resolvespec" {dataSourceOptions} />
589
+
590
+ {#if loading}<span>Loading…</span>{/if}
591
+ <span>{total} rows</span>
592
+ ```
593
+
594
+ #### Row/cell callbacks (GridlerFull enriches with row data)
595
+
596
+ | Callback | Signature | Notes |
597
+ |---|---|---|
598
+ | `onCellDblClick` | `(item: Item, cell: GridlerCell, rowData?)` | Double-click on a cell. |
599
+ | `onRowDblClick` | `(row: number, rowData?)` | Double-click on a row (fires alongside `onCellDblClick`). |
600
+ | `onRowClick` | `(row: number, rowData?)` | Single click on a body row. |
601
+ | `onRowContextMenu` | `(row: number, rowData?, x: number, y: number)` | Right-click on a body row. `x`/`y` are viewport pixel coordinates. |
602
+ | `onDataChange` | `(data: Record<string, unknown>[]) => Promise<void>` | Fires after an inline cell edit mutates local `dataState`. Server mode: not called. |
603
+ | `onColumnsChange` | `(columns: GridlerColumn[]) => void` | Fires when `manageColumns` reorders columns. |
604
+ | `onSearchValueChange` | `(value: string) => void` | Fires on every search input change. |
605
+ | `onLoadError` | `(error: string) => void` | Fires on any adapter fetch failure. |
606
+ | `onMenuItemSelect` | `(item: GridlerContextMenuItem, rowData?) => void` | Fires for both built-in and custom context menu items. |
607
+
608
+ `rowData` is `Record<string, unknown> | undefined` — it maps to the loaded row object for the clicked row index, or `undefined` if the row has not been loaded yet.
609
+
610
+ #### Context menu customisation
611
+
612
+ ```svelte
613
+ <GridlerFull
614
+ {columns}
615
+ menuItems={[
616
+ { id: 'edit', label: 'Edit', icon: 'pencil' },
617
+ { id: 'sep', kind: 'separator', label: '' },
618
+ { id: 'delete', label: 'Delete', icon: 'trash', onselect: (row) => handleDelete(row) },
619
+ ]}
620
+ onMenuItemSelect={(item, rowData) => console.log(item.id, rowData)}
621
+ />
622
+ ```
623
+
624
+ Built-in items (Refresh, Clear Search, Clear Filters, Clear Sort) appear first; custom `menuItems` are appended after a divider.
625
+
626
+ #### Public methods
627
+
628
+ | Method | Returns | Description |
629
+ |---|---|---|
630
+ | `getSelectedRows()` | `Record<string, unknown>[]` | Currently selected row objects. |
631
+ | `getLoadedData()` | `Record<string, unknown>[]` | All rows in the current data set (filtered local data or full server page buffer). |
632
+
633
+ ```svelte
634
+ <script lang="ts">
635
+ import { GridlerFull } from '@warkypublic/svelix';
636
+
637
+ let grid: ReturnType<typeof GridlerFull>;
638
+ </script>
639
+
640
+ <GridlerFull bind:this={grid} {columns} {dataSourceOptions} />
641
+
642
+ <button onclick={() => console.log(grid.getSelectedRows())}>Log selection</button>
643
+ ```
644
+
573
645
  Key behaviors:
574
646
  - `extraOptions` is stored on the adapter at construction time; changing `dataSourceOptions` recreates the adapter and triggers a full refetch.
575
647
  - Call-time `extraOptions` passed directly to `readPage` override the config-level defaults; specific params (sort, filters, limit, cursor, columns) override both.
@@ -41,6 +41,7 @@ This file now tracks the implemented state of the project rather than the origin
41
41
  ### Data and explorer components
42
42
  - `Gridler`
43
43
  - `GridlerFull`
44
+ - `onSelectionChange(selection, rowData?)` — second arg carries resolved row data for all selected rows (cell, row, range types)
44
45
  - `SvarkGrid`
45
46
  - `VTree`
46
47
  - shared generic grid types and supporting utilities
@@ -39,7 +39,7 @@ onGridEvent?: (
39
39
  | `sort_changed` | Sort order applied or cleared | — | — | — | `{ sortOrder: GridColumnSortOrder }` |
40
40
  | `filter_changed` | Filter applied or cleared | — | — | — | `{ filters: GridColumnFilters }` |
41
41
  | `search_changed` | Search input value changed | — | — | — | `{ searchValue: string }` |
42
- | `selection_changed` | Cell, row, range, or column selection changes | — | — | — | — |
42
+ | `selection_changed` | Cell, row, range, or column selection changes | — | — | — | — | (use `onSelectionChange` for the selection object + row data) |
43
43
  | `column_moved` | Column dragged to new position | — | — | — | `{ from: number, to: number }` |
44
44
  | `column_resized` | Column edge dragged to new width | — | moved column | — | `{ width: number }` |
45
45
  | `data_changed` | Cell edited and committed (local mode only) | updated row | edited column | — | `{ data: Row[] }` full new dataset |
@@ -47,6 +47,55 @@ onGridEvent?: (
47
47
 
48
48
  ---
49
49
 
50
+ ## `onSelectionChange`
51
+
52
+ Fired on every selection change — mouse clicks, arrow-key navigation, Ctrl+A, row-marker toggles.
53
+
54
+ ```ts
55
+ onSelectionChange?: (
56
+ selection: Selection,
57
+ rowData?: Record<string, unknown>[], // resolved row data for all selected rows
58
+ ) => void
59
+ ```
60
+
61
+ ### `Selection` type
62
+
63
+ ```ts
64
+ type Selection =
65
+ | { type: "none" }
66
+ | { type: "cell"; item: [col, row] }
67
+ | { type: "range"; start: [col, row]; end: [col, row] }
68
+ | { type: "row"; rows: number[] }
69
+ | { type: "column"; columns: number[] }
70
+ ```
71
+
72
+ ### `rowData` population
73
+
74
+ | Selection type | `rowData` |
75
+ |---|---|
76
+ | `cell` | single-element array with the row's data |
77
+ | `row` | array of data for each selected row |
78
+ | `range` | array of data for every row from `start[1]` to `end[1]` |
79
+ | `column` / `none` | `undefined` |
80
+
81
+ `rowData` is `undefined` when no row data can be resolved (column/none selections, or when `getRowData`/`data` is not provided).
82
+
83
+ ### Usage
84
+
85
+ ```svelte
86
+ <GridlerFull
87
+ {columns}
88
+ {data}
89
+ onSelectionChange={(sel, rowData) => {
90
+ if (sel.type === 'cell') {
91
+ console.log('Selected row:', rowData?.[0]);
92
+ }
93
+ }}
94
+ />
95
+ ```
96
+
97
+ ---
98
+
50
99
  ## `onCellEvent`
51
100
 
52
101
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warkypublic/svelix",
3
- "version": "0.1.41",
3
+ "version": "0.1.43",
4
4
  "description": "Svelte 5 component library with Skeleton UI and Tailwind CSS",
5
5
  "license": "Apache-2.0",
6
6
  "exports": {