@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
@@ -153,6 +153,7 @@
153
153
  style:top={`${gs.rect.top}px`}
154
154
  style:left={`${gs.rect.left}px`}
155
155
  style:min-width={`${Math.max(gs.rect.width, 320)}px`}
156
+ style:max-height={`${gs.rect.maxHeight}px`}
156
157
  onkeydown={gs.onPanelKeydown}
157
158
  >
158
159
  {#if searchable}
@@ -1,109 +1,109 @@
1
- <!--
2
- Built-in group cell for the server-side group/tree model. Drop it into the
3
- grouping column and it draws the expander + indentation for group rows (and an
4
- indented leaf value) - no hand-written cell recipe. Pair with `serverGroupRows`
5
- to feed `<SvGrid data>`, and wire `onToggle` to the controller's `toggleGroup`.
6
-
7
- cell: (ctx) => renderComponent(SvGroupCell, {
8
- row: ctx.row.original, onToggle: ctl.toggleGroup, leafField: 'name',
9
- })
10
- -->
11
- <script lang="ts" generics="TData">
12
- import type { ServerDisplayRow } from './server-data-source'
13
-
14
- type GroupGridRow = { __group?: ServerDisplayRow<TData> } & Record<string, unknown>
15
- type Props = {
16
- /** The grid row from `serverGroupRows` (carries the `__group` marker). */
17
- row: GroupGridRow
18
- /**
19
- * Expand / collapse a group, or load more children. Receives the grid row;
20
- * wire to `serverGroupNav(ctl).onToggle` (which drives clicks and keyboard).
21
- */
22
- onToggle: (row: GroupGridRow) => void
23
- /** Field to show for leaf rows in the group column (blank if omitted). */
24
- leafField?: string
25
- /** Pixels of indentation per level. Default 18. */
26
- indent?: number
27
- }
28
-
29
- let { row, onToggle, leafField, indent = 18 }: Props = $props()
30
- const meta = $derived(row.__group)
31
- </script>
32
-
33
- {#if meta && meta.kind === 'group'}
34
- <button
35
- type="button"
36
- class="sv-group-cell"
37
- style={`padding-inline-start: ${meta.level * indent}px`}
38
- aria-expanded={meta.expanded}
39
- aria-label={meta.expanded ? 'Collapse group' : 'Expand group'}
40
- onclick={() => onToggle(row)}
41
- >
42
- <span class="sv-group-chev" class:open={meta.expanded} aria-hidden="true">▸</span>
43
- <span class="sv-group-key">{meta.key}</span>
44
- {#if meta.loading}<span class="sv-group-load" aria-hidden="true">…</span>{/if}
45
- </button>
46
- {:else if meta && meta.kind === 'more'}
47
- <button
48
- type="button"
49
- class="sv-group-more"
50
- style={`padding-inline-start: ${meta.level * indent}px`}
51
- disabled={meta.loading}
52
- onclick={() => onToggle(row)}
53
- >
54
- {meta.loading ? 'Loading...' : `Load ${meta.remaining} more`}
55
- </button>
56
- {:else if meta && meta.kind === 'footer'}
57
- <span class="sv-group-footer" style={`padding-inline-start: ${meta.level * indent}px`}>Total</span>
58
- {:else if meta && meta.kind === 'skeleton'}
59
- <span class="sv-group-skeleton" style={`margin-inline-start: ${meta.level * indent}px`} aria-hidden="true"></span>
60
- {:else if meta}
61
- <span class="sv-group-leaf" style={`padding-inline-start: ${(meta.level + 1) * indent}px`}>
62
- {leafField ? (row[leafField] ?? '') : ''}
63
- </span>
64
- {/if}
65
-
66
- <style>
67
- .sv-group-cell {
68
- display: inline-flex;
69
- align-items: center;
70
- gap: 6px;
71
- background: none;
72
- border: 0;
73
- padding: 0;
74
- font: inherit;
75
- color: inherit;
76
- cursor: pointer;
77
- }
78
- .sv-group-chev {
79
- display: inline-block;
80
- transition: transform 0.15s;
81
- color: var(--sg-muted, #64748b);
82
- }
83
- .sv-group-chev.open { transform: rotate(90deg); }
84
- .sv-group-key { font-weight: 600; }
85
- .sv-group-load { color: var(--sg-muted, #64748b); font-size: 12px; }
86
- .sv-group-leaf { color: var(--sg-muted, #475569); }
87
- .sv-group-more {
88
- background: none;
89
- border: 0;
90
- padding: 0;
91
- font: inherit;
92
- font-size: 13px;
93
- color: var(--sg-accent, #2563eb);
94
- cursor: pointer;
95
- }
96
- .sv-group-more:disabled { color: var(--sg-muted, #64748b); cursor: default; }
97
- .sv-group-footer { font-weight: 600; color: var(--sg-muted, #64748b); font-size: 12px; text-transform: uppercase; letter-spacing: 0.03em; }
98
- .sv-group-skeleton {
99
- display: inline-block;
100
- width: 60%;
101
- height: 10px;
102
- border-radius: 5px;
103
- background: linear-gradient(90deg, var(--sg-border, #e2e8f0) 25%, var(--sg-header-bg, #f1f5f9) 50%, var(--sg-border, #e2e8f0) 75%);
104
- background-size: 200% 100%;
105
- animation: sv-group-shimmer 1.2s infinite;
106
- }
107
- @keyframes sv-group-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
108
- @media (prefers-reduced-motion: reduce) { .sv-group-skeleton { animation: none; } }
109
- </style>
1
+ <!--
2
+ Built-in group cell for the server-side group/tree model. Drop it into the
3
+ grouping column and it draws the expander + indentation for group rows (and an
4
+ indented leaf value) - no hand-written cell recipe. Pair with `serverGroupRows`
5
+ to feed `<SvGrid data>`, and wire `onToggle` to the controller's `toggleGroup`.
6
+
7
+ cell: (ctx) => renderComponent(SvGroupCell, {
8
+ row: ctx.row.original, onToggle: ctl.toggleGroup, leafField: 'name',
9
+ })
10
+ -->
11
+ <script lang="ts" generics="TData">
12
+ import type { ServerDisplayRow } from './server-data-source'
13
+
14
+ type GroupGridRow = { __group?: ServerDisplayRow<TData> } & Record<string, unknown>
15
+ type Props = {
16
+ /** The grid row from `serverGroupRows` (carries the `__group` marker). */
17
+ row: GroupGridRow
18
+ /**
19
+ * Expand / collapse a group, or load more children. Receives the grid row;
20
+ * wire to `serverGroupNav(ctl).onToggle` (which drives clicks and keyboard).
21
+ */
22
+ onToggle: (row: GroupGridRow) => void
23
+ /** Field to show for leaf rows in the group column (blank if omitted). */
24
+ leafField?: string
25
+ /** Pixels of indentation per level. Default 18. */
26
+ indent?: number
27
+ }
28
+
29
+ let { row, onToggle, leafField, indent = 18 }: Props = $props()
30
+ const meta = $derived(row.__group)
31
+ </script>
32
+
33
+ {#if meta && meta.kind === 'group'}
34
+ <button
35
+ type="button"
36
+ class="sv-group-cell"
37
+ style={`padding-inline-start: ${meta.level * indent}px`}
38
+ aria-expanded={meta.expanded}
39
+ aria-label={meta.expanded ? 'Collapse group' : 'Expand group'}
40
+ onclick={() => onToggle(row)}
41
+ >
42
+ <span class="sv-group-chev" class:open={meta.expanded} aria-hidden="true">▸</span>
43
+ <span class="sv-group-key">{meta.key}</span>
44
+ {#if meta.loading}<span class="sv-group-load" aria-hidden="true">…</span>{/if}
45
+ </button>
46
+ {:else if meta && meta.kind === 'more'}
47
+ <button
48
+ type="button"
49
+ class="sv-group-more"
50
+ style={`padding-inline-start: ${meta.level * indent}px`}
51
+ disabled={meta.loading}
52
+ onclick={() => onToggle(row)}
53
+ >
54
+ {meta.loading ? 'Loading...' : `Load ${meta.remaining} more`}
55
+ </button>
56
+ {:else if meta && meta.kind === 'footer'}
57
+ <span class="sv-group-footer" style={`padding-inline-start: ${meta.level * indent}px`}>Total</span>
58
+ {:else if meta && meta.kind === 'skeleton'}
59
+ <span class="sv-group-skeleton" style={`margin-inline-start: ${meta.level * indent}px`} aria-hidden="true"></span>
60
+ {:else if meta}
61
+ <span class="sv-group-leaf" style={`padding-inline-start: ${(meta.level + 1) * indent}px`}>
62
+ {leafField ? (row[leafField] ?? '') : ''}
63
+ </span>
64
+ {/if}
65
+
66
+ <style>
67
+ .sv-group-cell {
68
+ display: inline-flex;
69
+ align-items: center;
70
+ gap: 6px;
71
+ background: none;
72
+ border: 0;
73
+ padding: 0;
74
+ font: inherit;
75
+ color: inherit;
76
+ cursor: pointer;
77
+ }
78
+ .sv-group-chev {
79
+ display: inline-block;
80
+ transition: transform 0.15s;
81
+ color: var(--sg-muted, #64748b);
82
+ }
83
+ .sv-group-chev.open { transform: rotate(90deg); }
84
+ .sv-group-key { font-weight: 600; }
85
+ .sv-group-load { color: var(--sg-muted, #64748b); font-size: 12px; }
86
+ .sv-group-leaf { color: var(--sg-muted, #475569); }
87
+ .sv-group-more {
88
+ background: none;
89
+ border: 0;
90
+ padding: 0;
91
+ font: inherit;
92
+ font-size: 13px;
93
+ color: var(--sg-accent, #2563eb);
94
+ cursor: pointer;
95
+ }
96
+ .sv-group-more:disabled { color: var(--sg-muted, #64748b); cursor: default; }
97
+ .sv-group-footer { font-weight: 600; color: var(--sg-muted, #64748b); font-size: 12px; text-transform: uppercase; letter-spacing: 0.03em; }
98
+ .sv-group-skeleton {
99
+ display: inline-block;
100
+ width: 60%;
101
+ height: 10px;
102
+ border-radius: 5px;
103
+ background: linear-gradient(90deg, var(--sg-border, #e2e8f0) 25%, var(--sg-header-bg, #f1f5f9) 50%, var(--sg-border, #e2e8f0) 75%);
104
+ background-size: 200% 100%;
105
+ animation: sv-group-shimmer 1.2s infinite;
106
+ }
107
+ @keyframes sv-group-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
108
+ @media (prefers-reduced-motion: reduce) { .sv-group-skeleton { animation: none; } }
109
+ </style>
@@ -5,7 +5,7 @@
5
5
  * via `value` (scalar or array) + `onChange`. Scales to huge option sets with
6
6
  * `virtual` (fixed-row windowing), and each row can be an `itemTemplate`.
7
7
  */
8
- import { flushSync, type Snippet } from 'svelte'
8
+ import { type Snippet } from 'svelte'
9
9
  import { groupOptions, hasGroups, type ListOption } from './list-option'
10
10
  import { virtualRange, scrollToIndex } from './virtual'
11
11
  import { createListbox } from './createListbox.svelte'
@@ -101,17 +101,25 @@
101
101
  class:is-invalid={invalid}
102
102
  class:is-virtual={useVirtual}
103
103
  style:--sv-rows={rows}
104
- onscroll={(e) => { scrollTop = e.currentTarget.scrollTop; if (useVirtual) flushSync() }}
104
+ style:--sv-row-h={`${rowHeight}px`}
105
+ onscroll={(e) => (scrollTop = e.currentTarget.scrollTop)}
105
106
  bind:clientHeight={viewportH}
106
107
  {...lb.rootProps()}
107
108
  >
108
109
  {#if useVirtual}
109
- <!-- Fixed-height sizer establishes the scroll range; rows are absolutely
110
- positioned by transform so scrolling never reflows (no flash). -->
111
- <li class="sv-listbox__sizer" aria-hidden="true" style:height={`${vr.totalHeight}px`}></li>
110
+ <!-- Windowing by top/bottom spacers with the visible rows IN FLOW between
111
+ them (padTop + rows + padBottom always sum to the full height). Plain
112
+ flow rows (no absolute positioning, no will-change) keep the scroller
113
+ off its own compositor layer, so a fast jump repaints in step with the
114
+ scroll instead of showing the blank frame a promoted layer would. The
115
+ window updates from `scrollTop` state, which Svelte flushes before the
116
+ next paint - no flushSync (it over-flushes app-wide and, on a fling,
117
+ starves the very handler that has to keep up with the scroll). -->
118
+ <li class="sv-listbox__spacer" aria-hidden="true" style:height={`${vr.padTop}px`}></li>
112
119
  {#each windowed as opt (opt.value)}
113
120
  {@render optionLi(opt, opt.index)}
114
121
  {/each}
122
+ <li class="sv-listbox__spacer" aria-hidden="true" style:height={`${vr.padBottom}px`}></li>
115
123
  {:else}
116
124
  {#each groupOptions(options) as g (g.group ?? ' ')}
117
125
  {#if g.group != null}<li class="sv-listbox__group" role="presentation">{g.group}</li>{/if}
@@ -132,7 +140,6 @@
132
140
  class:is-active={lb.isActive(index)}
133
141
  class:is-disabled={opt.disabled}
134
142
  style:height={`${rowHeight}px`}
135
- style:transform={useVirtual ? `translateY(${index * rowHeight}px)` : undefined}
136
143
  {...lb.optionProps(index)}
137
144
  >
138
145
  {#if multiple}<span class="sv-listbox__check" aria-hidden="true">{isSel(opt) ? '✓' : ''}</span>{/if}
@@ -144,7 +151,7 @@
144
151
  .sv-listbox {
145
152
  --_accent: var(--sg-accent, #2563eb);
146
153
  margin: 0; padding: 4px; list-style: none;
147
- max-height: calc(var(--sv-rows, 7) * 32px + 8px); overflow-y: auto;
154
+ max-height: calc(var(--sv-rows, 7) * var(--sv-row-h, 32px) + 8px); overflow-y: auto;
148
155
  background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
149
156
  border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
150
157
  width: 220px; outline: none;
@@ -152,15 +159,30 @@
152
159
  .sv-listbox:focus-visible { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
153
160
  .sv-listbox.is-invalid { border-color: var(--sg-danger, #dc2626); }
154
161
  .sv-listbox.is-invalid:focus-visible { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
155
- /* Pre-promote the scrolling-contents layer at mount so the FIRST scroll
156
- doesn't trigger a one-time layer promotion + repaint (the initial flash). */
157
- .sv-listbox.is-virtual { position: relative; will-change: scroll-position; }
158
- .sv-listbox__sizer { padding: 0; margin: 0; pointer-events: none; }
159
- .sv-listbox.is-virtual .sv-listbox__opt {
160
- position: absolute; inset-inline: 4px; top: 4px;
162
+ /* Spacers reserve the off-screen height above/below the windowed rows. They
163
+ also paint faint per-row skeleton bars: on a fast scrollbar-thumb drag the
164
+ compositor can reveal this padding before JS re-windows (a drag jumps many
165
+ rows in one frame - overscan can't cover it), so the user would see blank.
166
+ The skeleton fills that gap with placeholder rows that the real rows paint
167
+ over a frame later. Bars are one per `--sv-row-h` band, aligned to the row
168
+ grid (padTop/padBottom are exact multiples of the row height). */
169
+ .sv-listbox__spacer {
170
+ padding: 0; margin: 0; flex: none; pointer-events: none; list-style: none;
171
+ background-image: linear-gradient(
172
+ to bottom,
173
+ transparent 0,
174
+ transparent calc((var(--sv-row-h, 32px) - 10px) / 2),
175
+ var(--sg-skeleton, color-mix(in srgb, currentColor 9%, transparent)) calc((var(--sv-row-h, 32px) - 10px) / 2),
176
+ var(--sg-skeleton, color-mix(in srgb, currentColor 9%, transparent)) calc((var(--sv-row-h, 32px) + 10px) / 2),
177
+ transparent calc((var(--sv-row-h, 32px) + 10px) / 2),
178
+ transparent var(--sv-row-h, 32px)
179
+ );
180
+ background-size: 62% var(--sv-row-h, 32px);
181
+ background-repeat: repeat-y;
182
+ background-position: 12px 0;
161
183
  }
162
184
  .sv-listbox__opt {
163
- display: flex; align-items: center; gap: 8px; box-sizing: border-box; padding: 0 10px;
185
+ display: flex; align-items: center; gap: 8px; box-sizing: border-box; flex: none; padding: 0 10px;
164
186
  border-radius: 6px; cursor: pointer; font-size: 13px;
165
187
  }
166
188
  .sv-listbox__opt.is-active { background: var(--sg-row-hover-bg, #f1f5f9); }
@@ -27,7 +27,7 @@
27
27
  }
28
28
 
29
29
  let {
30
- value = '',
30
+ value = $bindable(''),
31
31
  onChange,
32
32
  mask = '',
33
33
  placeholder,
@@ -53,7 +53,8 @@
53
53
 
54
54
  const mi = createMaskedInput({
55
55
  value: () => value,
56
- onChange: (m, r, c) => onChange?.(m, r, c),
56
+ // Write the masked `value` (so `bind:value` works) AND fire `onChange`.
57
+ onChange: (m, r, c) => { value = m; onChange?.(m, r, c) },
57
58
  mask: () => mask,
58
59
  placeholder: () => placeholder,
59
60
  disabled: () => disabled,
@@ -72,7 +73,7 @@
72
73
  {#if prefixIcon}<span class="sv-masked__adorn">{@render prefixIcon()}</span>{/if}
73
74
  <input class="sv-masked__input" {...mi.inputProps()} />
74
75
  {#if clearable && mi.masked && !disabled && !readonly}
75
- <button type="button" class="sv-masked__clear" aria-label="Clear" tabindex="-1" onclick={() => onChange?.('', '', false)}>
76
+ <button type="button" class="sv-masked__clear" aria-label="Clear" tabindex="-1" onclick={() => { value = ''; onChange?.('', '', false) }}>
76
77
  <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18" /></svg>
77
78
  </button>
78
79
  {/if}
@@ -20,6 +20,6 @@ type Props = SvEditorProps & {
20
20
  prefixIcon?: Snippet;
21
21
  suffixIcon?: Snippet;
22
22
  };
23
- declare const SvMaskedInput: import("svelte").Component<Props, {}, "">;
23
+ declare const SvMaskedInput: import("svelte").Component<Props, {}, "value">;
24
24
  type SvMaskedInput = ReturnType<typeof SvMaskedInput>;
25
25
  export default SvMaskedInput;
@@ -1,124 +1,124 @@
1
- <script lang="ts" module>
2
- export type { MenuItem } from './SvMenuList.svelte'
3
- </script>
4
-
5
- <script lang="ts">
6
- /**
7
- * SvMenu - a dropdown / context menu: a trigger opens a portalled, animated
8
- * menu surface with submenus, separators, icons, shortcuts and full keyboard
9
- * (arrows, Enter, Escape, ArrowRight/Left for submenus). Portals to <body> so it
10
- * escapes any clipping. Parity: Smart menu / context-menu.
11
- *
12
- * ```svelte
13
- * <SvMenu items={items} onSelect={(i) => run(i)}>
14
- * {#snippet anchor()}<button>Actions</button>{/snippet}
15
- * </SvMenu>
16
- * ```
17
- */
18
- import type { Snippet } from 'svelte'
19
- import { anchoredRect, portalToBody, popIn, type AnchoredRect } from './popover'
20
- import { createDismissableLayer } from './a11y/dismissable'
21
- import { getFocusable } from './a11y/focus-trap'
22
- import { type EditorDir } from './editor-contract'
23
- import SvMenuList, { type MenuItem } from './SvMenuList.svelte'
24
-
25
- let {
26
- items,
27
- open = $bindable(false),
28
- onOpenChange,
29
- onSelect,
30
- ariaLabel,
31
- anchor,
32
- dir,
33
- }: {
34
- items: ReadonlyArray<MenuItem>
35
- open?: boolean
36
- onOpenChange?: (open: boolean) => void
37
- onSelect?: (item: MenuItem) => void
38
- ariaLabel?: string
39
- anchor?: Snippet
40
- /** Text direction (rtl mirrors the submenu chevron + flips ArrowLeft/Right). */
41
- dir?: EditorDir
42
- } = $props()
43
-
44
- const resolvedDir = $derived(dir === 'ltr' || dir === 'rtl' ? dir : undefined)
45
-
46
- let anchorEl = $state<HTMLSpanElement | null>(null)
47
- let panelEl = $state<HTMLDivElement | null>(null)
48
- let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
49
-
50
- function setOpen(v: boolean) { if (v !== open) { open = v; onOpenChange?.(v) } }
51
- function toggle() { setOpen(!open) }
52
- function updatePos() {
53
- if (!anchorEl) return
54
- rect = anchoredRect(anchorEl.getBoundingClientRect(), { estimatedHeight: Math.min(items.length, 10) * 34 + 8, minWidth: 180 })
55
- }
56
- /** Return focus to the trigger. Every dismissal path (Escape, item selection,
57
- * outside-click, Tab) funnels through the effect cleanup below, so this
58
- * fires uniformly instead of dropping focus to <body> on close. */
59
- function focusTrigger() {
60
- if (!anchorEl) return
61
- ;(getFocusable(anchorEl)[0] ?? anchorEl)?.focus?.()
62
- }
63
-
64
- $effect(() => {
65
- if (!open) return
66
- updatePos()
67
- queueMicrotask(() => panelEl?.querySelector<HTMLElement>('[role="menuitem"]:not([disabled])')?.focus())
68
- const rp = () => updatePos()
69
- window.addEventListener('scroll', rp, true)
70
- window.addEventListener('resize', rp)
71
- // Outside-click dismissal via the shared layer stack (Escape stays owned by
72
- // SvMenuList's keyboard handling, so closeOnEscape is off here).
73
- const layer = createDismissableLayer({
74
- element: () => [anchorEl, panelEl],
75
- onDismiss: () => setOpen(false),
76
- closeOnEscape: false,
77
- })
78
- layer.activate()
79
- return () => {
80
- window.removeEventListener('scroll', rp, true)
81
- window.removeEventListener('resize', rp)
82
- layer.release()
83
- // Only on an actual close (not a mid-open rerun from e.g. `items` changing).
84
- if (!open) focusTrigger()
85
- }
86
- })
87
- </script>
88
-
89
- <!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
90
- <span
91
- bind:this={anchorEl}
92
- class="sv-menu__anchor"
93
- aria-haspopup="menu"
94
- aria-expanded={open}
95
- onclick={toggle}
96
- >
97
- {@render anchor?.()}
98
- </span>
99
-
100
- {#if open}
101
- <div
102
- bind:this={panelEl}
103
- class="sv-menu"
104
- use:portalToBody
105
- use:popIn={{ up: rect.openUpward }}
106
- style:position="fixed"
107
- style:top={`${rect.top}px`}
108
- style:left={`${rect.left}px`}
109
- dir={resolvedDir}
110
- aria-label={ariaLabel}
111
- >
112
- <SvMenuList {items} onclose={() => setOpen(false)} onselect={(i) => onSelect?.(i)} dir={resolvedDir} />
113
- </div>
114
- {/if}
115
-
116
- <style>
117
- .sv-menu__anchor { display: inline-flex; }
118
- :global(.sv-menu) {
119
- z-index: 2147483646; min-width: 180px;
120
- background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
121
- border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
122
- box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px;
123
- }
124
- </style>
1
+ <script lang="ts" module>
2
+ export type { MenuItem } from './SvMenuList.svelte'
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ /**
7
+ * SvMenu - a dropdown / context menu: a trigger opens a portalled, animated
8
+ * menu surface with submenus, separators, icons, shortcuts and full keyboard
9
+ * (arrows, Enter, Escape, ArrowRight/Left for submenus). Portals to <body> so it
10
+ * escapes any clipping. Parity: Smart menu / context-menu.
11
+ *
12
+ * ```svelte
13
+ * <SvMenu items={items} onSelect={(i) => run(i)}>
14
+ * {#snippet anchor()}<button>Actions</button>{/snippet}
15
+ * </SvMenu>
16
+ * ```
17
+ */
18
+ import type { Snippet } from 'svelte'
19
+ import { anchoredRect, portalToBody, popIn, type AnchoredRect } from './popover'
20
+ import { createDismissableLayer } from './a11y/dismissable'
21
+ import { getFocusable } from './a11y/focus-trap'
22
+ import { type EditorDir } from './editor-contract'
23
+ import SvMenuList, { type MenuItem } from './SvMenuList.svelte'
24
+
25
+ let {
26
+ items,
27
+ open = $bindable(false),
28
+ onOpenChange,
29
+ onSelect,
30
+ ariaLabel,
31
+ anchor,
32
+ dir,
33
+ }: {
34
+ items: ReadonlyArray<MenuItem>
35
+ open?: boolean
36
+ onOpenChange?: (open: boolean) => void
37
+ onSelect?: (item: MenuItem) => void
38
+ ariaLabel?: string
39
+ anchor?: Snippet
40
+ /** Text direction (rtl mirrors the submenu chevron + flips ArrowLeft/Right). */
41
+ dir?: EditorDir
42
+ } = $props()
43
+
44
+ const resolvedDir = $derived(dir === 'ltr' || dir === 'rtl' ? dir : undefined)
45
+
46
+ let anchorEl = $state<HTMLSpanElement | null>(null)
47
+ let panelEl = $state<HTMLDivElement | null>(null)
48
+ let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 })
49
+
50
+ function setOpen(v: boolean) { if (v !== open) { open = v; onOpenChange?.(v) } }
51
+ function toggle() { setOpen(!open) }
52
+ function updatePos() {
53
+ if (!anchorEl) return
54
+ rect = anchoredRect(anchorEl.getBoundingClientRect(), { estimatedHeight: Math.min(items.length, 10) * 34 + 8, minWidth: 180 })
55
+ }
56
+ /** Return focus to the trigger. Every dismissal path (Escape, item selection,
57
+ * outside-click, Tab) funnels through the effect cleanup below, so this
58
+ * fires uniformly instead of dropping focus to <body> on close. */
59
+ function focusTrigger() {
60
+ if (!anchorEl) return
61
+ ;(getFocusable(anchorEl)[0] ?? anchorEl)?.focus?.()
62
+ }
63
+
64
+ $effect(() => {
65
+ if (!open) return
66
+ updatePos()
67
+ queueMicrotask(() => panelEl?.querySelector<HTMLElement>('[role="menuitem"]:not([disabled])')?.focus())
68
+ const rp = () => updatePos()
69
+ window.addEventListener('scroll', rp, true)
70
+ window.addEventListener('resize', rp)
71
+ // Outside-click dismissal via the shared layer stack (Escape stays owned by
72
+ // SvMenuList's keyboard handling, so closeOnEscape is off here).
73
+ const layer = createDismissableLayer({
74
+ element: () => [anchorEl, panelEl],
75
+ onDismiss: () => setOpen(false),
76
+ closeOnEscape: false,
77
+ })
78
+ layer.activate()
79
+ return () => {
80
+ window.removeEventListener('scroll', rp, true)
81
+ window.removeEventListener('resize', rp)
82
+ layer.release()
83
+ // Only on an actual close (not a mid-open rerun from e.g. `items` changing).
84
+ if (!open) focusTrigger()
85
+ }
86
+ })
87
+ </script>
88
+
89
+ <!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
90
+ <span
91
+ bind:this={anchorEl}
92
+ class="sv-menu__anchor"
93
+ aria-haspopup="menu"
94
+ aria-expanded={open}
95
+ onclick={toggle}
96
+ >
97
+ {@render anchor?.()}
98
+ </span>
99
+
100
+ {#if open}
101
+ <div
102
+ bind:this={panelEl}
103
+ class="sv-menu"
104
+ use:portalToBody
105
+ use:popIn={{ up: rect.openUpward }}
106
+ style:position="fixed"
107
+ style:top={`${rect.top}px`}
108
+ style:left={`${rect.left}px`}
109
+ dir={resolvedDir}
110
+ aria-label={ariaLabel}
111
+ >
112
+ <SvMenuList {items} onclose={() => setOpen(false)} onselect={(i) => onSelect?.(i)} dir={resolvedDir} />
113
+ </div>
114
+ {/if}
115
+
116
+ <style>
117
+ .sv-menu__anchor { display: inline-flex; }
118
+ :global(.sv-menu) {
119
+ z-index: 2147483646; min-width: 180px;
120
+ background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
121
+ border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
122
+ box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px;
123
+ }
124
+ </style>