@svgrid/grid 2.1.22 → 2.2.0

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 (295) 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/FlexRender.svelte +96 -96
  7. package/dist/GridFooter.svelte +178 -178
  8. package/dist/GridMenus.svelte +83 -3
  9. package/dist/SvAutoComplete.svelte +5 -3
  10. package/dist/SvCarousel.svelte +141 -163
  11. package/dist/SvColorInput.svelte +187 -186
  12. package/dist/SvColorInput.svelte.d.ts +1 -1
  13. package/dist/SvComboBox.svelte +7 -3
  14. package/dist/SvCommand.svelte +28 -85
  15. package/dist/SvContextMenu.svelte +116 -116
  16. package/dist/SvCountryInput.svelte +1 -1
  17. package/dist/SvDateRangeInput.svelte +1 -1
  18. package/dist/SvDateTimePicker.svelte +9 -3
  19. package/dist/SvDateTimePicker.svelte.d.ts +3 -0
  20. package/dist/SvDockLayout.svelte +244 -0
  21. package/dist/SvDockLayout.svelte.d.ts +77 -0
  22. package/dist/SvDockManager.svelte +922 -0
  23. package/dist/SvDockManager.svelte.d.ts +106 -0
  24. package/dist/SvDrawer.svelte +250 -228
  25. package/dist/SvDrawer.svelte.d.ts +4 -0
  26. package/dist/SvDropDownList.svelte +85 -13
  27. package/dist/SvDropDownList.svelte.d.ts +3 -0
  28. package/dist/SvField.svelte +21 -4
  29. package/dist/SvField.svelte.d.ts +1 -0
  30. package/dist/SvForm.svelte +201 -91
  31. package/dist/SvForm.svelte.d.ts +15 -22
  32. package/dist/SvGrid.controller.svelte.d.ts +32 -1
  33. package/dist/SvGrid.controller.svelte.js +156 -27
  34. package/dist/SvGrid.css +193 -5
  35. package/dist/SvGrid.svelte +3131 -2786
  36. package/dist/SvGrid.types.d.ts +452 -1
  37. package/dist/SvGridBoard.svelte +2339 -2339
  38. package/dist/SvGridChart.svelte +1724 -1724
  39. package/dist/SvGridChartPanel.svelte +485 -485
  40. package/dist/SvGridDropdown.svelte +689 -682
  41. package/dist/SvGridSelect.svelte +1 -0
  42. package/dist/SvGroupCell.svelte +109 -109
  43. package/dist/SvListBox.svelte +36 -14
  44. package/dist/SvMaskedInput.svelte +4 -3
  45. package/dist/SvMaskedInput.svelte.d.ts +1 -1
  46. package/dist/SvMenu.svelte +124 -124
  47. package/dist/SvMenuList.svelte +117 -165
  48. package/dist/SvMenuList.svelte.d.ts +2 -13
  49. package/dist/SvModal.svelte +12 -29
  50. package/dist/SvMultiSelect.svelte +1 -0
  51. package/dist/SvNumberInput.svelte +131 -130
  52. package/dist/SvNumberInput.svelte.d.ts +1 -1
  53. package/dist/SvOtpInput.svelte +158 -158
  54. package/dist/SvPagination.svelte +18 -18
  55. package/dist/SvPasswordInput.svelte +3 -2
  56. package/dist/SvPasswordInput.svelte.d.ts +1 -1
  57. package/dist/SvPhoneInput.svelte +3 -2
  58. package/dist/SvPhoneInput.svelte.d.ts +1 -1
  59. package/dist/SvPopover.svelte +139 -139
  60. package/dist/SvRowGroupPanel.svelte +170 -170
  61. package/dist/SvScrollArea.svelte +61 -61
  62. package/dist/SvStepper.svelte +6 -9
  63. package/dist/SvTagsInput.svelte +3 -2
  64. package/dist/SvTagsInput.svelte.d.ts +1 -1
  65. package/dist/SvTooltip.svelte +127 -138
  66. package/dist/SvTour.svelte +204 -176
  67. package/dist/SvTree.svelte +377 -377
  68. package/dist/SvTreeSelect.svelte +230 -229
  69. package/dist/build-api.js +76 -54
  70. package/dist/cdn/svgrid.js +10913 -6450
  71. package/dist/cdn/svgrid.svelte-external.js +9628 -5481
  72. package/dist/chart-export.js +8 -8
  73. package/dist/columns.js +6 -0
  74. package/dist/conditional-formatting.d.ts +67 -3
  75. package/dist/conditional-formatting.js +124 -17
  76. package/dist/createCarousel.svelte.d.ts +72 -0
  77. package/dist/createCarousel.svelte.js +80 -0
  78. package/dist/createCommand.svelte.d.ts +62 -0
  79. package/dist/createCommand.svelte.js +109 -0
  80. package/dist/createForm.svelte.d.ts +62 -0
  81. package/dist/createForm.svelte.js +374 -0
  82. package/dist/createMenu.svelte.d.ts +88 -0
  83. package/dist/createMenu.svelte.js +153 -0
  84. package/dist/createOverlay.svelte.d.ts +38 -0
  85. package/dist/createOverlay.svelte.js +68 -0
  86. package/dist/createPagination.d.ts +74 -0
  87. package/dist/createPagination.js +71 -0
  88. package/dist/createPopoverSelect.svelte.js +6 -12
  89. package/dist/createStepper.d.ts +44 -0
  90. package/dist/createStepper.js +26 -0
  91. package/dist/createTooltip.svelte.d.ts +32 -0
  92. package/dist/createTooltip.svelte.js +61 -0
  93. package/dist/datetime/timezone.d.ts +36 -0
  94. package/dist/datetime/timezone.js +110 -0
  95. package/dist/dock-context.d.ts +52 -0
  96. package/dist/dock-context.js +1 -0
  97. package/dist/dock-manager-model.d.ts +136 -0
  98. package/dist/dock-manager-model.js +514 -0
  99. package/dist/dock-model.d.ts +105 -0
  100. package/dist/dock-model.js +288 -0
  101. package/dist/dock-popout.d.ts +30 -0
  102. package/dist/dock-popout.js +81 -0
  103. package/dist/editor-contract.d.ts +2 -0
  104. package/dist/filter-operators.d.ts +6 -0
  105. package/dist/filter-operators.js +35 -2
  106. package/dist/filtering/excel-filters.d.ts +18 -1
  107. package/dist/filtering/excel-filters.js +56 -0
  108. package/dist/form-field.d.ts +73 -0
  109. package/dist/form-field.js +15 -0
  110. package/dist/index.d.ts +25 -5
  111. package/dist/index.js +40 -4
  112. package/dist/list-nav.d.ts +18 -0
  113. package/dist/list-nav.js +30 -0
  114. package/dist/list-option.d.ts +2 -0
  115. package/dist/menu-item.d.ts +20 -0
  116. package/dist/menu-item.js +1 -0
  117. package/dist/menus.d.ts +5 -0
  118. package/dist/menus.js +68 -16
  119. package/dist/panel-resize.d.ts +25 -0
  120. package/dist/panel-resize.js +40 -0
  121. package/dist/popover.d.ts +29 -3
  122. package/dist/popover.js +22 -6
  123. package/dist/recurrence.d.ts +33 -7
  124. package/dist/recurrence.js +151 -13
  125. package/dist/row-drag.d.ts +8 -0
  126. package/dist/row-drag.js +11 -0
  127. package/dist/scheduler-ical.d.ts +30 -0
  128. package/dist/scheduler-ical.js +180 -0
  129. package/dist/scheduler-model.d.ts +269 -0
  130. package/dist/scheduler-model.js +602 -0
  131. package/dist/scheduler-view.svelte.d.ts +22 -0
  132. package/dist/scheduler-view.svelte.js +13 -0
  133. package/dist/spreadsheet.js +20 -12
  134. package/dist/ui-app.types.d.ts +3 -0
  135. package/package.json +14 -10
  136. package/src/DockNodeView.svelte +306 -0
  137. package/src/DockPopoutHost.svelte +17 -0
  138. package/src/FlexRender.svelte +96 -96
  139. package/src/GridFooter.svelte +178 -178
  140. package/src/GridMenus.svelte +83 -3
  141. package/src/SvAutoComplete.svelte +5 -3
  142. package/src/SvCarousel.svelte +141 -163
  143. package/src/SvCarousel.test.ts +68 -0
  144. package/src/SvColorInput.svelte +187 -186
  145. package/src/SvComboBox.svelte +7 -3
  146. package/src/SvCommand.svelte +28 -85
  147. package/src/SvCommand.test.ts +78 -0
  148. package/src/SvContextMenu.svelte +116 -116
  149. package/src/SvCountryInput.svelte +1 -1
  150. package/src/SvDateRangeInput.svelte +1 -1
  151. package/src/SvDateTimePicker.svelte +9 -3
  152. package/src/SvDockLayout.svelte +244 -0
  153. package/src/SvDockManager.svelte +922 -0
  154. package/src/SvDrawer.svelte +250 -228
  155. package/src/SvDropDownList.svelte +85 -13
  156. package/src/SvField.svelte +21 -4
  157. package/src/SvForm.svelte +201 -91
  158. package/src/SvForm.test.ts +299 -0
  159. package/src/SvGrid.controller.svelte.ts +3158 -3034
  160. package/src/SvGrid.css +193 -5
  161. package/src/SvGrid.svelte +3131 -2786
  162. package/src/SvGrid.types.ts +1489 -1027
  163. package/src/SvGridBoard.svelte +2339 -2339
  164. package/src/SvGridChart.svelte +1724 -1724
  165. package/src/SvGridChartPanel.svelte +485 -485
  166. package/src/SvGridDropdown.svelte +689 -682
  167. package/src/SvGridSelect.svelte +1 -0
  168. package/src/SvGroupCell.svelte +109 -109
  169. package/src/SvListBox.svelte +36 -14
  170. package/src/SvMaskedInput.svelte +4 -3
  171. package/src/SvMenu.svelte +124 -124
  172. package/src/SvMenu.test.ts +97 -73
  173. package/src/SvMenuList.svelte +117 -165
  174. package/src/SvModal.svelte +12 -29
  175. package/src/SvModal.test.ts +82 -0
  176. package/src/SvMultiSelect.svelte +1 -0
  177. package/src/SvNumberInput.svelte +131 -130
  178. package/src/SvOtpInput.svelte +158 -158
  179. package/src/SvPagination.svelte +18 -18
  180. package/src/SvPasswordInput.svelte +3 -2
  181. package/src/SvPhoneInput.svelte +3 -2
  182. package/src/SvPopover.svelte +139 -139
  183. package/src/SvRowGroupPanel.svelte +170 -170
  184. package/src/SvScrollArea.svelte +61 -61
  185. package/src/SvStepper.svelte +6 -9
  186. package/src/SvTagsInput.svelte +3 -2
  187. package/src/SvTooltip.svelte +127 -138
  188. package/src/SvTooltip.test.ts +73 -0
  189. package/src/SvTour.svelte +204 -176
  190. package/src/SvTree.svelte +377 -377
  191. package/src/SvTreeSelect.svelte +230 -229
  192. package/src/a11y.contract.test.ts +49 -49
  193. package/src/a11y.test.ts +59 -59
  194. package/src/a11y.ts +61 -61
  195. package/src/build-api.ts +870 -847
  196. package/src/cell-formatting.ts +169 -169
  197. package/src/cell-render.ts +469 -469
  198. package/src/chart-export.ts +201 -201
  199. package/src/chart.ts +2296 -2296
  200. package/src/collaboration.test.ts +104 -104
  201. package/src/collaboration.ts +167 -167
  202. package/src/columns.ts +5 -0
  203. package/src/conditional-formatting.test.ts +104 -0
  204. package/src/conditional-formatting.ts +176 -19
  205. package/src/core.performance.test.ts +30 -30
  206. package/src/core.ts +1111 -1111
  207. package/src/createAutocomplete.svelte.ts +132 -132
  208. package/src/createCarousel.svelte.ts +104 -0
  209. package/src/createCombobox.svelte.ts +179 -179
  210. package/src/createCommand.svelte.ts +125 -0
  211. package/src/createCountryInput.svelte.ts +157 -157
  212. package/src/createDropdownList.svelte.ts +162 -162
  213. package/src/createForm.svelte.ts +333 -0
  214. package/src/createGrid.svelte.ts +42 -42
  215. package/src/createGrid.test.ts +10 -10
  216. package/src/createGridState.svelte.ts +17 -17
  217. package/src/createMenu.svelte.ts +202 -0
  218. package/src/createOverlay.svelte.ts +101 -0
  219. package/src/createPagination.test.ts +65 -0
  220. package/src/createPagination.ts +84 -0
  221. package/src/createPopoverSelect.svelte.ts +206 -210
  222. package/src/createStepper.test.ts +62 -0
  223. package/src/createStepper.ts +61 -0
  224. package/src/createTooltip.svelte.ts +86 -0
  225. package/src/createTree.svelte.ts +319 -319
  226. package/src/datetime/timezone.test.ts +69 -0
  227. package/src/datetime/timezone.ts +134 -0
  228. package/src/dock-context.ts +47 -0
  229. package/src/dock-manager-model.test.ts +275 -0
  230. package/src/dock-manager-model.ts +593 -0
  231. package/src/dock-model.test.ts +163 -0
  232. package/src/dock-model.ts +368 -0
  233. package/src/dock-popout.ts +94 -0
  234. package/src/editing.test.ts +859 -859
  235. package/src/editing.ts +675 -675
  236. package/src/editor-contract.ts +2 -0
  237. package/src/export-data-api.test.ts +126 -126
  238. package/src/export-format.test.ts +107 -107
  239. package/src/export-format.ts +598 -598
  240. package/src/filter-operators.test.ts +59 -0
  241. package/src/filter-operators.ts +35 -2
  242. package/src/filtering/excel-filters.test.ts +86 -1
  243. package/src/filtering/excel-filters.ts +65 -0
  244. package/src/flex-render.ts +3 -3
  245. package/src/form-field.ts +87 -0
  246. package/src/index.ts +623 -469
  247. package/src/keyboard.test.ts +59 -59
  248. package/src/keyboard.ts +97 -97
  249. package/src/list-nav.test.ts +49 -0
  250. package/src/list-nav.ts +29 -0
  251. package/src/list-option.ts +2 -0
  252. package/src/menu-item.ts +21 -0
  253. package/src/menus.test.ts +48 -0
  254. package/src/menus.ts +639 -582
  255. package/src/merge-objects.ts +48 -48
  256. package/src/overlays.test.ts +90 -90
  257. package/src/panel-resize.ts +52 -0
  258. package/src/popover.bounds.test.ts +93 -0
  259. package/src/popover.ts +51 -7
  260. package/src/recurrence.test.ts +88 -1
  261. package/src/recurrence.ts +166 -15
  262. package/src/render-component.ts +28 -28
  263. package/src/row-drag.ts +10 -0
  264. package/src/scheduler-ical.test.ts +68 -0
  265. package/src/scheduler-ical.ts +180 -0
  266. package/src/scheduler-model.test.ts +546 -0
  267. package/src/scheduler-model.ts +853 -0
  268. package/src/scheduler-view.svelte.ts +33 -0
  269. package/src/selection.test.ts +754 -754
  270. package/src/selection.ts +600 -600
  271. package/src/server-data-source.test.ts +289 -289
  272. package/src/server-data-source.ts +413 -413
  273. package/src/sparkline.test.ts +68 -68
  274. package/src/sparkline.ts +169 -169
  275. package/src/spreadsheet.test.ts +489 -489
  276. package/src/spreadsheet.ts +312 -304
  277. package/src/static-functions.ts +11 -11
  278. package/src/subscribe.ts +38 -38
  279. package/src/svgrid-wrapper.types.ts +501 -501
  280. package/src/svgrid.behavior.test.ts +706 -706
  281. package/src/svgrid.charting.test.ts +527 -527
  282. package/src/svgrid.effect-driven-filter.svelte.test.ts +116 -0
  283. package/src/svgrid.features.test.ts +157 -157
  284. package/src/svgrid.new-features.wrapper.test.ts +251 -251
  285. package/src/svgrid.wrapper.test.ts +40 -40
  286. package/src/test-setup.ts +62 -62
  287. package/src/themes/index.ts +159 -159
  288. package/src/ui-app.types.ts +3 -0
  289. package/src/virtual.test.ts +6 -4
  290. package/src/virtualization/column-virtualizer.test.ts +27 -27
  291. package/src/virtualization/column-virtualizer.ts +30 -30
  292. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  293. package/src/virtualization/types.ts +30 -30
  294. package/src/virtualization/virtualizer.test.ts +47 -47
  295. package/src/virtualization/virtualizer.ts +296 -296
@@ -1,186 +1,187 @@
1
- <script lang="ts">
2
- /**
3
- * SvColorInput - a color swatch that opens a portalled popover with a hex
4
- * field, a native picker, and a preset palette. Parity: Smart `smart-color-input`.
5
- * Emits a hex string. Popover escapes the grid scroll container via popover.ts.
6
- *
7
- * The styled renderer over the headless `createColorInput` core: hex
8
- * normalization, palette, draft field and open/close state come from the core;
9
- * the portal + anchored measurement stay here (render concerns).
10
- */
11
- import { anchoredRect, portalToBody, popIn, type AnchoredRect } from './popover'
12
- import { createDismissableLayer } from './a11y/dismissable'
13
- import SvField from './SvField.svelte'
14
- import { nextEditorId, resolveMessages, type SvEditorProps } from './editor-contract'
15
- import { createColorInput } from './createColorInput.svelte'
16
-
17
- /** User-facing strings for the color popover (localizable via `messages`). */
18
- type ColorMessages = { dialog: string; picker: string; hex: string }
19
- const DEFAULT_MESSAGES: ColorMessages = { dialog: 'Choose color', picker: 'Color picker', hex: 'Hex value' }
20
-
21
- type Props = SvEditorProps & {
22
- value?: string
23
- onChange?: (hex: string) => void
24
- /** Preset swatches. */
25
- palette?: string[]
26
- autoOpen?: boolean
27
- /** Override the built-in popover strings. */
28
- messages?: Partial<ColorMessages>
29
- }
30
-
31
- let {
32
- value = '#3b82f6',
33
- onChange,
34
- disabled = false,
35
- readonly = false,
36
- palette,
37
- name,
38
- size = 'md',
39
- ariaLabel,
40
- invalid = false,
41
- required = false,
42
- error,
43
- label,
44
- hint,
45
- dir,
46
- id,
47
- autoOpen = false,
48
- messages,
49
- }: Props = $props()
50
-
51
- const autoId = nextEditorId('sv-color')
52
- const uid = $derived(id ?? autoId)
53
- const M = $derived(resolveMessages(DEFAULT_MESSAGES, messages))
54
-
55
- const col = createColorInput({
56
- value: () => value,
57
- onChange: (hex) => onChange?.(hex),
58
- disabled: () => disabled,
59
- readonly: () => readonly,
60
- palette: () => palette as string[],
61
- id: () => uid,
62
- invalid: () => invalid,
63
- required: () => required,
64
- error: () => error,
65
- hint: () => hint,
66
- ariaLabel: () => ariaLabel,
67
- })
68
-
69
- let triggerEl = $state<HTMLButtonElement | null>(null)
70
- let panelEl = $state<HTMLDivElement | null>(null)
71
- let panelRect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
72
-
73
- function updatePos() {
74
- if (!triggerEl) return
75
- panelRect = anchoredRect(triggerEl.getBoundingClientRect(), { estimatedHeight: 210, minWidth: 200 })
76
- }
77
-
78
- $effect(() => {
79
- if (!col.popover.open) return
80
- updatePos()
81
- const reposition = () => updatePos()
82
- window.addEventListener('scroll', reposition, true)
83
- window.addEventListener('resize', reposition)
84
- const layer = createDismissableLayer({
85
- element: () => [triggerEl, panelEl],
86
- onDismiss: () => col.popover.close(),
87
- })
88
- layer.activate()
89
- return () => {
90
- window.removeEventListener('scroll', reposition, true)
91
- window.removeEventListener('resize', reposition)
92
- layer.release()
93
- }
94
- })
95
-
96
- function focusOpen(node: HTMLButtonElement) {
97
- if (!autoOpen) return
98
- node.focus(); col.popover.show()
99
- }
100
- </script>
101
-
102
- <SvField id={uid} {label} {hint} {error} {required} {dir}>
103
- <button
104
- bind:this={triggerEl}
105
- class="sv-color sv-color--{size}"
106
- class:is-disabled={disabled}
107
- class:is-invalid={invalid}
108
- {...col.swatchProps()}
109
- use:focusOpen
110
- >
111
- <span class="sv-color__swatch" style:background={col.normalized}></span>
112
- <span class="sv-color__hex">{col.normalized}</span>
113
- </button>
114
- </SvField>
115
-
116
- {#if col.popover.open}
117
- <div
118
- class="sv-color__panel"
119
- bind:this={panelEl}
120
- use:portalToBody
121
- use:popIn={{ up: panelRect.openUpward }}
122
- style:position="fixed"
123
- style:top={`${panelRect.top}px`}
124
- style:left={`${panelRect.left}px`}
125
- role="dialog"
126
- aria-label={M.dialog}
127
- >
128
- <div class="sv-color__top">
129
- <input class="sv-color__native" type="color" value={col.normalized} oninput={(e) => col.pick((e.currentTarget as HTMLInputElement).value)} aria-label={M.picker} />
130
- <input
131
- class="sv-color__field"
132
- type="text"
133
- bind:value={col.hexDraft}
134
- spellcheck="false"
135
- aria-label={M.hex}
136
- onblur={col.commitHex}
137
- onkeydown={(e) => { if (e.key === 'Enter') col.commitHex() }}
138
- />
139
- </div>
140
- <div class="sv-color__palette">
141
- {#each col.palette as c (c)}
142
- <button
143
- class="sv-color__chip"
144
- class:is-active={col.isActive(c)}
145
- style:background={c}
146
- {...col.chipProps(c)}
147
- ></button>
148
- {/each}
149
- </div>
150
- </div>
151
- {/if}
152
- {#if name}<input type="hidden" {name} value={col.normalized} />{/if}
153
-
154
- <style>
155
- .sv-color {
156
- --_accent: var(--sg-accent, #2563eb);
157
- display: inline-flex; align-items: center; gap: 8px;
158
- background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
159
- border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
160
- padding: 0 10px; cursor: pointer; font: inherit;
161
- }
162
- .sv-color--sm { height: 28px; font-size: 12px; }
163
- .sv-color--md { height: 34px; font-size: 13px; }
164
- .sv-color--lg { height: 40px; font-size: 15px; }
165
- .sv-color.is-disabled { opacity: 0.6; cursor: not-allowed; }
166
- .sv-color.is-invalid { border-color: var(--sg-danger, #dc2626); }
167
- .sv-color.is-invalid:focus-within { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
168
- .sv-color:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; }
169
- .sv-color__swatch { width: 18px; height: 18px; border-radius: 4px; border: 1px solid rgba(0,0,0,0.15); flex: none; }
170
- .sv-color__hex { font-variant-numeric: tabular-nums; text-transform: lowercase; }
171
-
172
- :global(.sv-color__panel) {
173
- z-index: 2147483647; width: 208px; padding: 12px;
174
- background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e2e8f0);
175
- border-radius: 12px; box-shadow: 0 16px 48px -12px rgba(15,23,42,0.35);
176
- }
177
- :global(.sv-color__top) { display: flex; gap: 8px; margin-bottom: 10px; }
178
- :global(.sv-color__native) { width: 36px; height: 34px; padding: 0; border: 1px solid var(--sg-border, #e2e8f0); border-radius: 8px; background: none; cursor: pointer; }
179
- :global(.sv-color__field) {
180
- flex: 1; min-width: 0; height: 34px; padding: 0 10px; font: inherit; font-size: 13px;
181
- border: 1px solid var(--sg-input-border, #cbd5e1); border-radius: 8px; background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); outline: none;
182
- }
183
- :global(.sv-color__palette) { display: grid; grid-template-columns: repeat(10, 1fr); gap: 4px; }
184
- :global(.sv-color__chip) { width: 100%; aspect-ratio: 1; border-radius: 5px; border: 1px solid rgba(0,0,0,0.12); cursor: pointer; padding: 0; }
185
- :global(.sv-color__chip.is-active) { outline: 2px solid var(--sg-accent, #2563eb); outline-offset: 1px; }
186
- </style>
1
+ <script lang="ts">
2
+ /**
3
+ * SvColorInput - a color swatch that opens a portalled popover with a hex
4
+ * field, a native picker, and a preset palette. Parity: Smart `smart-color-input`.
5
+ * Emits a hex string. Popover escapes the grid scroll container via popover.ts.
6
+ *
7
+ * The styled renderer over the headless `createColorInput` core: hex
8
+ * normalization, palette, draft field and open/close state come from the core;
9
+ * the portal + anchored measurement stay here (render concerns).
10
+ */
11
+ import { anchoredRect, portalToBody, popIn, type AnchoredRect } from './popover'
12
+ import { createDismissableLayer } from './a11y/dismissable'
13
+ import SvField from './SvField.svelte'
14
+ import { nextEditorId, resolveMessages, type SvEditorProps } from './editor-contract'
15
+ import { createColorInput } from './createColorInput.svelte'
16
+
17
+ /** User-facing strings for the color popover (localizable via `messages`). */
18
+ type ColorMessages = { dialog: string; picker: string; hex: string }
19
+ const DEFAULT_MESSAGES: ColorMessages = { dialog: 'Choose color', picker: 'Color picker', hex: 'Hex value' }
20
+
21
+ type Props = SvEditorProps & {
22
+ value?: string
23
+ onChange?: (hex: string) => void
24
+ /** Preset swatches. */
25
+ palette?: string[]
26
+ autoOpen?: boolean
27
+ /** Override the built-in popover strings. */
28
+ messages?: Partial<ColorMessages>
29
+ }
30
+
31
+ let {
32
+ value = $bindable('#3b82f6'),
33
+ onChange,
34
+ disabled = false,
35
+ readonly = false,
36
+ palette,
37
+ name,
38
+ size = 'md',
39
+ ariaLabel,
40
+ invalid = false,
41
+ required = false,
42
+ error,
43
+ label,
44
+ hint,
45
+ dir,
46
+ id,
47
+ autoOpen = false,
48
+ messages,
49
+ }: Props = $props()
50
+
51
+ const autoId = nextEditorId('sv-color')
52
+ const uid = $derived(id ?? autoId)
53
+ const M = $derived(resolveMessages(DEFAULT_MESSAGES, messages))
54
+
55
+ const col = createColorInput({
56
+ value: () => value,
57
+ // Write `value` (so `bind:value` works) AND fire `onChange` (callback users).
58
+ onChange: (hex) => { value = hex; onChange?.(hex) },
59
+ disabled: () => disabled,
60
+ readonly: () => readonly,
61
+ palette: () => palette as string[],
62
+ id: () => uid,
63
+ invalid: () => invalid,
64
+ required: () => required,
65
+ error: () => error,
66
+ hint: () => hint,
67
+ ariaLabel: () => ariaLabel,
68
+ })
69
+
70
+ let triggerEl = $state<HTMLButtonElement | null>(null)
71
+ let panelEl = $state<HTMLDivElement | null>(null)
72
+ let panelRect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 })
73
+
74
+ function updatePos() {
75
+ if (!triggerEl) return
76
+ panelRect = anchoredRect(triggerEl.getBoundingClientRect(), { estimatedHeight: 210, minWidth: 200 })
77
+ }
78
+
79
+ $effect(() => {
80
+ if (!col.popover.open) return
81
+ updatePos()
82
+ const reposition = () => updatePos()
83
+ window.addEventListener('scroll', reposition, true)
84
+ window.addEventListener('resize', reposition)
85
+ const layer = createDismissableLayer({
86
+ element: () => [triggerEl, panelEl],
87
+ onDismiss: () => col.popover.close(),
88
+ })
89
+ layer.activate()
90
+ return () => {
91
+ window.removeEventListener('scroll', reposition, true)
92
+ window.removeEventListener('resize', reposition)
93
+ layer.release()
94
+ }
95
+ })
96
+
97
+ function focusOpen(node: HTMLButtonElement) {
98
+ if (!autoOpen) return
99
+ node.focus(); col.popover.show()
100
+ }
101
+ </script>
102
+
103
+ <SvField id={uid} {label} {hint} {error} {required} {dir}>
104
+ <button
105
+ bind:this={triggerEl}
106
+ class="sv-color sv-color--{size}"
107
+ class:is-disabled={disabled}
108
+ class:is-invalid={invalid}
109
+ {...col.swatchProps()}
110
+ use:focusOpen
111
+ >
112
+ <span class="sv-color__swatch" style:background={col.normalized}></span>
113
+ <span class="sv-color__hex">{col.normalized}</span>
114
+ </button>
115
+ </SvField>
116
+
117
+ {#if col.popover.open}
118
+ <div
119
+ class="sv-color__panel"
120
+ bind:this={panelEl}
121
+ use:portalToBody
122
+ use:popIn={{ up: panelRect.openUpward }}
123
+ style:position="fixed"
124
+ style:top={`${panelRect.top}px`}
125
+ style:left={`${panelRect.left}px`}
126
+ role="dialog"
127
+ aria-label={M.dialog}
128
+ >
129
+ <div class="sv-color__top">
130
+ <input class="sv-color__native" type="color" value={col.normalized} oninput={(e) => col.pick((e.currentTarget as HTMLInputElement).value)} aria-label={M.picker} />
131
+ <input
132
+ class="sv-color__field"
133
+ type="text"
134
+ bind:value={col.hexDraft}
135
+ spellcheck="false"
136
+ aria-label={M.hex}
137
+ onblur={col.commitHex}
138
+ onkeydown={(e) => { if (e.key === 'Enter') col.commitHex() }}
139
+ />
140
+ </div>
141
+ <div class="sv-color__palette">
142
+ {#each col.palette as c (c)}
143
+ <button
144
+ class="sv-color__chip"
145
+ class:is-active={col.isActive(c)}
146
+ style:background={c}
147
+ {...col.chipProps(c)}
148
+ ></button>
149
+ {/each}
150
+ </div>
151
+ </div>
152
+ {/if}
153
+ {#if name}<input type="hidden" {name} value={col.normalized} />{/if}
154
+
155
+ <style>
156
+ .sv-color {
157
+ --_accent: var(--sg-accent, #2563eb);
158
+ display: inline-flex; align-items: center; gap: 8px;
159
+ background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
160
+ border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
161
+ padding: 0 10px; cursor: pointer; font: inherit;
162
+ }
163
+ .sv-color--sm { height: 28px; font-size: 12px; }
164
+ .sv-color--md { height: 34px; font-size: 13px; }
165
+ .sv-color--lg { height: 40px; font-size: 15px; }
166
+ .sv-color.is-disabled { opacity: 0.6; cursor: not-allowed; }
167
+ .sv-color.is-invalid { border-color: var(--sg-danger, #dc2626); }
168
+ .sv-color.is-invalid:focus-within { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
169
+ .sv-color:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; }
170
+ .sv-color__swatch { width: 18px; height: 18px; border-radius: 4px; border: 1px solid rgba(0,0,0,0.15); flex: none; }
171
+ .sv-color__hex { font-variant-numeric: tabular-nums; text-transform: lowercase; }
172
+
173
+ :global(.sv-color__panel) {
174
+ z-index: 2147483647; width: 208px; padding: 12px;
175
+ background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e2e8f0);
176
+ border-radius: 12px; box-shadow: 0 16px 48px -12px rgba(15,23,42,0.35);
177
+ }
178
+ :global(.sv-color__top) { display: flex; gap: 8px; margin-bottom: 10px; }
179
+ :global(.sv-color__native) { width: 36px; height: 34px; padding: 0; border: 1px solid var(--sg-border, #e2e8f0); border-radius: 8px; background: none; cursor: pointer; }
180
+ :global(.sv-color__field) {
181
+ flex: 1; min-width: 0; height: 34px; padding: 0 10px; font: inherit; font-size: 13px;
182
+ border: 1px solid var(--sg-input-border, #cbd5e1); border-radius: 8px; background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); outline: none;
183
+ }
184
+ :global(.sv-color__palette) { display: grid; grid-template-columns: repeat(10, 1fr); gap: 4px; }
185
+ :global(.sv-color__chip) { width: 100%; aspect-ratio: 1; border-radius: 5px; border: 1px solid rgba(0,0,0,0.12); cursor: pointer; padding: 0; }
186
+ :global(.sv-color__chip.is-active) { outline: 2px solid var(--sg-accent, #2563eb); outline-offset: 1px; }
187
+ </style>
@@ -14,6 +14,6 @@ type Props = SvEditorProps & {
14
14
  /** Override the built-in popover strings. */
15
15
  messages?: Partial<ColorMessages>;
16
16
  };
17
- declare const SvColorInput: import("svelte").Component<Props, {}, "">;
17
+ declare const SvColorInput: import("svelte").Component<Props, {}, "value">;
18
18
  type SvColorInput = ReturnType<typeof SvColorInput>;
19
19
  export default SvColorInput;
@@ -65,7 +65,7 @@
65
65
  let inputEl = $state<HTMLInputElement | null>(null)
66
66
  let fieldEl = $state<HTMLDivElement | null>(null)
67
67
  let panelEl = $state<HTMLDivElement | null>(null)
68
- let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
68
+ let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 })
69
69
 
70
70
  // Remote data source: fetch options as the query changes (debounced).
71
71
  let remoteOptions = $state<ListOption[]>([])
@@ -111,7 +111,11 @@
111
111
 
112
112
  function updatePos() {
113
113
  if (!fieldEl) return
114
- rect = anchoredRect(fieldEl.getBoundingClientRect(), { estimatedHeight: Math.min(combo.filtered.length, 8) * 34 + 8 })
114
+ // estimatedHeight only picks the flip direction (up vs down); the panel's
115
+ // real max-height comes from availHeight below, so it takes its natural
116
+ // content height (options + group labels) and scrolls only when it truly
117
+ // overflows the room - never from an estimate that under-counts a row.
118
+ rect = anchoredRect(fieldEl.getBoundingClientRect(), { estimatedHeight: Math.min(combo.filtered.length, 8) * 34 + 12 })
115
119
  }
116
120
 
117
121
  // Position + reposition + outside-click close are render concerns (need the DOM).
@@ -150,7 +154,7 @@
150
154
  </SvField>
151
155
 
152
156
  {#if combo.open}
153
- <div bind:this={panelEl} class="sv-ddl__panel" use:portalToBody use:popIn={{ up: rect.openUpward }} style:position="fixed" style:top={`${rect.top}px`} style:left={`${rect.left}px`} style:min-width={`${rect.width}px`} {...combo.listboxProps()}>
157
+ <div bind:this={panelEl} class="sv-ddl__panel" use:portalToBody use:popIn={{ up: rect.openUpward }} style:position="fixed" style:top={rect.openUpward ? undefined : `${rect.top}px`} style:bottom={rect.openUpward ? `${rect.bottom}px` : undefined} style:left={`${rect.left}px`} style:min-width={`${rect.width}px`} style:max-height={`${Math.min(rect.availHeight, 288)}px`} {...combo.listboxProps()}>
154
158
  {#if loading}
155
159
  <div class="sv-ddl__empty sv-ddl__loading"><svg class="sv-ddl__spin" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M21 12a9 9 0 1 1-6.2-8.6" /></svg>{M.loading}</div>
156
160
  {:else if loadOptions && !searched && combo.query.trim().length < minLength}
@@ -16,10 +16,8 @@
16
16
  */
17
17
  import type { CommandItem } from './ui-app.types'
18
18
  import { portalToBody, popIn } from './popover'
19
- import { createFocusTrap } from './a11y/focus-trap'
20
- import { lockScroll } from './a11y/scroll-lock'
21
- import { createDismissableLayer } from './a11y/dismissable'
22
- import { fuzzyScore } from './fuzzy'
19
+ import { createOverlay } from './createOverlay.svelte'
20
+ import { createCommand } from './createCommand.svelte'
23
21
 
24
22
  type Props = {
25
23
  open?: boolean
@@ -42,72 +40,29 @@
42
40
  hotkey = 'mod+k',
43
41
  }: Props = $props()
44
42
 
45
- let query = $state('')
46
- let active = $state(0)
47
43
  let panelEl = $state<HTMLDivElement | null>(null)
48
44
  let inputEl = $state<HTMLInputElement | null>(null)
49
45
 
50
- const results = $derived.by(() => {
51
- const q = query.trim()
52
- const list = commands.filter((c) => !c.disabled)
53
- if (!q) return list
54
- return list
55
- .map((c) => ({ c, s: fuzzyScore(`${c.label} ${c.keywords ?? ''} ${c.group ?? ''}`, q) }))
56
- .filter((x) => x.s !== null)
57
- .sort((a, b) => (b.s as number) - (a.s as number))
58
- .map((x) => x.c)
59
- })
60
-
61
46
  function close() { open = false; onClose?.() }
62
- function run(cmd: CommandItem | undefined) {
63
- if (!cmd || cmd.disabled) return
64
- close()
65
- onRun?.(cmd)
66
- cmd.onRun?.()
67
- }
68
- function move(delta: number) {
69
- const n = results.length
70
- if (!n) return
71
- active = (active + delta + n) % n
72
- queueMicrotask(() =>
73
- panelEl?.querySelector(`[data-idx="${active}"]`)?.scrollIntoView({ block: 'nearest' }),
74
- )
75
- }
76
- function onKeydown(e: KeyboardEvent) {
77
- if (e.key === 'ArrowDown') { e.preventDefault(); move(1) }
78
- else if (e.key === 'ArrowUp') { e.preventDefault(); move(-1) }
79
- else if (e.key === 'Enter') { e.preventDefault(); run(results[active]) }
80
- // Escape + outside-click are owned by the dismissable layer.
81
- }
82
-
83
- // Reset active whenever the query changes.
84
- $effect(() => { void query; active = 0 })
85
47
 
86
- // Focus + scroll lock + Escape/outside dismissal while open.
87
- $effect(() => {
88
- if (!open || !panelEl) return
89
- query = ''
90
- active = 0
91
- const trap = createFocusTrap(panelEl, { initialFocus: () => inputEl })
92
- trap.activate()
93
- const unlock = lockScroll()
94
- const layer = createDismissableLayer({ element: () => panelEl, onDismiss: () => close() })
95
- layer.activate()
96
- return () => { layer.release(); unlock(); trap.release() }
48
+ // Fuzzy filter + roving + run + hotkey live in the command core; the overlay
49
+ // lifecycle (focus-trap into the input, scroll-lock, Escape/outside dismissal)
50
+ // is the shared dialog core.
51
+ const cmd = createCommand({
52
+ commands: () => commands,
53
+ open: () => open,
54
+ onRun,
55
+ onClose: close,
56
+ onToggleOpen: () => (open = !open),
57
+ hotkey: () => hotkey,
58
+ scrollActiveIntoView: (i) =>
59
+ queueMicrotask(() => panelEl?.querySelector(`[data-idx="${i}"]`)?.scrollIntoView({ block: 'nearest' })),
97
60
  })
98
-
99
- // Global toggle hotkey.
100
- $effect(() => {
101
- if (!hotkey) return
102
- const key = hotkey === 'mod+p' ? 'p' : 'k'
103
- const onKey = (e: KeyboardEvent) => {
104
- if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === key) {
105
- e.preventDefault()
106
- open = !open
107
- }
108
- }
109
- window.addEventListener('keydown', onKey)
110
- return () => window.removeEventListener('keydown', onKey)
61
+ const overlay = createOverlay({
62
+ open: () => open,
63
+ getDialog: () => panelEl,
64
+ onClose: close,
65
+ initialFocus: () => inputEl,
111
66
  })
112
67
  </script>
113
68
 
@@ -117,11 +72,8 @@
117
72
  bind:this={panelEl}
118
73
  class="sv-cmd"
119
74
  use:popIn={{}}
120
- role="dialog"
121
- aria-modal="true"
122
- aria-label="Command palette"
123
- tabindex="-1"
124
- onkeydown={onKeydown}
75
+ {...overlay.dialogProps({ label: 'Command palette' })}
76
+ onkeydown={cmd.onKeydown}
125
77
  >
126
78
  <div class="sv-cmd__search">
127
79
  <span class="sv-cmd__searchicon" aria-hidden="true">⌕</span>
@@ -129,29 +81,20 @@
129
81
  bind:this={inputEl}
130
82
  class="sv-cmd__input"
131
83
  type="text"
132
- role="combobox"
133
- aria-expanded="true"
134
- aria-controls="sv-cmd-list"
135
- aria-activedescendant={results[active] ? `sv-cmd-opt-${active}` : undefined}
84
+ aria-label="Command palette search"
136
85
  {placeholder}
137
- value={query}
138
- oninput={(e) => (query = e.currentTarget.value)}
86
+ {...cmd.inputProps()}
139
87
  />
140
88
  </div>
141
- <ul class="sv-cmd__list" id="sv-cmd-list" role="listbox" aria-label="Commands">
142
- {#each results as c, i (c.id)}
143
- {#if !query.trim() && c.group && c.group !== results[i - 1]?.group}
89
+ <ul class="sv-cmd__list" aria-label="Commands" {...cmd.listProps()}>
90
+ {#each cmd.results as c, i (c.id)}
91
+ {#if !cmd.query.trim() && c.group && c.group !== cmd.results[i - 1]?.group}
144
92
  <li class="sv-cmd__group" role="presentation">{c.group}</li>
145
93
  {/if}
146
94
  <li
147
- id={`sv-cmd-opt-${i}`}
148
- data-idx={i}
149
95
  class="sv-cmd__opt"
150
- class:is-active={i === active}
151
- role="option"
152
- aria-selected={i === active}
153
- onpointermove={() => (active = i)}
154
- onclick={() => run(c)}
96
+ class:is-active={cmd.isActive(i)}
97
+ {...cmd.optionProps(i)}
155
98
  >
156
99
  {#if c.icon}<span class="sv-cmd__icon" aria-hidden="true">{c.icon}</span>{/if}
157
100
  <span class="sv-cmd__label">{c.label}</span>