@svgrid/grid 2.1.6 → 2.1.7

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.
Files changed (94) hide show
  1. package/dist/SvCarousel.svelte +35 -6
  2. package/dist/SvCarousel.svelte.d.ts +3 -0
  3. package/dist/SvColorInput.svelte +0 -1
  4. package/dist/SvContextMenu.svelte +26 -6
  5. package/dist/SvDrawer.svelte +5 -3
  6. package/dist/SvGrid.controller.svelte.d.ts +85 -2
  7. package/dist/SvGrid.controller.svelte.js +494 -1
  8. package/dist/SvGrid.css +142 -0
  9. package/dist/SvGrid.svelte +49 -27
  10. package/dist/SvGrid.types.d.ts +78 -0
  11. package/dist/SvGridBoard.svelte +71 -13
  12. package/dist/SvGridChart.svelte +41 -7
  13. package/dist/SvGridChart.svelte.d.ts +4 -0
  14. package/dist/SvGridChartPanel.svelte +485 -0
  15. package/dist/SvGridChartPanel.svelte.d.ts +28 -0
  16. package/dist/SvGridDropdown.svelte +18 -2
  17. package/dist/SvGroupCell.svelte +5 -5
  18. package/dist/SvMenu.svelte +25 -2
  19. package/dist/SvMenu.svelte.d.ts +3 -0
  20. package/dist/SvMenuList.svelte +31 -9
  21. package/dist/SvMenuList.svelte.d.ts +9 -1
  22. package/dist/SvNumberInput.svelte +1 -1
  23. package/dist/SvOtpInput.svelte +3 -2
  24. package/dist/SvPopover.svelte +13 -1
  25. package/dist/SvRowGroupPanel.svelte +23 -0
  26. package/dist/SvScrollArea.svelte +13 -2
  27. package/dist/SvScrollArea.svelte.d.ts +2 -0
  28. package/dist/SvTooltip.svelte +27 -2
  29. package/dist/SvTooltip.svelte.d.ts +3 -2
  30. package/dist/SvTour.svelte +30 -5
  31. package/dist/SvTour.svelte.d.ts +3 -0
  32. package/dist/SvTree.svelte +7 -3
  33. package/dist/SvTreeSelect.svelte +16 -9
  34. package/dist/build-api.js +51 -0
  35. package/dist/cdn/svgrid.js +17394 -16052
  36. package/dist/cdn/svgrid.svelte-external.js +16784 -15644
  37. package/dist/chart-export.d.ts +21 -0
  38. package/dist/chart-export.js +52 -11
  39. package/dist/chart.d.ts +30 -0
  40. package/dist/chart.js +53 -13
  41. package/dist/createAutocomplete.svelte.d.ts +6 -0
  42. package/dist/createAutocomplete.svelte.js +10 -0
  43. package/dist/createCombobox.svelte.d.ts +1 -0
  44. package/dist/createCombobox.svelte.js +1 -0
  45. package/dist/createCountryInput.svelte.d.ts +5 -0
  46. package/dist/createCountryInput.svelte.js +11 -0
  47. package/dist/createDropdownList.svelte.d.ts +4 -0
  48. package/dist/createDropdownList.svelte.js +7 -0
  49. package/dist/createPopoverSelect.svelte.d.ts +5 -3
  50. package/dist/createPopoverSelect.svelte.js +5 -3
  51. package/dist/createTree.svelte.d.ts +1 -0
  52. package/dist/createTree.svelte.js +15 -2
  53. package/dist/index.d.ts +4 -2
  54. package/dist/index.js +3 -2
  55. package/dist/svgrid-wrapper.types.d.ts +58 -0
  56. package/package.json +1 -1
  57. package/src/SvCarousel.svelte +35 -6
  58. package/src/SvColorInput.svelte +0 -1
  59. package/src/SvContextMenu.svelte +26 -6
  60. package/src/SvDrawer.svelte +5 -3
  61. package/src/SvGrid.controller.svelte.ts +481 -0
  62. package/src/SvGrid.css +142 -0
  63. package/src/SvGrid.svelte +49 -27
  64. package/src/SvGrid.types.ts +83 -0
  65. package/src/SvGridBoard.svelte +71 -13
  66. package/src/SvGridChart.svelte +41 -7
  67. package/src/SvGridChartPanel.svelte +485 -0
  68. package/src/SvGridDropdown.svelte +18 -2
  69. package/src/SvGroupCell.svelte +5 -5
  70. package/src/SvMenu.svelte +25 -2
  71. package/src/SvMenuList.svelte +31 -9
  72. package/src/SvNumberInput.svelte +1 -1
  73. package/src/SvOtpInput.svelte +3 -2
  74. package/src/SvPopover.svelte +13 -1
  75. package/src/SvRowGroupPanel.svelte +23 -0
  76. package/src/SvScrollArea.svelte +13 -2
  77. package/src/SvTooltip.svelte +27 -2
  78. package/src/SvTour.svelte +30 -5
  79. package/src/SvTree.svelte +7 -3
  80. package/src/SvTreeSelect.svelte +16 -9
  81. package/src/build-api.ts +49 -0
  82. package/src/chart-export.ts +57 -0
  83. package/src/chart.ts +70 -13
  84. package/src/createAutocomplete.svelte.ts +11 -0
  85. package/src/createCombobox.svelte.ts +1 -0
  86. package/src/createCountryInput.svelte.ts +12 -0
  87. package/src/createDropdownList.svelte.ts +8 -0
  88. package/src/createPopoverSelect.svelte.ts +5 -3
  89. package/src/createTree.svelte.ts +15 -2
  90. package/src/index.ts +6 -0
  91. package/src/overlays.test.ts +5 -0
  92. package/src/svgrid-wrapper.types.ts +62 -0
  93. package/src/svgrid.charting.test.ts +527 -0
  94. package/src/test-setup.ts +22 -0
package/dist/index.js CHANGED
@@ -150,8 +150,9 @@ export { isDisabledDay, isOutOfRange, isRestricted, isImportant, } from './datet
150
150
  export { default as FlexRender } from './FlexRender.svelte';
151
151
  export { renderComponent, renderSnippet } from './render-component';
152
152
  export { default as SvGridChart } from './SvGridChart.svelte';
153
- export { buildChart, rowsToChartSpec, niceScale, niceLogScale, linearTrend, simpleMovingAverage, exponentialMovingAverage, computeOverlay, buildLinePath, sampleGradient, pickContrastText, DEFAULT_PALETTE, } from './chart';
154
- export { chartToSvgString, downloadChartSvg, chartToPngBlob, downloadChartPng, } from './chart-export';
153
+ export { default as SvGridChartPanel } from './SvGridChartPanel.svelte';
154
+ export { buildChart, formatChartValue, rowsToChartSpec, niceScale, niceLogScale, linearTrend, simpleMovingAverage, exponentialMovingAverage, computeOverlay, buildLinePath, sampleGradient, pickContrastText, DEFAULT_PALETTE, } from './chart';
155
+ export { chartToSvgString, downloadChartSvg, chartToPngBlob, downloadChartPng, chartSpecToCsv, downloadChartCsv, } from './chart-export';
155
156
  export { buildSparkline, toSparklineValues, } from './sparkline';
156
157
  export { spreadsheetLayout, spansToMerges, } from './spreadsheet';
157
158
  export { rowResize } from './row-resize';
@@ -1,5 +1,6 @@
1
1
  import type { CellFormatConfig, ColumnDef, RowData, SvGridOptions, TableFeatures } from './core';
2
2
  import type { GridExportOptions, GridClipboardOptions } from './export-format';
3
+ import type { ChartSpec, ChartType } from './chart';
3
4
  export type SvGridFilterOperator = 'contains' | 'equals' | 'startsWith' | 'greaterThan' | 'lessThan' | 'between' | 'isBlank';
4
5
  /**
5
6
  * A serializable snapshot of everything that makes up the current "view":
@@ -34,6 +35,32 @@ export type SvGridViewState = {
34
35
  }>;
35
36
  /** Facet (Excel-style value checklist) selections, keyed by column id. */
36
37
  facetFilters: Record<string, string[]>;
38
+ /** Built-in chart panel state (present only when `charting` is on): the
39
+ * ACTIVE chart, for back-compat + spread-and-tweak. */
40
+ chart?: {
41
+ open: boolean;
42
+ type: ChartType;
43
+ dimension: string | null;
44
+ series: string | null;
45
+ measure: string | null;
46
+ reduce: 'sum' | 'avg' | 'count';
47
+ stacked: boolean;
48
+ };
49
+ /** All charts in the tab strip (multi-chart), plus the active index. */
50
+ charts?: Array<{
51
+ title: string;
52
+ type: ChartType;
53
+ dimension: string | null;
54
+ series: string | null;
55
+ measure: string | null;
56
+ reduce: 'sum' | 'avg' | 'count';
57
+ stacked: boolean | null;
58
+ dataLabels: boolean | null;
59
+ logScale: boolean | null;
60
+ timeAxis: boolean | null;
61
+ valueFormat: 'number' | 'currency' | 'percent' | 'compact' | null;
62
+ }>;
63
+ chartActive?: number;
37
64
  };
38
65
  /**
39
66
  * A batch of row mutations for `api.applyTransaction`. `update` / `remove`
@@ -85,6 +112,37 @@ export type SvGridApi<TFeatures extends TableFeatures, TData extends RowData> =
85
112
  * `selectCells` accepts. Empty array when no range is active.
86
113
  */
87
114
  getSelected(): Array<[number, number, number, number]>;
115
+ /** Open the built-in chart panel. */
116
+ openChart(): void;
117
+ /** Close the built-in chart panel. */
118
+ closeChart(): void;
119
+ /** The live chart spec the panel is rendering, or `null`. */
120
+ getChartSpec(): ChartSpec | null;
121
+ /** Select cell rectangles + open the chart panel (scopes to the range). */
122
+ chartRange(ranges?: ReadonlyArray<readonly [number, number, number, number]>): void;
123
+ /**
124
+ * Configure the built-in chart panel's ACTIVE chart. Column references
125
+ * accept a column id OR field name. Opens the panel unless `open === false`.
126
+ */
127
+ configureChart(config: {
128
+ open?: boolean;
129
+ type?: ChartType;
130
+ dimension?: string | null;
131
+ series?: string | null;
132
+ measure?: string | null;
133
+ reduce?: 'sum' | 'avg' | 'count';
134
+ stacked?: boolean;
135
+ dataLabels?: boolean;
136
+ logScale?: boolean;
137
+ timeAxis?: boolean;
138
+ valueFormat?: 'number' | 'currency' | 'percent' | 'compact';
139
+ }): void;
140
+ /**
141
+ * Register a natural-language "chart this" handler. When set, the chart
142
+ * panel shows an AI button. `@svgrid/enterprise`'s `enableAiCharting(api)`
143
+ * fills this. Pass `null` to remove it.
144
+ */
145
+ setChartAiHandler(handler: ((prompt: string) => Promise<Record<string, unknown> | null>) | null): void;
88
146
  /** Add one row. `position` defaults to `'bottom'`. */
89
147
  addRow(row: TData, position?: 'top' | 'bottom' | number): void;
90
148
  addRows(rows: ReadonlyArray<TData>, position?: 'top' | 'bottom' | number): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svgrid/grid",
3
- "version": "2.1.6",
3
+ "version": "2.1.7",
4
4
  "description": "SvGrid - a headless-first, Svelte 5-native data grid engine and render component.",
5
5
  "author": "jQWidgets <boikom@jqwidgets.com>",
6
6
  "license": "MIT",
@@ -12,6 +12,7 @@
12
12
  * ```
13
13
  */
14
14
  import type { Snippet } from 'svelte'
15
+ import { type EditorDir } from './editor-contract'
15
16
 
16
17
  type Props = {
17
18
  count: number
@@ -24,6 +25,8 @@
24
25
  arrows?: boolean
25
26
  dots?: boolean
26
27
  ariaLabel?: string
28
+ /** Text direction (rtl mirrors the prev/next arrows + slide direction). */
29
+ dir?: EditorDir
27
30
  }
28
31
 
29
32
  let {
@@ -35,8 +38,12 @@
35
38
  arrows = true,
36
39
  dots = true,
37
40
  ariaLabel = 'Carousel',
41
+ dir,
38
42
  }: Props = $props()
39
43
 
44
+ const resolvedDir = $derived(dir === 'ltr' || dir === 'rtl' ? dir : undefined)
45
+ const rtl = $derived(resolvedDir === 'rtl')
46
+
40
47
  function go(i: number) {
41
48
  if (count === 0) return
42
49
  current = loop ? (i + count) % count : Math.max(0, Math.min(i, count - 1))
@@ -44,10 +51,13 @@
44
51
  const next = () => go(current + 1)
45
52
  const prev = () => go(current - 1)
46
53
 
47
- // Autoplay, paused while hovered/focused.
54
+ // Autoplay, paused while hovered/focused, or stopped via the persistent
55
+ // play/pause toggle (WCAG 2.2.2: auto-advancing content needs a way to stop
56
+ // it that doesn't depend on hover/focus).
48
57
  let paused = $state(false)
58
+ let userPaused = $state(false)
49
59
  $effect(() => {
50
- if (!autoplay || paused || count < 2) return
60
+ if (!autoplay || paused || userPaused || count < 2) return
51
61
  const t = setInterval(next, autoplay)
52
62
  return () => clearInterval(t)
53
63
  })
@@ -82,9 +92,9 @@
82
92
  >
83
93
  <!-- svelte-ignore a11y_no_static_element_interactions -->
84
94
  <div class="sv-carousel__viewport" onpointerdown={down} onpointermove={move} onpointerup={up} onpointercancel={up}>
85
- <div class="sv-carousel__track" class:is-dragging={dragging} style:transform={`translateX(calc(${-current * 100}% + ${dx}px))`}>
95
+ <div class="sv-carousel__track" class:is-dragging={dragging} style:transform={`translateX(calc(${(rtl ? current : -current) * 100}% + ${dx}px))`}>
86
96
  {#each Array(count) as _, i (i)}
87
- <div class="sv-carousel__slide" role="group" aria-roledescription="slide" aria-label={`${i + 1} of ${count}`} aria-hidden={i !== current}>
97
+ <div class="sv-carousel__slide" role="group" aria-roledescription="slide" aria-label={`${i + 1} of ${count}`} aria-hidden={i !== current} inert={i !== current}>
88
98
  {@render slide?.(i)}
89
99
  </div>
90
100
  {/each}
@@ -92,8 +102,8 @@
92
102
  </div>
93
103
 
94
104
  {#if arrows && count > 1}
95
- <button type="button" class="sv-carousel__arrow is-prev" aria-label="Previous slide" onclick={prev} disabled={!loop && current === 0}>‹</button>
96
- <button type="button" class="sv-carousel__arrow is-next" aria-label="Next slide" onclick={next} disabled={!loop && current === count - 1}>›</button>
105
+ <button type="button" class="sv-carousel__arrow is-prev" aria-label={rtl ? 'Next slide' : 'Previous slide'} onclick={rtl ? next : prev} disabled={rtl ? (!loop && current === count - 1) : (!loop && current === 0)}>{rtl ? '›' : '‹'}</button>
106
+ <button type="button" class="sv-carousel__arrow is-next" aria-label={rtl ? 'Previous slide' : 'Next slide'} onclick={rtl ? prev : next} disabled={rtl ? (!loop && current === 0) : (!loop && current === count - 1)}>{rtl ? '‹' : '›'}</button>
97
107
  {/if}
98
108
 
99
109
  {#if dots && count > 1}
@@ -103,6 +113,16 @@
103
113
  {/each}
104
114
  </div>
105
115
  {/if}
116
+
117
+ {#if autoplay > 0}
118
+ <button
119
+ type="button"
120
+ class="sv-carousel__playpause"
121
+ aria-label={userPaused ? 'Play autoplay' : 'Pause autoplay'}
122
+ aria-pressed={userPaused}
123
+ onclick={() => (userPaused = !userPaused)}
124
+ >{userPaused ? '▶' : '⏸'}</button>
125
+ {/if}
106
126
  </div>
107
127
 
108
128
  <style>
@@ -131,4 +151,13 @@
131
151
  background: color-mix(in srgb, var(--sg-fg, #0f172a) 30%, var(--sg-bg, #fff)); transition: background 0.15s, width 0.15s;
132
152
  }
133
153
  .sv-carousel__dot.is-active { width: 20px; border-radius: 4px; background: var(--sg-accent, #2563eb); }
154
+
155
+ .sv-carousel__playpause {
156
+ position: absolute; top: 10px; inset-inline-end: 10px; z-index: 2;
157
+ width: 28px; height: 28px; display: grid; place-items: center; border-radius: 50%;
158
+ background: color-mix(in srgb, var(--sg-bg, #fff) 82%, transparent); color: var(--sg-fg, #0f172a);
159
+ border: 1px solid var(--sg-border, #e2e8f0); cursor: pointer; font-size: 12px; line-height: 1;
160
+ box-shadow: 0 4px 12px -4px rgba(15, 23, 42, 0.3);
161
+ }
162
+ .sv-carousel__playpause:hover { background: var(--sg-bg, #fff); }
134
163
  </style>
@@ -84,7 +84,6 @@
84
84
  const layer = createDismissableLayer({
85
85
  element: () => [triggerEl, panelEl],
86
86
  onDismiss: () => col.popover.close(),
87
- closeOnEscape: false,
88
87
  })
89
88
  layer.activate()
90
89
  return () => {
@@ -18,6 +18,7 @@
18
18
  import type { Snippet } from 'svelte'
19
19
  import { portalToBody, popIn } from './popover'
20
20
  import { createDismissableLayer } from './a11y/dismissable'
21
+ import { createFocusTrap } from './a11y/focus-trap'
21
22
  import SvMenuList, { type MenuItem } from './SvMenuList.svelte'
22
23
 
23
24
  type Props = {
@@ -33,6 +34,7 @@
33
34
  let open = $state(false)
34
35
  let pos = $state({ x: 0, y: 0 })
35
36
  let panelEl = $state<HTMLDivElement | null>(null)
37
+ let zoneEl = $state<HTMLDivElement | null>(null)
36
38
 
37
39
  const MENU_W = 200
38
40
  function openAt(clientX: number, clientY: number) {
@@ -50,9 +52,24 @@
50
52
  openAt(e.clientX, e.clientY)
51
53
  }
52
54
 
55
+ // Keyboard equivalent of a right-click: the ContextMenu key, or Shift+F10.
56
+ // Anchors at the focused zone's own position instead of the (nonexistent)
57
+ // pointer coordinates.
58
+ function onZoneKeydown(e: KeyboardEvent) {
59
+ if (disabled) return
60
+ const isMenuKey = e.key === 'ContextMenu' || (e.key === 'F10' && e.shiftKey)
61
+ if (!isMenuKey) return
62
+ e.preventDefault()
63
+ const rect = zoneEl?.getBoundingClientRect()
64
+ openAt(rect ? rect.left : 0, rect ? rect.bottom : 0)
65
+ }
66
+
53
67
  $effect(() => {
54
- if (!open) return
55
- queueMicrotask(() => panelEl?.querySelector<HTMLElement>('[role="menuitem"]:not([disabled])')?.focus())
68
+ if (!open || !panelEl) return
69
+ const trap = createFocusTrap(panelEl, {
70
+ initialFocus: () => panelEl?.querySelector<HTMLElement>('[role="menuitem"]:not([disabled])') ?? null,
71
+ })
72
+ trap.activate()
56
73
  const layer = createDismissableLayer({
57
74
  element: () => panelEl,
58
75
  onDismiss: () => (open = false),
@@ -61,12 +78,12 @@
61
78
  // Reposition-close on scroll (a moved anchor would leave the menu stranded).
62
79
  const onScroll = () => (open = false)
63
80
  window.addEventListener('scroll', onScroll, true)
64
- return () => { layer.release(); window.removeEventListener('scroll', onScroll, true) }
81
+ return () => { layer.release(); trap.release(); window.removeEventListener('scroll', onScroll, true) }
65
82
  })
66
83
  </script>
67
84
 
68
- <!-- svelte-ignore a11y_no_static_element_interactions -->
69
- <div class="sv-ctx__zone" oncontextmenu={onContextMenu}>
85
+ <!-- svelte-ignore a11y_no_static_element_interactions, a11y_no_noninteractive_tabindex -->
86
+ <div class="sv-ctx__zone" bind:this={zoneEl} tabindex={disabled ? -1 : 0} oncontextmenu={onContextMenu} onkeydown={onZoneKeydown}>
70
87
  {@render children?.()}
71
88
  </div>
72
89
 
@@ -86,7 +103,10 @@
86
103
  {/if}
87
104
 
88
105
  <style>
89
- .sv-ctx__zone { display: contents; }
106
+ /* display:contents generates no box and per spec cannot receive focus - it
107
+ must be a real box for the keyboard-open path (tabindex) to work at all. */
108
+ .sv-ctx__zone { display: block; }
109
+ .sv-ctx__zone:focus-visible { outline: 2px solid var(--sg-accent, #6366f1); outline-offset: 1px; }
90
110
  :global(.sv-ctx) {
91
111
  z-index: 2147483646; min-width: 200px;
92
112
  background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
@@ -171,12 +171,14 @@
171
171
  box-shadow: 0 24px 64px -16px rgba(15, 23, 42, 0.5);
172
172
  overflow: hidden;
173
173
  }
174
- /* Left / right: full height, fixed width, slide along X. */
174
+ /* Left / right: full height, fixed width, slide along X. `side` is a stated
175
+ physical edge (like SvToaster's position prop), not a text-flow-relative
176
+ "start/end" - so it stays physical left/right regardless of page dir. */
175
177
  :global(.sv-drawer--right), :global(.sv-drawer--left) {
176
178
  top: 0; bottom: 0; width: var(--sv-drawer-size); max-width: 100vw;
177
179
  }
178
- :global(.sv-drawer--right) { inset-inline-end: 0; border-inline-start: 1px solid var(--sg-border, #e2e8f0); animation: sv-drawer-in-right 0.22s cubic-bezier(0.16, 1, 0.3, 1) both; }
179
- :global(.sv-drawer--left) { inset-inline-start: 0; border-inline-end: 1px solid var(--sg-border, #e2e8f0); animation: sv-drawer-in-left 0.22s cubic-bezier(0.16, 1, 0.3, 1) both; }
180
+ :global(.sv-drawer--right) { right: 0; border-left: 1px solid var(--sg-border, #e2e8f0); animation: sv-drawer-in-right 0.22s cubic-bezier(0.16, 1, 0.3, 1) both; }
181
+ :global(.sv-drawer--left) { left: 0; border-right: 1px solid var(--sg-border, #e2e8f0); animation: sv-drawer-in-left 0.22s cubic-bezier(0.16, 1, 0.3, 1) both; }
180
182
  /* Top / bottom: full width, fixed height, slide along Y. */
181
183
  :global(.sv-drawer--top), :global(.sv-drawer--bottom) {
182
184
  left: 0; right: 0; height: var(--sv-drawer-size); max-height: 100vh;