@svgrid/grid 2.1.21 → 2.1.23

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 (219) hide show
  1. package/README.md +9 -2
  2. package/dist/DockNodeView.svelte +306 -0
  3. package/dist/DockNodeView.svelte.d.ts +7 -0
  4. package/dist/DockPopoutHost.svelte +17 -0
  5. package/dist/DockPopoutHost.svelte.d.ts +14 -0
  6. package/dist/GridMenus.svelte +83 -3
  7. package/dist/SvAutoComplete.svelte +5 -3
  8. package/dist/SvCarousel.svelte +20 -42
  9. package/dist/SvColorInput.svelte +4 -3
  10. package/dist/SvColorInput.svelte.d.ts +1 -1
  11. package/dist/SvComboBox.svelte +7 -3
  12. package/dist/SvCommand.svelte +28 -85
  13. package/dist/SvCountryInput.svelte +1 -1
  14. package/dist/SvDateRangeInput.svelte +1 -1
  15. package/dist/SvDateTimePicker.svelte +9 -3
  16. package/dist/SvDateTimePicker.svelte.d.ts +3 -0
  17. package/dist/SvDockLayout.svelte +244 -0
  18. package/dist/SvDockLayout.svelte.d.ts +77 -0
  19. package/dist/SvDockManager.svelte +922 -0
  20. package/dist/SvDockManager.svelte.d.ts +106 -0
  21. package/dist/SvDrawer.svelte +69 -47
  22. package/dist/SvDrawer.svelte.d.ts +4 -0
  23. package/dist/SvDropDownList.svelte +85 -13
  24. package/dist/SvDropDownList.svelte.d.ts +3 -0
  25. package/dist/SvField.svelte +21 -4
  26. package/dist/SvField.svelte.d.ts +1 -0
  27. package/dist/SvForm.svelte +201 -91
  28. package/dist/SvForm.svelte.d.ts +15 -22
  29. package/dist/SvGrid.controller.svelte.d.ts +32 -1
  30. package/dist/SvGrid.controller.svelte.js +156 -27
  31. package/dist/SvGrid.css +193 -5
  32. package/dist/SvGrid.svelte +357 -12
  33. package/dist/SvGrid.types.d.ts +452 -1
  34. package/dist/SvGridDropdown.svelte +8 -1
  35. package/dist/SvGridSelect.svelte +1 -0
  36. package/dist/SvListBox.svelte +36 -14
  37. package/dist/SvMaskedInput.svelte +4 -3
  38. package/dist/SvMaskedInput.svelte.d.ts +1 -1
  39. package/dist/SvMenu.svelte +1 -1
  40. package/dist/SvMenuList.svelte +23 -71
  41. package/dist/SvMenuList.svelte.d.ts +2 -13
  42. package/dist/SvModal.svelte +12 -29
  43. package/dist/SvMultiSelect.svelte +1 -0
  44. package/dist/SvNumberInput.svelte +4 -3
  45. package/dist/SvNumberInput.svelte.d.ts +1 -1
  46. package/dist/SvPagination.svelte +18 -18
  47. package/dist/SvPasswordInput.svelte +3 -2
  48. package/dist/SvPasswordInput.svelte.d.ts +1 -1
  49. package/dist/SvPhoneInput.svelte +3 -2
  50. package/dist/SvPhoneInput.svelte.d.ts +1 -1
  51. package/dist/SvPopover.svelte +1 -1
  52. package/dist/SvStepper.svelte +6 -9
  53. package/dist/SvTagsInput.svelte +3 -2
  54. package/dist/SvTagsInput.svelte.d.ts +1 -1
  55. package/dist/SvTooltip.svelte +12 -23
  56. package/dist/SvTour.svelte +32 -4
  57. package/dist/SvTreeSelect.svelte +1 -0
  58. package/dist/build-api.js +76 -54
  59. package/dist/cdn/svgrid.js +10907 -6444
  60. package/dist/cdn/svgrid.svelte-external.js +9622 -5475
  61. package/dist/columns.js +6 -0
  62. package/dist/conditional-formatting.d.ts +67 -3
  63. package/dist/conditional-formatting.js +124 -17
  64. package/dist/createCarousel.svelte.d.ts +72 -0
  65. package/dist/createCarousel.svelte.js +80 -0
  66. package/dist/createCommand.svelte.d.ts +62 -0
  67. package/dist/createCommand.svelte.js +109 -0
  68. package/dist/createForm.svelte.d.ts +62 -0
  69. package/dist/createForm.svelte.js +374 -0
  70. package/dist/createMenu.svelte.d.ts +88 -0
  71. package/dist/createMenu.svelte.js +153 -0
  72. package/dist/createOverlay.svelte.d.ts +38 -0
  73. package/dist/createOverlay.svelte.js +68 -0
  74. package/dist/createPagination.d.ts +74 -0
  75. package/dist/createPagination.js +71 -0
  76. package/dist/createPopoverSelect.svelte.js +6 -12
  77. package/dist/createStepper.d.ts +44 -0
  78. package/dist/createStepper.js +26 -0
  79. package/dist/createTooltip.svelte.d.ts +32 -0
  80. package/dist/createTooltip.svelte.js +61 -0
  81. package/dist/datetime/timezone.d.ts +36 -0
  82. package/dist/datetime/timezone.js +110 -0
  83. package/dist/dock-context.d.ts +52 -0
  84. package/dist/dock-context.js +1 -0
  85. package/dist/dock-manager-model.d.ts +136 -0
  86. package/dist/dock-manager-model.js +514 -0
  87. package/dist/dock-model.d.ts +105 -0
  88. package/dist/dock-model.js +288 -0
  89. package/dist/dock-popout.d.ts +30 -0
  90. package/dist/dock-popout.js +81 -0
  91. package/dist/editor-contract.d.ts +2 -0
  92. package/dist/filter-operators.d.ts +6 -0
  93. package/dist/filter-operators.js +35 -2
  94. package/dist/filtering/excel-filters.d.ts +18 -1
  95. package/dist/filtering/excel-filters.js +56 -0
  96. package/dist/form-field.d.ts +73 -0
  97. package/dist/form-field.js +15 -0
  98. package/dist/index.d.ts +25 -5
  99. package/dist/index.js +40 -4
  100. package/dist/list-nav.d.ts +18 -0
  101. package/dist/list-nav.js +30 -0
  102. package/dist/list-option.d.ts +2 -0
  103. package/dist/menu-item.d.ts +20 -0
  104. package/dist/menu-item.js +1 -0
  105. package/dist/menus.d.ts +5 -0
  106. package/dist/menus.js +68 -16
  107. package/dist/panel-resize.d.ts +25 -0
  108. package/dist/panel-resize.js +40 -0
  109. package/dist/popover.d.ts +29 -3
  110. package/dist/popover.js +22 -6
  111. package/dist/recurrence.d.ts +33 -7
  112. package/dist/recurrence.js +151 -13
  113. package/dist/row-drag.d.ts +8 -0
  114. package/dist/row-drag.js +11 -0
  115. package/dist/scheduler-ical.d.ts +30 -0
  116. package/dist/scheduler-ical.js +180 -0
  117. package/dist/scheduler-model.d.ts +269 -0
  118. package/dist/scheduler-model.js +602 -0
  119. package/dist/scheduler-view.svelte.d.ts +22 -0
  120. package/dist/scheduler-view.svelte.js +13 -0
  121. package/dist/spreadsheet.js +20 -12
  122. package/dist/ui-app.types.d.ts +3 -0
  123. package/package.json +5 -1
  124. package/src/DockNodeView.svelte +306 -0
  125. package/src/DockPopoutHost.svelte +17 -0
  126. package/src/GridMenus.svelte +83 -3
  127. package/src/SvAutoComplete.svelte +5 -3
  128. package/src/SvCarousel.svelte +20 -42
  129. package/src/SvCarousel.test.ts +68 -0
  130. package/src/SvColorInput.svelte +4 -3
  131. package/src/SvComboBox.svelte +7 -3
  132. package/src/SvCommand.svelte +28 -85
  133. package/src/SvCommand.test.ts +78 -0
  134. package/src/SvCountryInput.svelte +1 -1
  135. package/src/SvDateRangeInput.svelte +1 -1
  136. package/src/SvDateTimePicker.svelte +9 -3
  137. package/src/SvDockLayout.svelte +244 -0
  138. package/src/SvDockManager.svelte +922 -0
  139. package/src/SvDrawer.svelte +69 -47
  140. package/src/SvDropDownList.svelte +85 -13
  141. package/src/SvField.svelte +21 -4
  142. package/src/SvForm.svelte +201 -91
  143. package/src/SvForm.test.ts +299 -0
  144. package/src/SvGrid.controller.svelte.ts +152 -28
  145. package/src/SvGrid.css +193 -5
  146. package/src/SvGrid.svelte +357 -12
  147. package/src/SvGrid.types.ts +463 -1
  148. package/src/SvGridDropdown.svelte +8 -1
  149. package/src/SvGridSelect.svelte +1 -0
  150. package/src/SvListBox.svelte +36 -14
  151. package/src/SvMaskedInput.svelte +4 -3
  152. package/src/SvMenu.svelte +1 -1
  153. package/src/SvMenu.test.ts +24 -0
  154. package/src/SvMenuList.svelte +23 -71
  155. package/src/SvModal.svelte +12 -29
  156. package/src/SvModal.test.ts +82 -0
  157. package/src/SvMultiSelect.svelte +1 -0
  158. package/src/SvNumberInput.svelte +4 -3
  159. package/src/SvPagination.svelte +18 -18
  160. package/src/SvPasswordInput.svelte +3 -2
  161. package/src/SvPhoneInput.svelte +3 -2
  162. package/src/SvPopover.svelte +1 -1
  163. package/src/SvStepper.svelte +6 -9
  164. package/src/SvTagsInput.svelte +3 -2
  165. package/src/SvTooltip.svelte +12 -23
  166. package/src/SvTooltip.test.ts +73 -0
  167. package/src/SvTour.svelte +32 -4
  168. package/src/SvTreeSelect.svelte +1 -0
  169. package/src/build-api.ts +74 -51
  170. package/src/columns.ts +5 -0
  171. package/src/conditional-formatting.test.ts +104 -0
  172. package/src/conditional-formatting.ts +176 -19
  173. package/src/createCarousel.svelte.ts +104 -0
  174. package/src/createCommand.svelte.ts +125 -0
  175. package/src/createForm.svelte.ts +333 -0
  176. package/src/createMenu.svelte.ts +202 -0
  177. package/src/createOverlay.svelte.ts +101 -0
  178. package/src/createPagination.test.ts +65 -0
  179. package/src/createPagination.ts +84 -0
  180. package/src/createPopoverSelect.svelte.ts +5 -9
  181. package/src/createStepper.test.ts +62 -0
  182. package/src/createStepper.ts +61 -0
  183. package/src/createTooltip.svelte.ts +86 -0
  184. package/src/datetime/timezone.test.ts +69 -0
  185. package/src/datetime/timezone.ts +134 -0
  186. package/src/dock-context.ts +47 -0
  187. package/src/dock-manager-model.test.ts +275 -0
  188. package/src/dock-manager-model.ts +593 -0
  189. package/src/dock-model.test.ts +163 -0
  190. package/src/dock-model.ts +368 -0
  191. package/src/dock-popout.ts +94 -0
  192. package/src/editor-contract.ts +2 -0
  193. package/src/filter-operators.test.ts +59 -0
  194. package/src/filter-operators.ts +35 -2
  195. package/src/filtering/excel-filters.test.ts +86 -1
  196. package/src/filtering/excel-filters.ts +65 -0
  197. package/src/form-field.ts +87 -0
  198. package/src/index.ts +158 -4
  199. package/src/list-nav.test.ts +49 -0
  200. package/src/list-nav.ts +29 -0
  201. package/src/list-option.ts +2 -0
  202. package/src/menu-item.ts +21 -0
  203. package/src/menus.test.ts +48 -0
  204. package/src/menus.ts +73 -16
  205. package/src/panel-resize.ts +52 -0
  206. package/src/popover.bounds.test.ts +93 -0
  207. package/src/popover.ts +51 -7
  208. package/src/recurrence.test.ts +88 -1
  209. package/src/recurrence.ts +166 -15
  210. package/src/row-drag.ts +10 -0
  211. package/src/scheduler-ical.test.ts +68 -0
  212. package/src/scheduler-ical.ts +180 -0
  213. package/src/scheduler-model.test.ts +546 -0
  214. package/src/scheduler-model.ts +853 -0
  215. package/src/scheduler-view.svelte.ts +33 -0
  216. package/src/spreadsheet.ts +20 -12
  217. package/src/svgrid.effect-driven-filter.svelte.test.ts +116 -0
  218. package/src/ui-app.types.ts +3 -0
  219. package/src/virtual.test.ts +6 -4
@@ -10,15 +10,59 @@
10
10
  * component just paints the result and the logic is unit-testable.
11
11
  */
12
12
 
13
+ /** A single gradient stop along the normalized 0..1 domain. */
14
+ export type ColorScaleStop = { offset: number; color: string }
15
+
16
+ /**
17
+ * How `minValue`/`maxValue` (and the derived column extremes) are read:
18
+ * - `absolute` (default): literal data values.
19
+ * - `percent`: 0..100 positions along the column's own min..max span, so you
20
+ * can say "tint the top 20%" without knowing the numbers up front.
21
+ */
22
+ export type ScaleBounds = 'absolute' | 'percent'
23
+
13
24
  export type ColorScaleFormat = {
14
25
  type: 'colorScale'
15
26
  /** 2-stop (min/max) or 3-stop (min/mid/max) gradient. Hex colors. */
16
- min: string
27
+ min?: string
17
28
  mid?: string
18
- max: string
29
+ max?: string
30
+ /**
31
+ * N-stop gradient along the normalized domain (offsets 0..1). When present
32
+ * this overrides `min`/`mid`/`max`, enabling banded / traffic-light scales.
33
+ */
34
+ stops?: ReadonlyArray<ColorScaleStop>
35
+ /**
36
+ * `hue` (default): interpolate between stop colors into an opaque fill.
37
+ * `alpha`: keep a single `base` color and interpolate its *opacity*, so the
38
+ * tint composites over zebra striping, selection, and pinned backgrounds
39
+ * instead of painting over them - Adaptable's "live heat map" look.
40
+ */
41
+ mode?: 'hue' | 'alpha'
42
+ /** Base color for `alpha` mode. Default `#2563eb`. */
43
+ base?: string
44
+ /** [min, max] opacity for `alpha` mode. Default [0.05, 0.85]. */
45
+ alphaBounds?: readonly [number, number]
19
46
  /** Fix the scale; otherwise derived from the column's data. */
20
47
  minValue?: number
21
48
  maxValue?: number
49
+ /** Interpret `minValue`/`maxValue` as absolute values or 0..100 percents. */
50
+ bounds?: ScaleBounds
51
+ /**
52
+ * Diverging scale pinned at 0: negatives and positives shade outward from a
53
+ * neutral midpoint, symmetric around zero. Ideal for P&L / price deltas.
54
+ */
55
+ zeroCentred?: boolean
56
+ /** Flip the ramp so the lowest values attract the most attention. */
57
+ reverse?: boolean
58
+ /**
59
+ * Tint by this cell's value as a proportion of another column's value on the
60
+ * SAME row (a field key on the row object), instead of the column extremes.
61
+ * Column comparison - e.g. filled vs target, open vs total.
62
+ */
63
+ compareColumn?: string
64
+ /** Attach a tooltip showing the raw value or its % position on the ramp. */
65
+ tooltip?: 'value' | 'percent'
22
66
  }
23
67
 
24
68
  export type DataBarFormat = {
@@ -27,6 +71,15 @@ export type DataBarFormat = {
27
71
  negativeColor?: string
28
72
  minValue?: number
29
73
  maxValue?: number
74
+ /** Interpret `minValue`/`maxValue` as absolute values or 0..100 percents. */
75
+ bounds?: ScaleBounds
76
+ /**
77
+ * Size the bar by this cell's value as a proportion of another column's
78
+ * value on the same row (a field key), instead of the column extremes.
79
+ */
80
+ compareColumn?: string
81
+ /** Fill the bar with a left-to-right gradient rather than a flat color. */
82
+ gradient?: boolean
30
83
  /** Show the cell's text on top of the bar. Default true. */
31
84
  showValue?: boolean
32
85
  }
@@ -66,9 +119,11 @@ export type ResolvedCellFormat = {
66
119
  background?: string
67
120
  color?: string
68
121
  fontWeight?: string | number
69
- dataBar?: { percent: number; color: string; fromRight: boolean }
122
+ dataBar?: { percent: number; color: string; fromRight: boolean; gradient?: boolean }
70
123
  icon?: string
71
124
  iconOnly?: boolean
125
+ /** Tooltip text (value / percentile), when a format requests one. */
126
+ title?: string
72
127
  }
73
128
 
74
129
  export type ColumnStat = { min: number; max: number }
@@ -158,13 +213,85 @@ export function lerpColor(a: string, b: string, t: number): string {
158
213
  ])
159
214
  }
160
215
 
216
+ /** Build an `rgba()` string from a hex color + opacity. Falls back to the hex. */
217
+ export function rgba(hex: string, alpha: number): string {
218
+ const rgb = parseHex(hex)
219
+ if (!rgb) return hex
220
+ return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${clamp01(alpha).toFixed(3)})`
221
+ }
222
+
223
+ /** Color of an N-stop ramp at normalized position `t`. Stops sorted by offset. */
224
+ function stopsColorAt(stops: ReadonlyArray<ColorScaleStop>, t: number): string {
225
+ const sorted = [...stops].sort((a, b) => a.offset - b.offset)
226
+ if (sorted.length === 0) return '#000000'
227
+ if (t <= sorted[0]!.offset) return sorted[0]!.color
228
+ const last = sorted[sorted.length - 1]!
229
+ if (t >= last.offset) return last.color
230
+ for (let i = 0; i < sorted.length - 1; i += 1) {
231
+ const a = sorted[i]!
232
+ const b = sorted[i + 1]!
233
+ if (t >= a.offset && t <= b.offset) {
234
+ const span = b.offset - a.offset || 1
235
+ return lerpColor(a.color, b.color, (t - a.offset) / span)
236
+ }
237
+ }
238
+ return last.color
239
+ }
240
+
241
+ /** The interpolated color for a color-scale at normalized position `t`. */
161
242
  function colorScaleAt(fmt: ColorScaleFormat, t: number): string {
243
+ if (fmt.stops && fmt.stops.length >= 2) return stopsColorAt(fmt.stops, t)
244
+ const min = fmt.min ?? '#ffffff'
245
+ const max = fmt.max ?? '#000000'
162
246
  if (fmt.mid) {
163
247
  return t <= 0.5
164
- ? lerpColor(fmt.min, fmt.mid, t / 0.5)
165
- : lerpColor(fmt.mid, fmt.max, (t - 0.5) / 0.5)
248
+ ? lerpColor(min, fmt.mid, t / 0.5)
249
+ : lerpColor(fmt.mid, max, (t - 0.5) / 0.5)
166
250
  }
167
- return lerpColor(fmt.min, fmt.max, t)
251
+ return lerpColor(min, max, t)
252
+ }
253
+
254
+ /**
255
+ * Normalized position (0..1) of `n` within a format's domain, honoring
256
+ * absolute/percent bounds, zero-centred diverging scales, cross-column
257
+ * comparison, and reverse. Returns null when the domain can't be resolved
258
+ * (e.g. compareColumn missing / zero on the row).
259
+ */
260
+ function domainT(
261
+ fmt: ColorScaleFormat | DataBarFormat,
262
+ n: number,
263
+ row: unknown,
264
+ stat: ColumnStat | null,
265
+ ): number | null {
266
+ let t: number
267
+
268
+ if (fmt.compareColumn) {
269
+ const other = Number((row as Record<string, unknown> | null)?.[fmt.compareColumn])
270
+ if (!Number.isFinite(other) || other === 0) return null
271
+ t = clamp01(n / other)
272
+ } else if (fmt.type === 'colorScale' && fmt.zeroCentred) {
273
+ const dMin = fmt.minValue ?? stat?.min ?? n
274
+ const dMax = fmt.maxValue ?? stat?.max ?? n
275
+ const bound = Math.max(Math.abs(dMin), Math.abs(dMax)) || 1
276
+ t = clamp01(0.5 + n / bound / 2)
277
+ } else {
278
+ const dMin = stat?.min ?? n
279
+ const dMax = stat?.max ?? n
280
+ let lo: number
281
+ let hi: number
282
+ if (fmt.bounds === 'percent') {
283
+ const span = dMax - dMin
284
+ lo = dMin + ((fmt.minValue ?? 0) / 100) * span
285
+ hi = dMin + ((fmt.maxValue ?? 100) / 100) * span
286
+ } else {
287
+ lo = fmt.minValue ?? dMin
288
+ hi = fmt.maxValue ?? dMax
289
+ }
290
+ t = hi === lo ? 0.5 : clamp01((n - lo) / (hi - lo))
291
+ }
292
+
293
+ if (fmt.type === 'colorScale' && fmt.reverse) t = 1 - t
294
+ return t
168
295
  }
169
296
 
170
297
  /**
@@ -203,23 +330,42 @@ export function resolveCellFormat<TData = unknown>(
203
330
  if (value == null || value === '' || !Number.isFinite(n)) continue
204
331
 
205
332
  if (fmt.type === 'colorScale') {
206
- const lo = fmt.minValue ?? stat?.min ?? n
207
- const hi = fmt.maxValue ?? stat?.max ?? n
208
- const t = hi === lo ? 0.5 : clamp01((n - lo) / (hi - lo))
209
- out.background = colorScaleAt(fmt, t)
210
- // The scale fills the whole cell, so keep the text legible against it.
211
- const c = contrastText(out.background)
212
- if (c) out.color = c
333
+ const t = domainT(fmt, n, row, stat)
334
+ if (t == null) continue
335
+ if (fmt.mode === 'alpha') {
336
+ const [alo, ahi] = fmt.alphaBounds ?? [0.05, 0.85]
337
+ out.background = rgba(fmt.base ?? '#2563eb', alo + (ahi - alo) * t)
338
+ // Translucent tint composites over the row bg; leave text color alone.
339
+ } else {
340
+ out.background = colorScaleAt(fmt, t)
341
+ // The scale fills the whole cell, so keep the text legible against it.
342
+ const c = contrastText(out.background)
343
+ if (c) out.color = c
344
+ }
345
+ if (fmt.tooltip === 'value') out.title = String(value)
346
+ else if (fmt.tooltip === 'percent') out.title = `${Math.round(t * 100)}%`
213
347
  } else if (fmt.type === 'dataBar') {
214
- const lo = fmt.minValue ?? Math.min(0, stat?.min ?? 0)
215
- const hi = fmt.maxValue ?? stat?.max ?? n
216
- const span = hi - lo || 1
217
- const percent = clamp01((n - lo) / span) * 100
348
+ let percent: number
349
+ if (fmt.compareColumn) {
350
+ const t = domainT(fmt, n, row, stat)
351
+ if (t == null) continue
352
+ percent = t * 100
353
+ } else if (fmt.bounds === 'percent') {
354
+ const t = domainT(fmt, n, row, stat)
355
+ if (t == null) continue
356
+ percent = t * 100
357
+ } else {
358
+ const lo = fmt.minValue ?? Math.min(0, stat?.min ?? 0)
359
+ const hi = fmt.maxValue ?? stat?.max ?? n
360
+ const span = hi - lo || 1
361
+ percent = clamp01((n - lo) / span) * 100
362
+ }
218
363
  const negative = n < 0
219
364
  out.dataBar = {
220
365
  percent,
221
366
  color: negative ? (fmt.negativeColor ?? '#ef4444') : fmt.color,
222
367
  fromRight: false,
368
+ gradient: fmt.gradient,
223
369
  }
224
370
  if (fmt.showValue === false) out.color = 'transparent'
225
371
  } else if (fmt.type === 'iconSet') {
@@ -231,9 +377,20 @@ export function resolveCellFormat<TData = unknown>(
231
377
  return out
232
378
  }
233
379
 
234
- /** Column ids that need a numeric min/max precomputed (colorScale/dataBar). */
380
+ /**
381
+ * Whether any format needs a numeric min/max precomputed. Color-scale and
382
+ * data-bar formats do, EXCEPT when they derive their range from another column
383
+ * on the same row (`compareColumn`) - those read the row, not column stats.
384
+ */
385
+ export function formatNeedsStats(f: ConditionalFormat<any>): boolean {
386
+ return (
387
+ (f.type === 'colorScale' || f.type === 'dataBar') &&
388
+ !('compareColumn' in f && f.compareColumn)
389
+ )
390
+ }
391
+
235
392
  export function formatsNeedingStats(
236
393
  formats: ReadonlyArray<ConditionalFormat<any>>,
237
394
  ): boolean {
238
- return formats.some((f) => f.type === 'colorScale' || f.type === 'dataBar')
395
+ return formats.some(formatNeedsStats)
239
396
  }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * createCarousel - the HEADLESS core behind <SvCarousel>: slide navigation
3
+ * (with optional looping), autoplay that pauses on hover/focus and via a
4
+ * persistent stop toggle (WCAG 2.2.2), and rtl-aware arrow wiring - as
5
+ * prop-getters you spread onto your own markup. The component keeps only the
6
+ * visual track + swipe. Runes-based, like `createListbox`.
7
+ */
8
+ import { type EditorDir } from './editor-contract'
9
+
10
+ export type CarouselConfig = {
11
+ count: () => number
12
+ /** Active slide index (the component owns the state; core calls onChange). */
13
+ current: () => number
14
+ onChange?: (index: number) => void
15
+ loop?: () => boolean
16
+ /** Autoplay interval in ms; 0 = off. */
17
+ autoplay?: () => number
18
+ dir?: () => EditorDir | undefined
19
+ }
20
+
21
+ export function createCarousel(config: CarouselConfig) {
22
+ const count = () => config.count()
23
+ const current = () => config.current()
24
+ const loop = () => config.loop?.() ?? true
25
+ const rtl = () => config.dir?.() === 'rtl'
26
+
27
+ let paused = $state(false) // hover/focus
28
+ let userPaused = $state(false) // explicit stop toggle
29
+
30
+ function go(i: number) {
31
+ const n = count()
32
+ if (n === 0) return
33
+ const target = loop() ? ((i % n) + n) % n : Math.max(0, Math.min(i, n - 1))
34
+ config.onChange?.(target)
35
+ }
36
+ const next = () => go(current() + 1)
37
+ const prev = () => go(current() - 1)
38
+ const canGoPrev = () => loop() || current() > 0
39
+ const canGoNext = () => loop() || current() < count() - 1
40
+
41
+ // Autoplay, paused while hovered/focused or explicitly stopped (WCAG 2.2.2).
42
+ $effect(() => {
43
+ const ms = config.autoplay?.() ?? 0
44
+ if (!ms || paused || userPaused || count() < 2) return
45
+ const t = setInterval(() => next(), ms)
46
+ return () => clearInterval(t)
47
+ })
48
+
49
+ return {
50
+ get paused() { return paused },
51
+ get userPaused() { return userPaused },
52
+ rtl,
53
+ isActive: (i: number) => i === current(),
54
+ go,
55
+ next,
56
+ prev,
57
+ canGoPrev,
58
+ canGoNext,
59
+ /** Spread onto the carousel container. Pauses autoplay on hover/focus. */
60
+ rootProps: (ariaLabel = 'Carousel') => ({
61
+ role: 'group' as const,
62
+ 'aria-roledescription': 'carousel',
63
+ 'aria-label': ariaLabel,
64
+ onpointerenter: () => { paused = true },
65
+ onpointerleave: () => { paused = false },
66
+ onfocusin: () => { paused = true },
67
+ onfocusout: () => { paused = false },
68
+ }),
69
+ /** Spread onto each slide element. */
70
+ slideProps: (i: number) => ({
71
+ role: 'group' as const,
72
+ 'aria-roledescription': 'slide',
73
+ 'aria-label': `${i + 1} of ${count()}`,
74
+ 'aria-hidden': i !== current(),
75
+ inert: i !== current(),
76
+ }),
77
+ /** The visually-leading (inline-start) arrow: logical prev in ltr, next in rtl. */
78
+ prevArrowProps: () => ({
79
+ 'aria-label': rtl() ? 'Next slide' : 'Previous slide',
80
+ onclick: rtl() ? next : prev,
81
+ disabled: rtl() ? !canGoNext() : !canGoPrev(),
82
+ }),
83
+ /** The visually-trailing (inline-end) arrow: logical next in ltr, prev in rtl. */
84
+ nextArrowProps: () => ({
85
+ 'aria-label': rtl() ? 'Previous slide' : 'Next slide',
86
+ onclick: rtl() ? prev : next,
87
+ disabled: rtl() ? !canGoPrev() : !canGoNext(),
88
+ }),
89
+ /** Spread onto a dot indicator. */
90
+ dotProps: (i: number) => ({
91
+ 'aria-label': `Go to slide ${i + 1}`,
92
+ 'aria-current': (i === current() ? 'true' : undefined) as 'true' | undefined,
93
+ onclick: () => go(i),
94
+ }),
95
+ /** Spread onto the persistent play/pause toggle (render only when autoplay > 0). */
96
+ playPauseProps: () => ({
97
+ 'aria-label': userPaused ? 'Play autoplay' : 'Pause autoplay',
98
+ 'aria-pressed': userPaused,
99
+ onclick: () => { userPaused = !userPaused },
100
+ }),
101
+ }
102
+ }
103
+
104
+ export type Carousel = ReturnType<typeof createCarousel>
@@ -0,0 +1,125 @@
1
+ /**
2
+ * createCommand - the HEADLESS core behind <SvCommand> (the ⌘K palette): the
3
+ * fuzzy-filtered result list, roving active index, run/close, the global toggle
4
+ * hotkey, and the WAI-ARIA combobox wiring (activedescendant) - as prop-getters
5
+ * you spread onto your own input/list/option markup. The overlay lifecycle
6
+ * (focus-trap + scroll-lock + dismissal) is a separate concern; pair this with
7
+ * `createOverlay`. Runes-based, like `createListbox`.
8
+ */
9
+ import type { CommandItem } from './ui-app.types'
10
+ import { fuzzyScore } from './fuzzy'
11
+ import { nextEditorId } from './editor-contract'
12
+
13
+ export type CommandConfig = {
14
+ commands: () => ReadonlyArray<CommandItem>
15
+ /** Whether the palette is open (used to reset query/active on open). */
16
+ open: () => boolean
17
+ onRun?: (cmd: CommandItem) => void
18
+ /** Close the palette (a command ran, or requested by the input). */
19
+ onClose?: () => void
20
+ /** Toggle open/closed - fired by the global hotkey. */
21
+ onToggleOpen?: () => void
22
+ /** Global toggle hotkey (Cmd/Ctrl + key). `false` disables it. Default 'mod+k'. */
23
+ hotkey?: () => 'mod+k' | 'mod+p' | false
24
+ /** DOM hook to scroll the newly-active option into view. */
25
+ scrollActiveIntoView?: (index: number) => void
26
+ }
27
+
28
+ export function createCommand(config: CommandConfig) {
29
+ const id = nextEditorId('sv-cmd')
30
+ const listId = `${id}-list`
31
+ const optionId = (i: number) => `${id}-opt-${i}`
32
+
33
+ let query = $state('')
34
+ let active = $state(0)
35
+
36
+ const results = $derived.by(() => {
37
+ const q = query.trim()
38
+ const list = config.commands().filter((c) => !c.disabled)
39
+ if (!q) return list
40
+ return list
41
+ .map((c) => ({ c, s: fuzzyScore(`${c.label} ${c.keywords ?? ''} ${c.group ?? ''}`, q) }))
42
+ .filter((x) => x.s !== null)
43
+ .sort((a, b) => (b.s as number) - (a.s as number))
44
+ .map((x) => x.c)
45
+ })
46
+
47
+ function run(cmd: CommandItem | undefined) {
48
+ if (!cmd || cmd.disabled) return
49
+ config.onClose?.()
50
+ config.onRun?.(cmd)
51
+ cmd.onRun?.()
52
+ }
53
+ function move(delta: number) {
54
+ const n = results.length
55
+ if (!n) return
56
+ active = (active + delta + n) % n
57
+ config.scrollActiveIntoView?.(active)
58
+ }
59
+ function onInputKeydown(e: KeyboardEvent) {
60
+ if (e.key === 'ArrowDown') { e.preventDefault(); move(1) }
61
+ else if (e.key === 'ArrowUp') { e.preventDefault(); move(-1) }
62
+ else if (e.key === 'Enter') { e.preventDefault(); run(results[active]) }
63
+ // Escape + outside-click are owned by the overlay/dismissable layer.
64
+ }
65
+
66
+ // Reset the active row as the query changes.
67
+ $effect(() => { void query; active = 0 })
68
+ // Reset query + active each time the palette opens.
69
+ $effect(() => { if (config.open()) { query = ''; active = 0 } })
70
+ // Global toggle hotkey.
71
+ $effect(() => {
72
+ const hk = config.hotkey?.() ?? 'mod+k'
73
+ if (!hk) return
74
+ const key = hk === 'mod+p' ? 'p' : 'k'
75
+ const onKey = (e: KeyboardEvent) => {
76
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === key) {
77
+ e.preventDefault()
78
+ config.onToggleOpen?.()
79
+ }
80
+ }
81
+ window.addEventListener('keydown', onKey)
82
+ return () => window.removeEventListener('keydown', onKey)
83
+ })
84
+
85
+ return {
86
+ get query() { return query },
87
+ set query(v: string) { query = v },
88
+ get active() { return active },
89
+ get results() { return results },
90
+ isActive: (i: number) => i === active,
91
+ move,
92
+ run,
93
+ /** Arrow/Enter handler - attach to the panel (catches events bubbled from the
94
+ * input) so keys work whether dispatched on the input or the dialog. */
95
+ onKeydown: onInputKeydown,
96
+ /** Id of the list element (for aria-controls). */
97
+ listId,
98
+ /** Id of the option at `i` (for aria-activedescendant + item id). */
99
+ optionId,
100
+ /** Spread onto the search `<input>`. */
101
+ inputProps: () => ({
102
+ role: 'combobox' as const,
103
+ 'aria-expanded': true as const,
104
+ 'aria-controls': listId,
105
+ 'aria-activedescendant': results[active] ? optionId(active) : undefined,
106
+ value: query,
107
+ oninput: (e: Event) => { query = (e.currentTarget as HTMLInputElement).value },
108
+ // Arrow/Enter is handled on the panel (see `onKeydown`) via bubbling, so it
109
+ // works for both input-dispatched and panel-dispatched key events.
110
+ }),
111
+ /** Spread onto the `<ul>` list. */
112
+ listProps: () => ({ id: listId, role: 'listbox' as const }),
113
+ /** Spread onto the option element at `i`. */
114
+ optionProps: (i: number) => ({
115
+ id: optionId(i),
116
+ 'data-idx': i,
117
+ role: 'option' as const,
118
+ 'aria-selected': i === active,
119
+ onpointermove: () => { active = i },
120
+ onclick: () => run(results[i]),
121
+ }),
122
+ }
123
+ }
124
+
125
+ export type Command = ReturnType<typeof createCommand>