@warkypublic/svelix 0.1.28 → 0.1.31

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.
@@ -97,6 +97,7 @@
97
97
  let dropdownZ = $state(1100);
98
98
  let pointerInteractingWithDropdown = $state(false);
99
99
  let insideDialog = $state(false);
100
+ let suppressOpenOnFocus = $state(false);
100
101
  const effectiveDisablePortal = $derived(disablePortal || insideDialog);
101
102
  // Plain variable — NOT $state to avoid deep proxy on the complex virtualizer object.
102
103
  let rawVirtualizer: SvelteVirtualizer<
@@ -303,6 +304,12 @@
303
304
  activeOptionIndex = null;
304
305
  }
305
306
  break;
307
+ case "Tab":
308
+ if (multiSelect && $store.opened && $store.boxerData.length > 0) {
309
+ e.preventDefault();
310
+ onOptionSubmit(0);
311
+ }
312
+ break;
306
313
  }
307
314
  }
308
315
 
@@ -331,6 +338,7 @@
331
338
  e.preventDefault();
332
339
  store.setOpened(false);
333
340
  activeOptionIndex = null;
341
+ suppressOpenOnFocus = true;
334
342
  targetRef?.focus();
335
343
  break;
336
344
  }
@@ -387,6 +395,7 @@
387
395
  activeOptionIndex = null;
388
396
  }
389
397
  export function focus() {
398
+ suppressOpenOnFocus = false;
390
399
  targetRef?.focus();
391
400
  }
392
401
  export function getValue() {
@@ -504,7 +513,13 @@
504
513
  isFetching={$store.isFetching}
505
514
  search={$store.input}
506
515
  onclear={onClear}
507
- onfocus={() => store.setOpened(true)}
516
+ onfocus={() => {
517
+ if (suppressOpenOnFocus) {
518
+ suppressOpenOnFocus = false;
519
+ return;
520
+ }
521
+ store.setOpened(true);
522
+ }}
508
523
  onkeydown={onInputKeydown}
509
524
  onsearch={(v) => {
510
525
  store.setSearch(v);
@@ -6,5 +6,5 @@ export declare class GridlerResolveSpecAdapter {
6
6
  private readonly entity;
7
7
  readonly uniqueID: string;
8
8
  constructor(config: GridlerAdapterConfig);
9
- readPage(limit: number, cursorForward?: string, sort?: SortOption[], filters?: FilterOption[], extraOptions?: Partial<Options>): Promise<GridlerPageResult>;
9
+ readPage(limit: number, cursorForward?: string, sort?: SortOption[], filters?: FilterOption[], fields?: string[], extraOptions?: Partial<Options>): Promise<GridlerPageResult>;
10
10
  }
@@ -11,13 +11,14 @@ export class GridlerResolveSpecAdapter {
11
11
  this.entity = config.entity;
12
12
  this.uniqueID = config.uniqueID ?? 'id';
13
13
  }
14
- async readPage(limit, cursorForward, sort, filters, extraOptions) {
14
+ async readPage(limit, cursorForward, sort, filters, fields, extraOptions) {
15
15
  const options = {
16
16
  ...extraOptions,
17
17
  sort: sort ?? extraOptions?.sort ?? [],
18
18
  filters: filters ?? extraOptions?.filters ?? [],
19
19
  limit,
20
20
  ...(cursorForward != null ? { cursor_forward: cursorForward } : {}),
21
+ ...(fields?.length ? { columns: fields } : {}),
21
22
  };
22
23
  const response = await this.client.read(this.schema, this.entity, undefined, options);
23
24
  const rows = Array.isArray(response?.data) ? response.data : [];
@@ -6,5 +6,5 @@ export declare class GridlerRestHeaderSpecAdapter {
6
6
  private readonly entity;
7
7
  readonly uniqueID: string;
8
8
  constructor(config: GridlerAdapterConfig);
9
- readPage(limit: number, cursorForward?: string, sort?: SortOption[], filters?: FilterOption[], extraOptions?: Partial<Options>): Promise<GridlerPageResult>;
9
+ readPage(limit: number, cursorForward?: string, sort?: SortOption[], filters?: FilterOption[], fields?: string[], extraOptions?: Partial<Options>): Promise<GridlerPageResult>;
10
10
  }
@@ -11,13 +11,14 @@ export class GridlerRestHeaderSpecAdapter {
11
11
  this.entity = config.entity;
12
12
  this.uniqueID = config.uniqueID ?? 'id';
13
13
  }
14
- async readPage(limit, cursorForward, sort, filters, extraOptions) {
14
+ async readPage(limit, cursorForward, sort, filters, fields, extraOptions) {
15
15
  const options = {
16
16
  ...extraOptions,
17
17
  sort: sort ?? extraOptions?.sort ?? [],
18
18
  filters: filters ?? extraOptions?.filters ?? [],
19
19
  limit,
20
20
  ...(cursorForward != null ? { cursor_forward: cursorForward } : {}),
21
+ ...(fields?.length ? { columns: fields } : {}),
21
22
  };
22
23
  const response = await this.client.read(this.schema, this.entity, undefined, options);
23
24
  const rows = Array.isArray(response?.data) ? response.data : [];
@@ -28,6 +28,8 @@
28
28
  item?: Record<string, unknown>;
29
29
  column?: GridlerColumn;
30
30
  }) => void;
31
+ /** Resolve raw row data by row index for populating onCellEvent. */
32
+ getRowData?: (row: number) => Record<string, unknown> | undefined;
31
33
  children?: Snippet;
32
34
  }
33
35
 
@@ -68,8 +70,9 @@
68
70
  sortOrder,
69
71
  onFilterChange: _onFilterChange,
70
72
  filters: _filters,
71
- selectedItems: _selectedItems,
73
+ selectedItems,
72
74
  onSelectedItemsChange: _onSelectedItemsChange,
75
+ getRowData,
73
76
  settings,
74
77
  children,
75
78
  }: Props = $props();
@@ -245,6 +248,7 @@
245
248
 
246
249
  function handleKeyDown(e: KeyboardEvent) {
247
250
  if (!canvasComponent?.getHasFocus()) return;
251
+ onGridEvent?.("keydown", undefined, undefined, { x: 0, y: 0, code: e.key });
248
252
 
249
253
  // Search toggle
250
254
  if ((e.ctrlKey || e.metaKey) && e.key === "f") {
@@ -308,7 +312,7 @@
308
312
  if (!resolvedReadonly) {
309
313
  beginEdit(focusedCell);
310
314
  }
311
- onCellEvent?.("enter_key", {}, columns[col] ?? { id: "", title: "" });
315
+ onCellEvent?.("enter_key", getRowData?.(row) ?? {}, columns[col] ?? { id: "", title: "" });
312
316
  e.preventDefault();
313
317
  return;
314
318
  case "Escape": {
@@ -334,7 +338,7 @@
334
338
  onRowAppended?.();
335
339
  }
336
340
  }
337
- onCellEvent?.("tab_key", {}, columns[col] ?? { id: "", title: "" });
341
+ onCellEvent?.("tab_key", getRowData?.(row) ?? {}, columns[col] ?? { id: "", title: "" });
338
342
  e.preventDefault();
339
343
  break;
340
344
  case "Delete":
@@ -346,7 +350,7 @@
346
350
  onDelete?.(delSel);
347
351
  }
348
352
  }
349
- onCellEvent?.("delte_key", {}, columns[col] ?? { id: "", title: "" });
353
+ onCellEvent?.("delete_key", getRowData?.(row) ?? {}, columns[col] ?? { id: "", title: "" });
350
354
  return;
351
355
  }
352
356
  default:
@@ -376,7 +380,7 @@
376
380
 
377
381
  function handleVisibleRangeChange(range: VisibleRange) {
378
382
  onVisibleRangeChange?.(range);
379
- onGridEvent?.("scroll", { row: range.firstRow, column: range.firstCol });
383
+ onGridEvent?.("scroll", undefined, undefined, undefined, { row: range.firstRow, column: range.firstCol });
380
384
  }
381
385
 
382
386
  async function handleMenuOpen(d: {
@@ -423,15 +427,45 @@
423
427
  onSelectionChange={(sel) => {
424
428
  currentSelection = sel;
425
429
  onSelectionChange?.(sel);
430
+ onGridEvent?.("selection_changed");
426
431
  }}
427
432
  onCellDblClick={(item, cell) => {
428
433
  onCellDblClick?.(item, cell);
429
- onCellEvent?.("dblclick", {}, columns[item[0]] ?? { id: "", title: "" });
434
+ onCellEvent?.("dblclick", getRowData?.(item[1]) ?? {}, columns[item[0]] ?? { id: "", title: "" });
435
+ onGridEvent?.("dblclick", getRowData?.(item[1]) ?? {}, columns[item[0]] ?? { id: "", title: "" });
436
+ }}
437
+ onCellClick={(item) => {
438
+ onCellEvent?.("click", getRowData?.(item[1]) ?? {}, columns[item[0]] ?? { id: "", title: "" });
439
+ onGridEvent?.("click", getRowData?.(item[1]) ?? {}, columns[item[0]] ?? { id: "", title: "" });
440
+ }}
441
+ onCellHover={(item) => {
442
+ onCellEvent?.("hover", getRowData?.(item[1]) ?? {}, columns[item[0]] ?? { id: "", title: "" });
443
+ onGridEvent?.("hover", getRowData?.(item[1]) ?? {}, columns[item[0]] ?? { id: "", title: "" });
444
+ }}
445
+ onCellLeave={(item) => {
446
+ onCellEvent?.("leave", getRowData?.(item[1]) ?? {}, columns[item[0]] ?? { id: "", title: "" });
447
+ onGridEvent?.("leave", getRowData?.(item[1]) ?? {}, columns[item[0]] ?? { id: "", title: "" });
448
+ }}
449
+ onCellContextMenu={(item, x, y) => {
450
+ onCellEvent?.("contextmenu", getRowData?.(item[1]) ?? {}, columns[item[0]] ?? { id: "", title: "" }, { x, y });
451
+ onGridEvent?.("contextmenu", getRowData?.(item[1]) ?? {}, columns[item[0]] ?? { id: "", title: "" }, { x, y });
452
+ }}
453
+ onColumnResized={(col, width) => {
454
+ onGridEvent?.("column_resized", undefined, columns[col], undefined, { width });
455
+ }}
456
+ onGridResize={(width, height) => {
457
+ onGridEvent?.("resize", undefined, undefined, undefined, { width, height });
458
+ }}
459
+ onGridEnter={() => {
460
+ onGridEvent?.("enter");
430
461
  }}
431
462
  {sortOrder}
432
463
  {onSortOrderChange}
433
464
  {onHeaderContextMenu}
434
- {onColumnMoved}
465
+ onColumnMoved={onColumnMoved || onGridEvent ? (from, to) => {
466
+ onColumnMoved?.(from, to);
467
+ onGridEvent?.("column_moved", undefined, undefined, undefined, { from, to });
468
+ } : undefined}
435
469
  {onCellEdited}
436
470
  {onDelete}
437
471
  {onRowAppended}
@@ -13,6 +13,8 @@ export interface Props extends Partial<GridlerProps> {
13
13
  item?: Record<string, unknown>;
14
14
  column?: GridlerColumn;
15
15
  }) => void;
16
+ /** Resolve raw row data by row index for populating onCellEvent. */
17
+ getRowData?: (row: number) => Record<string, unknown> | undefined;
16
18
  children?: Snippet;
17
19
  }
18
20
  declare const Gridler: import("svelte").Component<Props, {}, "">;
@@ -72,6 +72,13 @@
72
72
  item?: Record<string, unknown>;
73
73
  column?: GridColumn<Record<string, unknown>>;
74
74
  }) => void;
75
+ onCellClick?: (item: Item) => void;
76
+ onCellHover?: (item: Item) => void;
77
+ onCellLeave?: (item: Item) => void;
78
+ onCellContextMenu?: (item: Item, x: number, y: number) => void;
79
+ onColumnResized?: (col: number, width: number) => void;
80
+ onGridResize?: (width: number, height: number) => void;
81
+ onGridEnter?: () => void;
75
82
  onkeydown?: (e: KeyboardEvent) => void;
76
83
  children?: Snippet;
77
84
  }
@@ -111,6 +118,13 @@
111
118
  onSearchValueChange,
112
119
  onSearchClose,
113
120
  onGridMenuOpen,
121
+ onCellClick,
122
+ onCellHover,
123
+ onCellLeave,
124
+ onCellContextMenu,
125
+ onColumnResized,
126
+ onGridResize,
127
+ onGridEnter,
114
128
  onkeydown,
115
129
  children,
116
130
  }: Props = $props();
@@ -133,6 +147,7 @@
133
147
  let hasFocus = $state(false);
134
148
  const gridId = `gridler-${Math.random().toString(36).slice(2, 10)}`;
135
149
 
150
+ let hoveredCell = $state<Item | null>(null);
136
151
  let isDraggingVScrollbar = $state(false);
137
152
  let isDraggingHScrollbar = $state(false);
138
153
  let vScrollDragStartY = $state(0);
@@ -286,6 +301,7 @@
286
301
  if (entry) {
287
302
  containerWidth = entry.contentRect.width;
288
303
  containerHeight = entry.contentRect.height;
304
+ onGridResize?.(entry.contentRect.width, entry.contentRect.height);
289
305
  }
290
306
  });
291
307
  observer.observe(containerRef);
@@ -468,8 +484,7 @@
468
484
  // ── Header context menu ───────────────────────────────────────────────────────
469
485
 
470
486
  function handleContextMenu(e: MouseEvent) {
471
- if (!onHeaderContextMenu) return;
472
- if (e.offsetY >= headerHeight || e.offsetX < rowMarkerWidth) return;
487
+ if (e.offsetX < rowMarkerWidth) return;
473
488
  const fixedBoundaryX =
474
489
  fixedColumns > 0
475
490
  ? getColumnX(effectiveColumns, Math.min(fixedColumns, columns.length))
@@ -477,10 +492,31 @@
477
492
  const localX = e.offsetX - rowMarkerWidth;
478
493
  const effectiveScrollX =
479
494
  fixedColumns > 0 && localX >= 0 && localX < fixedBoundaryX ? 0 : scrollX;
480
- const col = getColumnFromX(localX + effectiveScrollX, effectiveColumns);
481
- if (col >= 0 && col < columns.length) {
482
- e.preventDefault();
483
- onHeaderContextMenu(col, e.clientX, e.clientY);
495
+
496
+ if (e.offsetY < headerHeight) {
497
+ if (!onHeaderContextMenu) return;
498
+ const col = getColumnFromX(localX + effectiveScrollX, effectiveColumns);
499
+ if (col >= 0 && col < columns.length) {
500
+ e.preventDefault();
501
+ onHeaderContextMenu(col, e.clientX, e.clientY);
502
+ }
503
+ return;
504
+ }
505
+
506
+ if (onCellContextMenu) {
507
+ const cell = getCellFromPoint(
508
+ localX,
509
+ e.offsetY,
510
+ effectiveScrollX,
511
+ scrollY,
512
+ effectiveColumns,
513
+ rowHeight,
514
+ headerHeight,
515
+ );
516
+ if (cell) {
517
+ e.preventDefault();
518
+ onCellContextMenu(cell, e.clientX, e.clientY);
519
+ }
484
520
  }
485
521
  }
486
522
 
@@ -619,6 +655,7 @@
619
655
  }
620
656
 
621
657
  if (cell) {
658
+ onCellClick?.(cell);
622
659
  if (e.shiftKey && focusedCell) {
623
660
  const newSelection: Selection = {
624
661
  type: "range",
@@ -802,6 +839,36 @@
802
839
  return;
803
840
  }
804
841
 
842
+ // Hover cell tracking
843
+ if (e.offsetY >= headerHeight && e.offsetX >= rowMarkerWidth) {
844
+ const hoverFixedBoundaryX =
845
+ fixedColumns > 0
846
+ ? getColumnX(effectiveColumns, Math.min(fixedColumns, columns.length))
847
+ : 0;
848
+ const hoverLocalX = e.offsetX - rowMarkerWidth;
849
+ const hoverScrollX =
850
+ fixedColumns > 0 && hoverLocalX >= 0 && hoverLocalX < hoverFixedBoundaryX
851
+ ? 0
852
+ : scrollX;
853
+ const hoverCell = getCellFromPoint(
854
+ hoverLocalX,
855
+ e.offsetY,
856
+ hoverScrollX,
857
+ scrollY,
858
+ effectiveColumns,
859
+ rowHeight,
860
+ headerHeight,
861
+ );
862
+ if (hoverCell?.[0] !== hoveredCell?.[0] || hoverCell?.[1] !== hoveredCell?.[1]) {
863
+ if (hoveredCell) onCellLeave?.(hoveredCell);
864
+ hoveredCell = hoverCell;
865
+ if (hoverCell) onCellHover?.(hoverCell);
866
+ }
867
+ } else if (hoveredCell !== null) {
868
+ onCellLeave?.(hoveredCell);
869
+ hoveredCell = null;
870
+ }
871
+
805
872
  if (!isDragging || !dragStart) return;
806
873
 
807
874
  const fixedBoundaryX =
@@ -865,6 +932,9 @@
865
932
  scheduleDraw();
866
933
  }
867
934
 
935
+ if (resizingColumn !== null) {
936
+ onColumnResized?.(resizingColumn, columns[resizingColumn].width);
937
+ }
868
938
  isDragging = false;
869
939
  dragStart = null;
870
940
  resizingColumn = null;
@@ -876,6 +946,10 @@
876
946
  hoverResizeCol = null;
877
947
  hoverMenuButton = false;
878
948
  hoverSortCol = null;
949
+ if (hoveredCell) {
950
+ onCellLeave?.(hoveredCell);
951
+ hoveredCell = null;
952
+ }
879
953
  if (!isDraggingVScrollbar && !isDraggingHScrollbar) {
880
954
  handleMouseUp(e);
881
955
  } else {
@@ -1073,6 +1147,7 @@
1073
1147
  style:--gridler-header-height={`${headerHeight}px`}
1074
1148
  onfocusin={handleFocusIn}
1075
1149
  onfocusout={handleFocusOut}
1150
+ onmouseenter={onGridEnter}
1076
1151
  {onkeydown}
1077
1152
  >
1078
1153
  <canvas
@@ -41,6 +41,13 @@ interface Props {
41
41
  item?: Record<string, unknown>;
42
42
  column?: GridColumn<Record<string, unknown>>;
43
43
  }) => void;
44
+ onCellClick?: (item: Item) => void;
45
+ onCellHover?: (item: Item) => void;
46
+ onCellLeave?: (item: Item) => void;
47
+ onCellContextMenu?: (item: Item, x: number, y: number) => void;
48
+ onColumnResized?: (col: number, width: number) => void;
49
+ onGridResize?: (width: number, height: number) => void;
50
+ onGridEnter?: () => void;
44
51
  onkeydown?: (e: KeyboardEvent) => void;
45
52
  children?: Snippet;
46
53
  }
@@ -199,9 +199,11 @@
199
199
  if (typeof data === "function") {
200
200
  data().then((result) => {
201
201
  dataState = result;
202
+ onGridEvent?.("load");
202
203
  });
203
204
  } else {
204
205
  dataState = data.slice();
206
+ onGridEvent?.("load");
205
207
  }
206
208
  });
207
209
 
@@ -258,6 +260,7 @@
258
260
  internalSearchValue = value;
259
261
  }
260
262
  onSearchValueChange?.(value);
263
+ onGridEvent?.("search_changed", undefined, undefined, undefined, { searchValue: value });
261
264
  }
262
265
 
263
266
  // ── Context menu ───────────────────────────────────────────────────────────
@@ -477,6 +480,7 @@
477
480
  function handleFilterChange(next: GridColumnFilters) {
478
481
  if (filters === undefined) internalFilters = next;
479
482
  onFilterChange?.(next);
483
+ onGridEvent?.("filter_changed", undefined, undefined, undefined, { filters: next });
480
484
  }
481
485
 
482
486
 
@@ -513,6 +517,15 @@
513
517
  return [...base, ...buildSearchFilters(search, columnsState, searchColumns)];
514
518
  }
515
519
 
520
+ // Compute column fields (dataKey ?? id) merged with hotfields, deduped.
521
+ const resolvedFields = $derived.by(() => {
522
+ const columnFields = columnsState.map((c) => c.dataKey ?? c.id);
523
+ const hotfields = dataSourceOptions?.hotfields ?? [];
524
+ const seen = new Set(columnFields);
525
+ const extra = hotfields.filter((f) => !seen.has(f));
526
+ return [...columnFields, ...extra];
527
+ });
528
+
516
529
  // ── Sort state (controlled or internal) ───────────────────────────────────
517
530
 
518
531
  let internalSortOrder = $state<GridColumnSortOrder>({});
@@ -521,6 +534,7 @@
521
534
  function handleSortOrderChange(order: GridColumnSortOrder) {
522
535
  if (sortOrder === undefined) internalSortOrder = order;
523
536
  onSortOrderChange?.(order);
537
+ onGridEvent?.("sort_changed", undefined, undefined, undefined, { sortOrder: order });
524
538
  }
525
539
 
526
540
  const resolvedSort = $derived(
@@ -561,7 +575,7 @@
561
575
  const allFilters = buildAllFilters(_search);
562
576
 
563
577
  _adapter
564
- .readPage(_pageSize, undefined, _sort, allFilters)
578
+ .readPage(_pageSize, undefined, _sort, allFilters, resolvedFields)
565
579
  .then((result) => {
566
580
  if (cancelled) return;
567
581
  serverData = result.data;
@@ -604,11 +618,13 @@
604
618
  cursorSnapshot,
605
619
  resolvedSort,
606
620
  allFilters,
621
+ resolvedFields,
607
622
  );
608
623
  if (serverData.length !== lengthSnapshot) return;
609
624
  serverData = [...serverData, ...result.data];
610
625
  serverCursor = result.nextCursor;
611
626
  serverAllLoaded = result.data.length < pageSize || !result.nextCursor;
627
+ onGridEvent?.("page_loaded", undefined, undefined, undefined, { data: result.data, total: serverData.length });
612
628
  } catch {
613
629
  // Silent — don't clobber existing rows with an error on append.
614
630
  } finally {
@@ -642,16 +658,39 @@
642
658
 
643
659
  // ── Selection → selectedItems bridge ──────────────────────────────────────
644
660
 
661
+ let internalSelectedItems = $state<Record<string, unknown>[]>([]);
662
+
645
663
  function handleSelectionChange(sel: Selection) {
646
- if (sel.type === "row" && onSelectedItemsChange) {
647
- const items = sel.rows
664
+ let items: Record<string, unknown>[] = [];
665
+ if (sel.type === "row") {
666
+ items = sel.rows
648
667
  .map((i) => resolveRowData(i))
649
668
  .filter((r): r is Record<string, unknown> => r !== undefined);
650
- onSelectedItemsChange(items);
669
+ } else if (sel.type === "cell") {
670
+ const row = resolveRowData(sel.item[1]);
671
+ if (row) items = [row];
672
+ } else if (sel.type === "range") {
673
+ const minRow = Math.min(sel.start[1], sel.end[1]);
674
+ const maxRow = Math.max(sel.start[1], sel.end[1]);
675
+ for (let i = minRow; i <= maxRow; i++) {
676
+ const row = resolveRowData(i);
677
+ if (row) items.push(row);
678
+ }
651
679
  }
680
+ internalSelectedItems = items;
681
+ if (items.length > 0) onSelectedItemsChange?.(items);
652
682
  onSelectionChange?.(sel);
653
683
  }
654
684
 
685
+ // ── Settings change event ─────────────────────────────────────────────────
686
+
687
+ let lastSettings = $state(settings);
688
+ $effect(() => {
689
+ if (settings === untrack(() => lastSettings)) return;
690
+ lastSettings = settings;
691
+ onGridEvent?.("settings_changed", undefined, undefined, undefined, { settings });
692
+ });
693
+
655
694
  // ── Cell content ───────────────────────────────────────────────────────────
656
695
 
657
696
  const serverGetCellContent = $derived(makeGetCellContent(serverData, columnsState));
@@ -690,6 +729,7 @@
690
729
  };
691
730
  dataState = next;
692
731
  await onDataChange?.(next);
732
+ onGridEvent?.("data_changed", next[originalIndex], columnsState[col], undefined, { data: next });
693
733
  }
694
734
 
695
735
  async function handleGridMenuClick(d?: {
@@ -737,7 +777,10 @@
737
777
  onCellEdited?.(item, value);
738
778
  }}
739
779
  onCellDblClick={handleCellDblClick}
780
+ selectedItems={internalSelectedItems}
781
+ getRowData={resolveRowData}
740
782
  onSelectionChange={handleSelectionChange}
783
+ {onMenuClick}
741
784
  onGridMenuOpen={handleGridMenuClick}
742
785
  sortOrder={resolvedSortOrder}
743
786
  onSortOrderChange={handleSortOrderChange}
@@ -71,6 +71,7 @@ export interface GridCommonProps<RowDataType = unknown, CellType = unknown> {
71
71
  entity?: string;
72
72
  uniqueID?: string;
73
73
  headers?: Record<string, string>;
74
+ hotfields?: string[];
74
75
  };
75
76
  data?: RowDataType[] | (() => Promise<RowDataType[]>);
76
77
  onDataChange?: (data: RowDataType[]) => Promise<void>;
package/llm/README.md CHANGED
@@ -16,6 +16,7 @@ This folder contains AI-readable documentation that ships with the package.
16
16
  - [tools/SVAR.md](./tools/SVAR.md)
17
17
  - [tools/resolvespec-js.md](./tools/resolvespec-js.md)
18
18
  - [plans/canvasgrid.md](./plans/canvasgrid.md)
19
+ - [gridler-events.md](./gridler-events.md)
19
20
 
20
21
  ## Installed package paths
21
22
 
@@ -0,0 +1,139 @@
1
+ # Gridler Events
2
+
3
+ `GridlerFull` (and the lower-level `Gridler`) emit two event callbacks defined in `GridCommonProps` (`src/lib/components/Types/generic_grid.ts`):
4
+
5
+ | Callback | Purpose |
6
+ |---|---|
7
+ | `onGridEvent` | Grid-level events (interaction, state, lifecycle) |
8
+ | `onCellEvent` | Cell-level events (mouse + keyboard on a specific cell) |
9
+
10
+ ---
11
+
12
+ ## `onGridEvent`
13
+
14
+ ```ts
15
+ onGridEvent?: (
16
+ type: GridEventType,
17
+ item?: Record<string, unknown>, // resolved raw row data
18
+ column?: GridColumn,
19
+ coords?: GridEventCoords, // { x, y, code? }
20
+ detail?: GridEventDetail, // event-specific payload
21
+ ) => void
22
+ ```
23
+
24
+ ### All `GridEventType` values
25
+
26
+ | Type | Fired from | `item` | `column` | `coords` | `detail` |
27
+ |---|---|---|---|---|---|
28
+ | `click` | Mouse click on a cell | row data | clicked column | — | — |
29
+ | `dblclick` | Double-click on a cell | row data | clicked column | — | — |
30
+ | `contextmenu` | Right-click on a cell | row data | clicked column | `{ x, y }` client coords | — |
31
+ | `keydown` | Any key pressed while grid focused | — | — | `{ x:0, y:0, code }` | — |
32
+ | `hover` | Mouse moves over a new cell | row data | hovered column | — | — |
33
+ | `leave` | Mouse leaves a cell | row data | vacated column | — | — |
34
+ | `enter` | Mouse enters the grid container | — | — | — | — |
35
+ | `load` | Local `data` prop set/changed, or first server page loaded | — | — | — | — |
36
+ | `scroll` | Visible range changes (scroll) | — | — | — | `{ row, column }` first visible |
37
+ | `resize` | Grid container resized (ResizeObserver) | — | — | — | `{ width, height }` |
38
+ | `page_loaded` | Next cursor page loaded from server | — | — | — | `{ data, total }` |
39
+ | `sort_changed` | Sort order applied or cleared | — | — | — | `{ sortOrder: GridColumnSortOrder }` |
40
+ | `filter_changed` | Filter applied or cleared | — | — | — | `{ filters: GridColumnFilters }` |
41
+ | `search_changed` | Search input value changed | — | — | — | `{ searchValue: string }` |
42
+ | `selection_changed` | Cell, row, range, or column selection changes | — | — | — | — |
43
+ | `column_moved` | Column dragged to new position | — | — | — | `{ from: number, to: number }` |
44
+ | `column_resized` | Column edge dragged to new width | — | moved column | — | `{ width: number }` |
45
+ | `data_changed` | Cell edited and committed (local mode only) | updated row | edited column | — | `{ data: Row[] }` full new dataset |
46
+ | `settings_changed` | `settings` prop reference changed | — | — | — | `{ settings }` |
47
+
48
+ ---
49
+
50
+ ## `onCellEvent`
51
+
52
+ ```ts
53
+ onCellEvent?: (
54
+ type: GridCellEventType,
55
+ item: Record<string, unknown>, // resolved raw row data
56
+ column: GridColumn,
57
+ coords?: GridEventCoords,
58
+ detail?: GridEventDetail,
59
+ ) => void
60
+ ```
61
+
62
+ ### All `GridCellEventType` values
63
+
64
+ | Type | Fired when |
65
+ |---|---|
66
+ | `click` | Mouse click on the cell |
67
+ | `dblclick` | Double-click on the cell |
68
+ | `contextmenu` | Right-click on the cell |
69
+ | `hover` | Mouse moves over this cell |
70
+ | `leave` | Mouse leaves this cell |
71
+ | `enter_key` | Enter pressed while this cell is focused |
72
+ | `tab_key` | Tab pressed while this cell is focused |
73
+ | `delete_key` | Delete or Backspace pressed while this cell is focused |
74
+
75
+ For all `onCellEvent` calls, `item` carries the **resolved raw row data** (from `serverData` or local `filteredDataState`). When the row cannot be resolved by index (e.g. from a top-level menu button), `item` falls back to the value in `selectedItems[0]`.
76
+
77
+ ---
78
+
79
+ ## Row data resolution
80
+
81
+ `GridlerFull` passes `getRowData` to `Gridler`, which calls `resolveRowData(rowIndex)`:
82
+
83
+ ```ts
84
+ function resolveRowData(row: number): Record<string, unknown> | undefined {
85
+ return isServerMode ? serverData[row] : filteredDataState[row];
86
+ }
87
+ ```
88
+
89
+ The current selection's row data is also tracked in `internalSelectedItems` state and passed as `selectedItems` to `Gridler`. This is used as a fallback when no row index is available.
90
+
91
+ ---
92
+
93
+ ## Field selection sent to adapters (hotfields)
94
+
95
+ When `dataSource="resolvespec"` or `"headerspec"`, `GridlerFull` derives a `resolvedFields` array and passes it to every `readPage` call as `options.columns` (maps to `X-Select-Fields` in headerspec):
96
+
97
+ ```ts
98
+ const resolvedFields = $derived.by(() => {
99
+ const columnFields = columnsState.map((c) => c.dataKey ?? c.id);
100
+ const hotfields = dataSourceOptions?.hotfields ?? [];
101
+ const seen = new Set(columnFields);
102
+ const extra = hotfields.filter((f) => !seen.has(f));
103
+ return [...columnFields, ...extra];
104
+ });
105
+ ```
106
+
107
+ - **Column fields** come from `column.dataKey ?? column.id` for every column.
108
+ - **Hotfields** (`dataSourceOptions.hotfields`) are extra fields needed server-side (for filtering, sorting, FK lookups) that are not grid columns. They are appended without duplicating existing column fields.
109
+
110
+ ---
111
+
112
+ ## Usage example
113
+
114
+ ```svelte
115
+ <GridlerFull
116
+ {columns}
117
+ {data}
118
+ onGridEvent={(type, item, column, coords, detail) => {
119
+ if (type === 'sort_changed') console.log('Sort:', detail?.sortOrder);
120
+ if (type === 'filter_changed') console.log('Filter:', detail?.filters);
121
+ if (type === 'search_changed') console.log('Search:', detail?.searchValue);
122
+ if (type === 'data_changed') console.log('Edited row:', item, 'All data:', detail?.data);
123
+ if (type === 'column_moved') console.log('Moved col', detail?.from, '→', detail?.to);
124
+ if (type === 'page_loaded') console.log('New page:', detail?.data, 'Total so far:', detail?.total);
125
+ }}
126
+ onCellEvent={(type, item, column) => {
127
+ if (type === 'click') console.log('Clicked:', item, column.id);
128
+ if (type === 'enter_key') console.log('Enter on row:', item);
129
+ }}
130
+ dataSource="resolvespec"
131
+ dataSourceOptions={{
132
+ url: 'https://api.example.com',
133
+ schema: 'public',
134
+ entity: 'users',
135
+ uniqueID: 'id',
136
+ hotfields: ['tenant_id', 'deleted_at'], // fetched but not shown as columns
137
+ }}
138
+ />
139
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warkypublic/svelix",
3
- "version": "0.1.28",
3
+ "version": "0.1.31",
4
4
  "description": "Svelte 5 component library with Skeleton UI and Tailwind CSS",
5
5
  "license": "Apache-2.0",
6
6
  "exports": {
@@ -26,43 +26,43 @@
26
26
  },
27
27
  "devDependencies": {
28
28
  "@changesets/cli": "^2.30.0",
29
- "@chromatic-com/storybook": "^5.0.2",
30
- "@eslint/compat": "^2.0.3",
29
+ "@chromatic-com/storybook": "^5.1.1",
30
+ "@eslint/compat": "^2.0.4",
31
31
  "@eslint/js": "^10.0.1",
32
- "@playwright/test": "^1.58.2",
33
- "@sentry/svelte": "^10.45.0",
34
- "@skeletonlabs/skeleton": "^4.13.0",
35
- "@skeletonlabs/skeleton-svelte": "^4.13.0",
36
- "@storybook/addon-a11y": "^10.3.1",
37
- "@storybook/addon-docs": "^10.3.1",
38
- "@storybook/addon-svelte-csf": "^5.0.12",
39
- "@storybook/addon-vitest": "^10.3.1",
40
- "@storybook/sveltekit": "^10.3.1",
32
+ "@playwright/test": "^1.59.1",
33
+ "@sentry/svelte": "^10.47.0",
34
+ "@skeletonlabs/skeleton": "^4.15.1",
35
+ "@skeletonlabs/skeleton-svelte": "^4.15.1",
36
+ "@storybook/addon-a11y": "^10.3.4",
37
+ "@storybook/addon-docs": "^10.3.4",
38
+ "@storybook/addon-svelte-csf": "^5.1.2",
39
+ "@storybook/addon-vitest": "^10.3.4",
40
+ "@storybook/sveltekit": "^10.3.4",
41
41
  "@storybook/test": "^8.6.15",
42
42
  "@sveltejs/adapter-auto": "^7.0.1",
43
- "@sveltejs/kit": "^2.55.0",
43
+ "@sveltejs/kit": "^2.56.1",
44
44
  "@sveltejs/package": "^2.5.7",
45
45
  "@sveltejs/vite-plugin-svelte": "^7.0.0",
46
46
  "@tailwindcss/vite": "^4.2.2",
47
47
  "@tanstack/svelte-virtual": "^3.13.23",
48
- "@types/node": "^25.5.0",
49
- "@vitest/browser-playwright": "^4.1.0",
50
- "@vitest/coverage-v8": "^4.1.0",
51
- "eslint": "^10.0.3",
52
- "eslint-plugin-svelte": "^3.15.2",
48
+ "@types/node": "^25.5.2",
49
+ "@vitest/browser-playwright": "^4.1.3",
50
+ "@vitest/coverage-v8": "^4.1.3",
51
+ "eslint": "^10.2.0",
52
+ "eslint-plugin-svelte": "^3.17.0",
53
53
  "globals": "^17.4.0",
54
- "playwright": "^1.58.2",
54
+ "playwright": "^1.59.1",
55
55
  "publint": "^0.3.18",
56
- "storybook": "^10.3.1",
57
- "svelte": "^5.54.0",
58
- "svelte-check": "^4.4.5",
56
+ "storybook": "^10.3.4",
57
+ "svelte": "^5.55.1",
58
+ "svelte-check": "^4.4.6",
59
59
  "tailwindcss": "^4.2.2",
60
60
  "tslib": "^2.8.1",
61
- "typescript": "^5.9.3",
62
- "typescript-eslint": "^8.57.1",
63
- "vite": "^8.0.1",
61
+ "typescript": "^6.0.2",
62
+ "typescript-eslint": "^8.58.0",
63
+ "vite": "^8.0.7",
64
64
  "vite-plugin-monaco-editor": "^1.1.0",
65
- "vitest": "^4.1.0"
65
+ "vitest": "^4.1.3"
66
66
  },
67
67
  "svelte": "./dist/index.js",
68
68
  "types": "./dist/index.d.ts",
@@ -76,13 +76,13 @@
76
76
  "@cartamd/plugin-math": "^4.3.1",
77
77
  "@friendofsvelte/tipex": "^0.1.1",
78
78
  "@js-temporal/polyfill": "^0.5.1",
79
- "@svar-ui/svelte-grid": "^2.6.0",
79
+ "@svar-ui/svelte-grid": "^2.6.1",
80
80
  "@warkypublic/resolvespec-js": "^1.0.1",
81
81
  "@warkypublic/artemis-kit": "^1.0.10",
82
82
  "carta-md": "^4.11.1",
83
83
  "github-markdown-css": "^5.9.0",
84
- "isomorphic-dompurify": "^3.5.1",
85
- "katex": "^0.16.39",
84
+ "isomorphic-dompurify": "^3.7.1",
85
+ "katex": "^0.16.45",
86
86
  "monaco-editor": "^0.55.1"
87
87
  },
88
88
  "scripts": {