@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
package/src/test-setup.ts CHANGED
@@ -1,62 +1,62 @@
1
- // Vitest setup. jsdom doesn't ship ResizeObserver, IntersectionObserver,
2
- // or the layout APIs SvGrid touches inside its mount effects. Provide
3
- // minimal no-op stubs so component mounting completes without crashing.
4
- //
5
- // These assign to globals/prototypes the jsdom env doesn't fully type, so
6
- // the targets are cast to `any`. We deliberately avoid expect-error
7
- // suppression directives: when a stub happens to line up with the lib types,
8
- // such a directive becomes "unused" and svelte-check fails the build (an
9
- // unused expect-error directive is itself an error).
10
-
11
- if (typeof globalThis.ResizeObserver === 'undefined') {
12
- ;(globalThis as any).ResizeObserver = class {
13
- observe() {}
14
- unobserve() {}
15
- disconnect() {}
16
- }
17
- }
18
-
19
- if (typeof globalThis.IntersectionObserver === 'undefined') {
20
- ;(globalThis as any).IntersectionObserver = class {
21
- observe() {}
22
- unobserve() {}
23
- disconnect() {}
24
- takeRecords() {
25
- return []
26
- }
27
- }
28
- }
29
-
30
- // jsdom's HTMLElement.scrollIntoView is a no-op; some grid code calls it
31
- // during the first effect. Make sure the method exists on every element.
32
- if (typeof Element !== 'undefined' && !Element.prototype.scrollIntoView) {
33
- ;(Element.prototype as any).scrollIntoView = function () {}
34
- }
35
-
36
- // jsdom returns 0 for offset* and getBoundingClientRect; the grid only uses
37
- // these for visual layout (column widths, virtualization windowing), so the
38
- // zeros are harmless for behavioral tests.
39
-
40
- // jsdom lacks the Web Animations API that Svelte 5 transitions (e.g. the chart
41
- // drawer's slide-in) drive via element.animate(). Provide a no-op so mounting
42
- // transitioned components doesn't throw "element.animate is not a function".
43
- if (typeof Element !== 'undefined' && !Element.prototype.animate) {
44
- ;(Element.prototype as any).animate = function () {
45
- return {
46
- cancel() {},
47
- finish() {},
48
- play() {},
49
- pause() {},
50
- reverse() {},
51
- addEventListener() {},
52
- removeEventListener() {},
53
- finished: Promise.resolve(),
54
- currentTime: 0,
55
- playState: 'finished',
56
- onfinish: null,
57
- oncancel: null,
58
- }
59
- }
60
- }
61
-
62
- export {}
1
+ // Vitest setup. jsdom doesn't ship ResizeObserver, IntersectionObserver,
2
+ // or the layout APIs SvGrid touches inside its mount effects. Provide
3
+ // minimal no-op stubs so component mounting completes without crashing.
4
+ //
5
+ // These assign to globals/prototypes the jsdom env doesn't fully type, so
6
+ // the targets are cast to `any`. We deliberately avoid expect-error
7
+ // suppression directives: when a stub happens to line up with the lib types,
8
+ // such a directive becomes "unused" and svelte-check fails the build (an
9
+ // unused expect-error directive is itself an error).
10
+
11
+ if (typeof globalThis.ResizeObserver === 'undefined') {
12
+ ;(globalThis as any).ResizeObserver = class {
13
+ observe() {}
14
+ unobserve() {}
15
+ disconnect() {}
16
+ }
17
+ }
18
+
19
+ if (typeof globalThis.IntersectionObserver === 'undefined') {
20
+ ;(globalThis as any).IntersectionObserver = class {
21
+ observe() {}
22
+ unobserve() {}
23
+ disconnect() {}
24
+ takeRecords() {
25
+ return []
26
+ }
27
+ }
28
+ }
29
+
30
+ // jsdom's HTMLElement.scrollIntoView is a no-op; some grid code calls it
31
+ // during the first effect. Make sure the method exists on every element.
32
+ if (typeof Element !== 'undefined' && !Element.prototype.scrollIntoView) {
33
+ ;(Element.prototype as any).scrollIntoView = function () {}
34
+ }
35
+
36
+ // jsdom returns 0 for offset* and getBoundingClientRect; the grid only uses
37
+ // these for visual layout (column widths, virtualization windowing), so the
38
+ // zeros are harmless for behavioral tests.
39
+
40
+ // jsdom lacks the Web Animations API that Svelte 5 transitions (e.g. the chart
41
+ // drawer's slide-in) drive via element.animate(). Provide a no-op so mounting
42
+ // transitioned components doesn't throw "element.animate is not a function".
43
+ if (typeof Element !== 'undefined' && !Element.prototype.animate) {
44
+ ;(Element.prototype as any).animate = function () {
45
+ return {
46
+ cancel() {},
47
+ finish() {},
48
+ play() {},
49
+ pause() {},
50
+ reverse() {},
51
+ addEventListener() {},
52
+ removeEventListener() {},
53
+ finished: Promise.resolve(),
54
+ currentTime: 0,
55
+ playState: 'finished',
56
+ onfinish: null,
57
+ oncancel: null,
58
+ }
59
+ }
60
+ }
61
+
62
+ export {}
@@ -1,159 +1,159 @@
1
- /**
2
- * SvGrid's built-in design-system theme presets (shadcn/ui, Tailwind, Material 3,
3
- * Excel, Fluent 2, ...). Each preset carries an explicit light + dark palette;
4
- * resolving one (plus a light/dark mode) yields the full `--sg-*` token set that
5
- * restyles the whole grid - not just the accent.
6
- *
7
- * Pure, framework-agnostic data + helpers - safe to import from Node or the
8
- * browser. `@svgrid/enterprise`'s Studio layer builds its `ProjectTheme`-aware
9
- * wrapper on top of this module instead of keeping its own copy.
10
- */
11
-
12
- export type ThemeMode = 'light' | 'dark'
13
-
14
- /** The core palette for one mode. */
15
- export type ThemePalette = {
16
- bg: string; fg: string; muted: string; border: string
17
- headerBg: string; headerFg: string; accent: string
18
- rowAlt: string; rowHover: string; selectionBg: string
19
- }
20
-
21
- export type ThemePreset = {
22
- /** Stable id, e.g. used as a CSS file name (`themes/<id>.css`). */
23
- id: string
24
- /** Display name shown on a theme picker. */
25
- name: string
26
- /** Control corner rounding (px). */
27
- radius: number
28
- /** Font stack applied to the app surface. */
29
- font: string
30
- light: ThemePalette
31
- dark: ThemePalette
32
- }
33
-
34
- /** The built-in theme presets, in gallery order (default first). */
35
- export const themePresets: ThemePreset[] = [
36
- { id: 'shadcn', name: 'shadcn/ui', radius: 6, font: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
37
- light: { bg: '#ffffff', fg: '#09090b', muted: '#71717a', border: '#e4e4e7', headerBg: '#fafafa', headerFg: '#71717a', accent: '#18181b', rowAlt: '#ffffff', rowHover: '#f4f4f5', selectionBg: '#f4f4f5' },
38
- dark: { bg: '#0a0a0a', fg: '#fafafa', muted: '#a1a1aa', border: '#27272a', headerBg: '#18181b', headerFg: '#a1a1aa', accent: '#fafafa', rowAlt: '#0a0a0a', rowHover: '#18181b', selectionBg: '#27272a' } },
39
- { id: 'tailwind', name: 'Tailwind', radius: 6, font: 'Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
40
- light: { bg: '#ffffff', fg: '#0f172a', muted: '#64748b', border: '#e2e8f0', headerBg: '#f8fafc', headerFg: '#0f172a', accent: '#4f46e5', rowAlt: '#ffffff', rowHover: '#f1f5f9', selectionBg: '#e0e7ff' },
41
- dark: { bg: '#0f172a', fg: '#f8fafc', muted: '#94a3b8', border: '#334155', headerBg: '#1e293b', headerFg: '#f8fafc', accent: '#818cf8', rowAlt: '#0f172a', rowHover: '#1e293b', selectionBg: '#312e81' } },
42
- { id: 'material', name: 'Material 3', radius: 8, font: '"Roboto Flex", Roboto, "Segoe UI", system-ui, sans-serif',
43
- light: { bg: '#fef7ff', fg: '#1d1b20', muted: '#49454f', border: '#cac4d0', headerBg: '#f3edf7', headerFg: '#1d1b20', accent: '#6750a4', rowAlt: '#fffbfe', rowHover: '#ece6f0', selectionBg: '#e8def8' },
44
- dark: { bg: '#141218', fg: '#e6e0e9', muted: '#cac4d0', border: '#49454f', headerBg: '#211f26', headerFg: '#e6e0e9', accent: '#d0bcff', rowAlt: '#141218', rowHover: '#2b2930', selectionBg: '#4a4458' } },
45
- { id: 'excel', name: 'Excel', radius: 0, font: '"Aptos", "Segoe UI", Calibri, Arial, sans-serif',
46
- light: { bg: '#ffffff', fg: '#323130', muted: '#605e5c', border: '#d2d0ce', headerBg: '#f3f2f1', headerFg: '#444444', accent: '#107c41', rowAlt: '#ffffff', rowHover: '#edebe9', selectionBg: 'rgba(16, 124, 65, 0.10)' },
47
- dark: { bg: '#1b1a19', fg: '#f3f2f1', muted: '#c8c6c4', border: '#3b3a39', headerBg: '#252423', headerFg: '#c8c6c4', accent: '#58c188', rowAlt: '#1b1a19', rowHover: '#323130', selectionBg: 'rgba(88, 193, 136, 0.16)' } },
48
- { id: 'fluent', name: 'Fluent 2', radius: 4, font: '"Segoe UI Variable", "Segoe UI", system-ui, sans-serif',
49
- light: { bg: '#ffffff', fg: '#242424', muted: '#616161', border: '#e0e0e0', headerBg: '#f5f5f5', headerFg: '#242424', accent: '#0f6cbd', rowAlt: '#fafafa', rowHover: '#f0f0f0', selectionBg: '#ebebeb' },
50
- dark: { bg: '#1f1f1f', fg: '#ffffff', muted: '#d6d6d6', border: '#666666', headerBg: '#141414', headerFg: '#ffffff', accent: '#479ef5', rowAlt: '#1a1a1a', rowHover: '#3d3d3d', selectionBg: '#383838' } },
51
- { id: 'carbon', name: 'Carbon', radius: 0, font: '"IBM Plex Sans", "Helvetica Neue", Arial, sans-serif',
52
- light: { bg: '#ffffff', fg: '#161616', muted: '#525252', border: '#e0e0e0', headerBg: '#e0e0e0', headerFg: '#161616', accent: '#0f62fe', rowAlt: '#f4f4f4', rowHover: '#e8e8e8', selectionBg: '#d0e2ff' },
53
- dark: { bg: '#161616', fg: '#f4f4f4', muted: '#c6c6c6', border: '#393939', headerBg: '#393939', headerFg: '#f4f4f4', accent: '#78a9ff', rowAlt: '#262626', rowHover: '#333333', selectionBg: '#393939' } },
54
- { id: 'sap', name: 'SAP Fiori', radius: 6, font: '"72", "72full", "Arial", sans-serif',
55
- light: { bg: '#f5f6f7', fg: '#131e29', muted: '#556b82', border: '#e5e5e5', headerBg: '#ffffff', headerFg: '#131e29', accent: '#0070f2', rowAlt: '#f5f6f7', rowHover: '#eaecee', selectionBg: '#ebf8ff' },
56
- dark: { bg: '#12171c', fg: '#f5f6f7', muted: '#8396a8', border: '#2e3742', headerBg: '#1d232a', headerFg: '#f5f6f7', accent: '#4db1ff', rowAlt: '#12171c', rowHover: '#222b35', selectionBg: '#1d2d3e' } },
57
- { id: 'salesforce', name: 'Salesforce', radius: 4, font: '"Salesforce Sans", "SF Pro", Helvetica, Arial, sans-serif',
58
- light: { bg: '#ffffff', fg: '#181818', muted: '#757575', border: '#c9c9c9', headerBg: '#f3f3f3', headerFg: '#181818', accent: '#0176d3', rowAlt: '#ffffff', rowHover: '#f3f3f3', selectionBg: '#ecebea' },
59
- dark: { bg: '#181818', fg: '#ffffff', muted: '#c9c9c9', border: '#3e3e3c', headerBg: '#2e2e2e', headerFg: '#ffffff', accent: '#1b96ff', rowAlt: '#181818', rowHover: '#2e2e2e', selectionBg: '#1e4d8e' } },
60
- { id: 'atlassian', name: 'Atlassian', radius: 3, font: '"Charlie Display", "Atlassian Sans", -apple-system, "Segoe UI", sans-serif',
61
- light: { bg: '#ffffff', fg: '#172b4d', muted: '#44546f', border: '#dcdfe4', headerBg: '#fafbfc', headerFg: '#44546f', accent: '#0c66e4', rowAlt: '#ffffff', rowHover: '#f1f2f4', selectionBg: '#e9f2ff' },
62
- dark: { bg: '#1d2125', fg: '#b6c2cf', muted: '#8590a2', border: '#38414a', headerBg: '#22272b', headerFg: '#8590a2', accent: '#579dff', rowAlt: '#1d2125', rowHover: '#2c333a', selectionBg: '#09326c' } },
63
- { id: 'github', name: 'GitHub', radius: 6, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif',
64
- light: { bg: '#ffffff', fg: '#1f2328', muted: '#59636e', border: '#d1d9e0', headerBg: '#f6f8fa', headerFg: '#59636e', accent: '#0969da', rowAlt: '#ffffff', rowHover: '#f6f8fa', selectionBg: '#ddf4ff' },
65
- dark: { bg: '#0d1117', fg: '#f0f6fc', muted: '#9198a1', border: '#3d444d', headerBg: '#151b23', headerFg: '#9198a1', accent: '#4493f8', rowAlt: '#0d1117', rowHover: '#151b23', selectionBg: 'rgba(56, 139, 253, 0.10)' } },
66
- { id: 'antd', name: 'Ant Design', radius: 6, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", sans-serif',
67
- light: { bg: '#ffffff', fg: 'rgba(0, 0, 0, 0.88)', muted: 'rgba(0, 0, 0, 0.65)', border: '#d9d9d9', headerBg: '#fafafa', headerFg: 'rgba(0, 0, 0, 0.88)', accent: '#1677ff', rowAlt: '#ffffff', rowHover: '#f5f5f5', selectionBg: '#e6f4ff' },
68
- dark: { bg: '#141414', fg: 'rgba(255, 255, 255, 0.85)', muted: 'rgba(255, 255, 255, 0.65)', border: '#424242', headerBg: '#1f1f1f', headerFg: 'rgba(255, 255, 255, 0.85)', accent: '#177ddc', rowAlt: '#141414', rowHover: '#262626', selectionBg: '#111a2c' } },
69
- { id: 'ag-alpine', name: 'Alpine', radius: 3, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
70
- light: { bg: '#ffffff', fg: '#181d1f', muted: 'rgba(24, 29, 31, 0.5)', border: '#babfc7', headerBg: '#f8f8f8', headerFg: '#181d1f', accent: '#2196f3', rowAlt: '#fcfcfc', rowHover: 'rgba(33, 150, 243, 0.1)', selectionBg: 'rgba(33, 150, 243, 0.3)' },
71
- dark: { bg: '#181d1f', fg: '#ffffff', muted: 'rgba(255, 255, 255, 0.5)', border: '#68686e', headerBg: '#222628', headerFg: '#ffffff', accent: '#2196f3', rowAlt: '#222628', rowHover: 'rgba(33, 150, 243, 0.1)', selectionBg: 'rgba(33, 150, 243, 0.3)' } },
72
- { id: 'bootstrap', name: 'Bootstrap 5', radius: 6, font: 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
73
- light: { bg: '#ffffff', fg: '#212529', muted: '#6c757d', border: '#dee2e6', headerBg: '#f8f9fa', headerFg: '#212529', accent: '#0d6efd', rowAlt: '#fcfcfd', rowHover: '#f2f2f2', selectionBg: '#cfe2ff' },
74
- dark: { bg: '#212529', fg: '#dee2e6', muted: '#adb5bd', border: '#495057', headerBg: '#2b3035', headerFg: '#dee2e6', accent: '#0d6efd', rowAlt: '#25292e', rowHover: '#2c3034', selectionBg: '#031633' } },
75
- { id: 'vercel', name: 'Vercel', radius: 6, font: '"Geist", "Geist Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
76
- light: { bg: '#ffffff', fg: '#000000', muted: '#666666', border: '#eaeaea', headerBg: '#fafafa', headerFg: '#666666', accent: '#0070f3', rowAlt: '#fafafa', rowHover: '#f5f5f5', selectionBg: '#e8f0fe' },
77
- dark: { bg: '#000000', fg: '#ffffff', muted: '#888888', border: '#333333', headerBg: '#111111', headerFg: '#888888', accent: '#3291ff', rowAlt: '#0a0a0a', rowHover: '#1a1a1a', selectionBg: '#0d2847' } },
78
- { id: 'linear', name: 'Linear', radius: 6, font: '"Inter Variable", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
79
- light: { bg: '#ffffff', fg: '#282a30', muted: '#6b6f76', border: '#e9e9eb', headerBg: '#f7f8f8', headerFg: '#6b6f76', accent: '#5e6ad2', rowAlt: '#fbfbfc', rowHover: '#f4f4f5', selectionBg: '#e8eafd' },
80
- dark: { bg: '#08090a', fg: '#f7f8f8', muted: '#8a8f98', border: '#23252a', headerBg: '#0f1011', headerFg: '#8a8f98', accent: '#5e6ad2', rowAlt: '#0f1011', rowHover: '#1a1b1e', selectionBg: '#2a2d4a' } },
81
- { id: 'notion', name: 'Notion', radius: 4, font: 'ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif',
82
- light: { bg: '#ffffff', fg: '#37352f', muted: '#787774', border: '#e9e9e7', headerBg: '#f7f6f3', headerFg: '#787774', accent: '#2383e2', rowAlt: '#fbfbfa', rowHover: '#f1f1ef', selectionBg: '#d9eaf7' },
83
- dark: { bg: '#191919', fg: '#d4d4d4', muted: '#9b9b9b', border: '#2f2f2f', headerBg: '#202020', headerFg: '#9b9b9b', accent: '#529cca', rowAlt: '#1c1c1c', rowHover: '#252525', selectionBg: '#20415c' } },
84
- { id: 'nord', name: 'Nord', radius: 4, font: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
85
- light: { bg: '#eceff4', fg: '#2e3440', muted: '#4c566a', border: '#d8dee9', headerBg: '#e5e9f0', headerFg: '#2e3440', accent: '#5e81ac', rowAlt: '#e5e9f0', rowHover: '#dde3ec', selectionBg: '#d8dee9' },
86
- dark: { bg: '#2e3440', fg: '#d8dee9', muted: '#4c566a', border: '#434c5e', headerBg: '#3b4252', headerFg: '#e5e9f0', accent: '#88c0d0', rowAlt: '#333b48', rowHover: '#3b4252', selectionBg: '#434c5e' } },
87
- { id: 'dracula', name: 'Dracula', radius: 6, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
88
- light: { bg: '#fffbeb', fg: '#1f1f1f', muted: '#6c664b', border: '#dedccf', headerBg: '#ece9df', headerFg: '#1f1f1f', accent: '#644ac9', rowAlt: '#fffdf5', rowHover: '#f2efe3', selectionBg: '#cfcfde' },
89
- dark: { bg: '#282a36', fg: '#f8f8f2', muted: '#6272a4', border: '#44475a', headerBg: '#21222c', headerFg: '#f8f8f2', accent: '#bd93f9', rowAlt: '#2d2f3b', rowHover: '#343746', selectionBg: '#44475a' } },
90
- { id: 'catppuccin', name: 'Catppuccin', radius: 8, font: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
91
- light: { bg: '#eff1f5', fg: '#4c4f69', muted: '#6c6f85', border: '#ccd0da', headerBg: '#e6e9ef', headerFg: '#4c4f69', accent: '#8839ef', rowAlt: '#e6e9ef', rowHover: '#dce0e8', selectionBg: '#dce0e8' },
92
- dark: { bg: '#1e1e2e', fg: '#cdd6f4', muted: '#a6adc8', border: '#313244', headerBg: '#181825', headerFg: '#cdd6f4', accent: '#cba6f7', rowAlt: '#1a1a28', rowHover: '#313244', selectionBg: '#45475a' } },
93
- ]
94
-
95
- /** The default preset (used when nothing is chosen). */
96
- export const defaultThemePreset: ThemePreset = themePresets.find((t) => t.id === 'tailwind') ?? themePresets[0]!
97
-
98
- /** Look up a preset by id. */
99
- export const getThemePreset = (id?: string): ThemePreset | undefined =>
100
- id ? themePresets.find((t) => t.id === id) : undefined
101
-
102
- /** A readable (near-black / white) foreground for text placed on the accent. */
103
- function onAccent(accent: string): string {
104
- const m = /^#?([0-9a-f]{6})$/i.exec(accent.trim())
105
- if (!m) return '#ffffff'
106
- const n = parseInt(m[1]!, 16)
107
- const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255
108
- // Perceived luminance (sRGB, quick approximation).
109
- const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255
110
- return lum > 0.6 ? '#18181b' : '#ffffff'
111
- }
112
-
113
- /** Resolve the effective `--sg-*` tokens for a preset in a given mode, with an
114
- * optional accent override applied. */
115
- export function resolveThemeTokens(preset: ThemePreset | undefined, mode: ThemeMode, accentOverride?: string): Record<string, string> {
116
- const resolved = preset ?? defaultThemePreset
117
- const p = resolved[mode]
118
- const accent = accentOverride || p.accent
119
- return {
120
- '--sg-bg': p.bg,
121
- '--sg-fg': p.fg,
122
- '--sg-muted': p.muted,
123
- '--sg-border': p.border,
124
- '--sg-header-bg': p.headerBg,
125
- '--sg-header-fg': p.headerFg,
126
- // A subtle inset surface (used by e.g. the master-detail region behind the
127
- // nested grid). Themed so it isn't a fixed light grey on dark presets.
128
- '--sg-bg-subtle': p.headerBg,
129
- '--sg-row-alt-bg': p.rowAlt,
130
- '--sg-row-hover-bg': p.rowHover,
131
- '--sg-selection-bg': p.selectionBg,
132
- // Inputs/checkboxes: pin to the theme's own surface so they never borrow the
133
- // host page's input tokens (which is how a themed grid ended up with dark
134
- // checkboxes on a light page when the host mode differed).
135
- '--sg-input-bg': p.bg,
136
- '--sg-input-border': p.border,
137
- '--sg-accent': accent,
138
- '--sg-on-accent': onAccent(accent),
139
- '--sg-radius': `${resolved.radius}px`,
140
- '--sg-font': resolved.font,
141
- // The grid's custom scrollbar is a shadow-DOM web component that reads its
142
- // OWN --sg-scrollbar-* tokens (with fixed light-mode fallbacks). Without
143
- // these it ignores the theme entirely - a light scrollbar left on a dark
144
- // grid. CSS custom properties pierce shadow DOM, so defining them here (on
145
- // the themed container) reaches the scrollbar. Derive them from the palette
146
- // so the scrollbar tracks the preset + light/dark mode like everything else.
147
- '--sg-scrollbar-bg': p.headerBg,
148
- '--sg-scrollbar-border': p.border,
149
- '--sg-scrollbar-thumb': p.muted,
150
- '--sg-scrollbar-thumb-hover': p.fg,
151
- '--sg-scrollbar-thumb-active': p.fg,
152
- '--sg-scrollbar-arrow': p.muted,
153
- '--sg-scrollbar-arrow-hover': p.fg,
154
- '--sg-scrollbar-arrow-hover-bg': p.rowHover,
155
- '--sg-scrollbar-arrow-active': p.fg,
156
- '--sg-scrollbar-arrow-active-bg': p.selectionBg,
157
- '--sg-scrollbar-arrow-disabled': p.border,
158
- }
159
- }
1
+ /**
2
+ * SvGrid's built-in design-system theme presets (shadcn/ui, Tailwind, Material 3,
3
+ * Excel, Fluent 2, ...). Each preset carries an explicit light + dark palette;
4
+ * resolving one (plus a light/dark mode) yields the full `--sg-*` token set that
5
+ * restyles the whole grid - not just the accent.
6
+ *
7
+ * Pure, framework-agnostic data + helpers - safe to import from Node or the
8
+ * browser. `@svgrid/enterprise`'s Studio layer builds its `ProjectTheme`-aware
9
+ * wrapper on top of this module instead of keeping its own copy.
10
+ */
11
+
12
+ export type ThemeMode = 'light' | 'dark'
13
+
14
+ /** The core palette for one mode. */
15
+ export type ThemePalette = {
16
+ bg: string; fg: string; muted: string; border: string
17
+ headerBg: string; headerFg: string; accent: string
18
+ rowAlt: string; rowHover: string; selectionBg: string
19
+ }
20
+
21
+ export type ThemePreset = {
22
+ /** Stable id, e.g. used as a CSS file name (`themes/<id>.css`). */
23
+ id: string
24
+ /** Display name shown on a theme picker. */
25
+ name: string
26
+ /** Control corner rounding (px). */
27
+ radius: number
28
+ /** Font stack applied to the app surface. */
29
+ font: string
30
+ light: ThemePalette
31
+ dark: ThemePalette
32
+ }
33
+
34
+ /** The built-in theme presets, in gallery order (default first). */
35
+ export const themePresets: ThemePreset[] = [
36
+ { id: 'shadcn', name: 'shadcn/ui', radius: 6, font: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
37
+ light: { bg: '#ffffff', fg: '#09090b', muted: '#71717a', border: '#e4e4e7', headerBg: '#fafafa', headerFg: '#71717a', accent: '#18181b', rowAlt: '#ffffff', rowHover: '#f4f4f5', selectionBg: '#f4f4f5' },
38
+ dark: { bg: '#0a0a0a', fg: '#fafafa', muted: '#a1a1aa', border: '#27272a', headerBg: '#18181b', headerFg: '#a1a1aa', accent: '#fafafa', rowAlt: '#0a0a0a', rowHover: '#18181b', selectionBg: '#27272a' } },
39
+ { id: 'tailwind', name: 'Tailwind', radius: 6, font: 'Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
40
+ light: { bg: '#ffffff', fg: '#0f172a', muted: '#64748b', border: '#e2e8f0', headerBg: '#f8fafc', headerFg: '#0f172a', accent: '#4f46e5', rowAlt: '#ffffff', rowHover: '#f1f5f9', selectionBg: '#e0e7ff' },
41
+ dark: { bg: '#0f172a', fg: '#f8fafc', muted: '#94a3b8', border: '#334155', headerBg: '#1e293b', headerFg: '#f8fafc', accent: '#818cf8', rowAlt: '#0f172a', rowHover: '#1e293b', selectionBg: '#312e81' } },
42
+ { id: 'material', name: 'Material 3', radius: 8, font: '"Roboto Flex", Roboto, "Segoe UI", system-ui, sans-serif',
43
+ light: { bg: '#fef7ff', fg: '#1d1b20', muted: '#49454f', border: '#cac4d0', headerBg: '#f3edf7', headerFg: '#1d1b20', accent: '#6750a4', rowAlt: '#fffbfe', rowHover: '#ece6f0', selectionBg: '#e8def8' },
44
+ dark: { bg: '#141218', fg: '#e6e0e9', muted: '#cac4d0', border: '#49454f', headerBg: '#211f26', headerFg: '#e6e0e9', accent: '#d0bcff', rowAlt: '#141218', rowHover: '#2b2930', selectionBg: '#4a4458' } },
45
+ { id: 'excel', name: 'Excel', radius: 0, font: '"Aptos", "Segoe UI", Calibri, Arial, sans-serif',
46
+ light: { bg: '#ffffff', fg: '#323130', muted: '#605e5c', border: '#d2d0ce', headerBg: '#f3f2f1', headerFg: '#444444', accent: '#107c41', rowAlt: '#ffffff', rowHover: '#edebe9', selectionBg: 'rgba(16, 124, 65, 0.10)' },
47
+ dark: { bg: '#1b1a19', fg: '#f3f2f1', muted: '#c8c6c4', border: '#3b3a39', headerBg: '#252423', headerFg: '#c8c6c4', accent: '#58c188', rowAlt: '#1b1a19', rowHover: '#323130', selectionBg: 'rgba(88, 193, 136, 0.16)' } },
48
+ { id: 'fluent', name: 'Fluent 2', radius: 4, font: '"Segoe UI Variable", "Segoe UI", system-ui, sans-serif',
49
+ light: { bg: '#ffffff', fg: '#242424', muted: '#616161', border: '#e0e0e0', headerBg: '#f5f5f5', headerFg: '#242424', accent: '#0f6cbd', rowAlt: '#fafafa', rowHover: '#f0f0f0', selectionBg: '#ebebeb' },
50
+ dark: { bg: '#1f1f1f', fg: '#ffffff', muted: '#d6d6d6', border: '#666666', headerBg: '#141414', headerFg: '#ffffff', accent: '#479ef5', rowAlt: '#1a1a1a', rowHover: '#3d3d3d', selectionBg: '#383838' } },
51
+ { id: 'carbon', name: 'Carbon', radius: 0, font: '"IBM Plex Sans", "Helvetica Neue", Arial, sans-serif',
52
+ light: { bg: '#ffffff', fg: '#161616', muted: '#525252', border: '#e0e0e0', headerBg: '#e0e0e0', headerFg: '#161616', accent: '#0f62fe', rowAlt: '#f4f4f4', rowHover: '#e8e8e8', selectionBg: '#d0e2ff' },
53
+ dark: { bg: '#161616', fg: '#f4f4f4', muted: '#c6c6c6', border: '#393939', headerBg: '#393939', headerFg: '#f4f4f4', accent: '#78a9ff', rowAlt: '#262626', rowHover: '#333333', selectionBg: '#393939' } },
54
+ { id: 'sap', name: 'SAP Fiori', radius: 6, font: '"72", "72full", "Arial", sans-serif',
55
+ light: { bg: '#f5f6f7', fg: '#131e29', muted: '#556b82', border: '#e5e5e5', headerBg: '#ffffff', headerFg: '#131e29', accent: '#0070f2', rowAlt: '#f5f6f7', rowHover: '#eaecee', selectionBg: '#ebf8ff' },
56
+ dark: { bg: '#12171c', fg: '#f5f6f7', muted: '#8396a8', border: '#2e3742', headerBg: '#1d232a', headerFg: '#f5f6f7', accent: '#4db1ff', rowAlt: '#12171c', rowHover: '#222b35', selectionBg: '#1d2d3e' } },
57
+ { id: 'salesforce', name: 'Salesforce', radius: 4, font: '"Salesforce Sans", "SF Pro", Helvetica, Arial, sans-serif',
58
+ light: { bg: '#ffffff', fg: '#181818', muted: '#757575', border: '#c9c9c9', headerBg: '#f3f3f3', headerFg: '#181818', accent: '#0176d3', rowAlt: '#ffffff', rowHover: '#f3f3f3', selectionBg: '#ecebea' },
59
+ dark: { bg: '#181818', fg: '#ffffff', muted: '#c9c9c9', border: '#3e3e3c', headerBg: '#2e2e2e', headerFg: '#ffffff', accent: '#1b96ff', rowAlt: '#181818', rowHover: '#2e2e2e', selectionBg: '#1e4d8e' } },
60
+ { id: 'atlassian', name: 'Atlassian', radius: 3, font: '"Charlie Display", "Atlassian Sans", -apple-system, "Segoe UI", sans-serif',
61
+ light: { bg: '#ffffff', fg: '#172b4d', muted: '#44546f', border: '#dcdfe4', headerBg: '#fafbfc', headerFg: '#44546f', accent: '#0c66e4', rowAlt: '#ffffff', rowHover: '#f1f2f4', selectionBg: '#e9f2ff' },
62
+ dark: { bg: '#1d2125', fg: '#b6c2cf', muted: '#8590a2', border: '#38414a', headerBg: '#22272b', headerFg: '#8590a2', accent: '#579dff', rowAlt: '#1d2125', rowHover: '#2c333a', selectionBg: '#09326c' } },
63
+ { id: 'github', name: 'GitHub', radius: 6, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif',
64
+ light: { bg: '#ffffff', fg: '#1f2328', muted: '#59636e', border: '#d1d9e0', headerBg: '#f6f8fa', headerFg: '#59636e', accent: '#0969da', rowAlt: '#ffffff', rowHover: '#f6f8fa', selectionBg: '#ddf4ff' },
65
+ dark: { bg: '#0d1117', fg: '#f0f6fc', muted: '#9198a1', border: '#3d444d', headerBg: '#151b23', headerFg: '#9198a1', accent: '#4493f8', rowAlt: '#0d1117', rowHover: '#151b23', selectionBg: 'rgba(56, 139, 253, 0.10)' } },
66
+ { id: 'antd', name: 'Ant Design', radius: 6, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", sans-serif',
67
+ light: { bg: '#ffffff', fg: 'rgba(0, 0, 0, 0.88)', muted: 'rgba(0, 0, 0, 0.65)', border: '#d9d9d9', headerBg: '#fafafa', headerFg: 'rgba(0, 0, 0, 0.88)', accent: '#1677ff', rowAlt: '#ffffff', rowHover: '#f5f5f5', selectionBg: '#e6f4ff' },
68
+ dark: { bg: '#141414', fg: 'rgba(255, 255, 255, 0.85)', muted: 'rgba(255, 255, 255, 0.65)', border: '#424242', headerBg: '#1f1f1f', headerFg: 'rgba(255, 255, 255, 0.85)', accent: '#177ddc', rowAlt: '#141414', rowHover: '#262626', selectionBg: '#111a2c' } },
69
+ { id: 'ag-alpine', name: 'Alpine', radius: 3, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
70
+ light: { bg: '#ffffff', fg: '#181d1f', muted: 'rgba(24, 29, 31, 0.5)', border: '#babfc7', headerBg: '#f8f8f8', headerFg: '#181d1f', accent: '#2196f3', rowAlt: '#fcfcfc', rowHover: 'rgba(33, 150, 243, 0.1)', selectionBg: 'rgba(33, 150, 243, 0.3)' },
71
+ dark: { bg: '#181d1f', fg: '#ffffff', muted: 'rgba(255, 255, 255, 0.5)', border: '#68686e', headerBg: '#222628', headerFg: '#ffffff', accent: '#2196f3', rowAlt: '#222628', rowHover: 'rgba(33, 150, 243, 0.1)', selectionBg: 'rgba(33, 150, 243, 0.3)' } },
72
+ { id: 'bootstrap', name: 'Bootstrap 5', radius: 6, font: 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
73
+ light: { bg: '#ffffff', fg: '#212529', muted: '#6c757d', border: '#dee2e6', headerBg: '#f8f9fa', headerFg: '#212529', accent: '#0d6efd', rowAlt: '#fcfcfd', rowHover: '#f2f2f2', selectionBg: '#cfe2ff' },
74
+ dark: { bg: '#212529', fg: '#dee2e6', muted: '#adb5bd', border: '#495057', headerBg: '#2b3035', headerFg: '#dee2e6', accent: '#0d6efd', rowAlt: '#25292e', rowHover: '#2c3034', selectionBg: '#031633' } },
75
+ { id: 'vercel', name: 'Vercel', radius: 6, font: '"Geist", "Geist Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
76
+ light: { bg: '#ffffff', fg: '#000000', muted: '#666666', border: '#eaeaea', headerBg: '#fafafa', headerFg: '#666666', accent: '#0070f3', rowAlt: '#fafafa', rowHover: '#f5f5f5', selectionBg: '#e8f0fe' },
77
+ dark: { bg: '#000000', fg: '#ffffff', muted: '#888888', border: '#333333', headerBg: '#111111', headerFg: '#888888', accent: '#3291ff', rowAlt: '#0a0a0a', rowHover: '#1a1a1a', selectionBg: '#0d2847' } },
78
+ { id: 'linear', name: 'Linear', radius: 6, font: '"Inter Variable", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
79
+ light: { bg: '#ffffff', fg: '#282a30', muted: '#6b6f76', border: '#e9e9eb', headerBg: '#f7f8f8', headerFg: '#6b6f76', accent: '#5e6ad2', rowAlt: '#fbfbfc', rowHover: '#f4f4f5', selectionBg: '#e8eafd' },
80
+ dark: { bg: '#08090a', fg: '#f7f8f8', muted: '#8a8f98', border: '#23252a', headerBg: '#0f1011', headerFg: '#8a8f98', accent: '#5e6ad2', rowAlt: '#0f1011', rowHover: '#1a1b1e', selectionBg: '#2a2d4a' } },
81
+ { id: 'notion', name: 'Notion', radius: 4, font: 'ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif',
82
+ light: { bg: '#ffffff', fg: '#37352f', muted: '#787774', border: '#e9e9e7', headerBg: '#f7f6f3', headerFg: '#787774', accent: '#2383e2', rowAlt: '#fbfbfa', rowHover: '#f1f1ef', selectionBg: '#d9eaf7' },
83
+ dark: { bg: '#191919', fg: '#d4d4d4', muted: '#9b9b9b', border: '#2f2f2f', headerBg: '#202020', headerFg: '#9b9b9b', accent: '#529cca', rowAlt: '#1c1c1c', rowHover: '#252525', selectionBg: '#20415c' } },
84
+ { id: 'nord', name: 'Nord', radius: 4, font: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
85
+ light: { bg: '#eceff4', fg: '#2e3440', muted: '#4c566a', border: '#d8dee9', headerBg: '#e5e9f0', headerFg: '#2e3440', accent: '#5e81ac', rowAlt: '#e5e9f0', rowHover: '#dde3ec', selectionBg: '#d8dee9' },
86
+ dark: { bg: '#2e3440', fg: '#d8dee9', muted: '#4c566a', border: '#434c5e', headerBg: '#3b4252', headerFg: '#e5e9f0', accent: '#88c0d0', rowAlt: '#333b48', rowHover: '#3b4252', selectionBg: '#434c5e' } },
87
+ { id: 'dracula', name: 'Dracula', radius: 6, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
88
+ light: { bg: '#fffbeb', fg: '#1f1f1f', muted: '#6c664b', border: '#dedccf', headerBg: '#ece9df', headerFg: '#1f1f1f', accent: '#644ac9', rowAlt: '#fffdf5', rowHover: '#f2efe3', selectionBg: '#cfcfde' },
89
+ dark: { bg: '#282a36', fg: '#f8f8f2', muted: '#6272a4', border: '#44475a', headerBg: '#21222c', headerFg: '#f8f8f2', accent: '#bd93f9', rowAlt: '#2d2f3b', rowHover: '#343746', selectionBg: '#44475a' } },
90
+ { id: 'catppuccin', name: 'Catppuccin', radius: 8, font: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
91
+ light: { bg: '#eff1f5', fg: '#4c4f69', muted: '#6c6f85', border: '#ccd0da', headerBg: '#e6e9ef', headerFg: '#4c4f69', accent: '#8839ef', rowAlt: '#e6e9ef', rowHover: '#dce0e8', selectionBg: '#dce0e8' },
92
+ dark: { bg: '#1e1e2e', fg: '#cdd6f4', muted: '#a6adc8', border: '#313244', headerBg: '#181825', headerFg: '#cdd6f4', accent: '#cba6f7', rowAlt: '#1a1a28', rowHover: '#313244', selectionBg: '#45475a' } },
93
+ ]
94
+
95
+ /** The default preset (used when nothing is chosen). */
96
+ export const defaultThemePreset: ThemePreset = themePresets.find((t) => t.id === 'tailwind') ?? themePresets[0]!
97
+
98
+ /** Look up a preset by id. */
99
+ export const getThemePreset = (id?: string): ThemePreset | undefined =>
100
+ id ? themePresets.find((t) => t.id === id) : undefined
101
+
102
+ /** A readable (near-black / white) foreground for text placed on the accent. */
103
+ function onAccent(accent: string): string {
104
+ const m = /^#?([0-9a-f]{6})$/i.exec(accent.trim())
105
+ if (!m) return '#ffffff'
106
+ const n = parseInt(m[1]!, 16)
107
+ const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255
108
+ // Perceived luminance (sRGB, quick approximation).
109
+ const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255
110
+ return lum > 0.6 ? '#18181b' : '#ffffff'
111
+ }
112
+
113
+ /** Resolve the effective `--sg-*` tokens for a preset in a given mode, with an
114
+ * optional accent override applied. */
115
+ export function resolveThemeTokens(preset: ThemePreset | undefined, mode: ThemeMode, accentOverride?: string): Record<string, string> {
116
+ const resolved = preset ?? defaultThemePreset
117
+ const p = resolved[mode]
118
+ const accent = accentOverride || p.accent
119
+ return {
120
+ '--sg-bg': p.bg,
121
+ '--sg-fg': p.fg,
122
+ '--sg-muted': p.muted,
123
+ '--sg-border': p.border,
124
+ '--sg-header-bg': p.headerBg,
125
+ '--sg-header-fg': p.headerFg,
126
+ // A subtle inset surface (used by e.g. the master-detail region behind the
127
+ // nested grid). Themed so it isn't a fixed light grey on dark presets.
128
+ '--sg-bg-subtle': p.headerBg,
129
+ '--sg-row-alt-bg': p.rowAlt,
130
+ '--sg-row-hover-bg': p.rowHover,
131
+ '--sg-selection-bg': p.selectionBg,
132
+ // Inputs/checkboxes: pin to the theme's own surface so they never borrow the
133
+ // host page's input tokens (which is how a themed grid ended up with dark
134
+ // checkboxes on a light page when the host mode differed).
135
+ '--sg-input-bg': p.bg,
136
+ '--sg-input-border': p.border,
137
+ '--sg-accent': accent,
138
+ '--sg-on-accent': onAccent(accent),
139
+ '--sg-radius': `${resolved.radius}px`,
140
+ '--sg-font': resolved.font,
141
+ // The grid's custom scrollbar is a shadow-DOM web component that reads its
142
+ // OWN --sg-scrollbar-* tokens (with fixed light-mode fallbacks). Without
143
+ // these it ignores the theme entirely - a light scrollbar left on a dark
144
+ // grid. CSS custom properties pierce shadow DOM, so defining them here (on
145
+ // the themed container) reaches the scrollbar. Derive them from the palette
146
+ // so the scrollbar tracks the preset + light/dark mode like everything else.
147
+ '--sg-scrollbar-bg': p.headerBg,
148
+ '--sg-scrollbar-border': p.border,
149
+ '--sg-scrollbar-thumb': p.muted,
150
+ '--sg-scrollbar-thumb-hover': p.fg,
151
+ '--sg-scrollbar-thumb-active': p.fg,
152
+ '--sg-scrollbar-arrow': p.muted,
153
+ '--sg-scrollbar-arrow-hover': p.fg,
154
+ '--sg-scrollbar-arrow-hover-bg': p.rowHover,
155
+ '--sg-scrollbar-arrow-active': p.fg,
156
+ '--sg-scrollbar-arrow-active-bg': p.selectionBg,
157
+ '--sg-scrollbar-arrow-disabled': p.border,
158
+ }
159
+ }
@@ -72,6 +72,9 @@ export type TourStep = {
72
72
  target?: string | HTMLElement
73
73
  title?: string
74
74
  content: string
75
+ /** Optional illustration shown as a banner above the title. A URL / data-URI
76
+ * (rendered as an `<img>`) or inline SVG/HTML markup (author-controlled). */
77
+ image?: string
75
78
  }
76
79
 
77
80
  /** One entry in a timeline. */
@@ -48,10 +48,12 @@ describe('SvListBox virtual', () => {
48
48
  const rendered = target.querySelectorAll('.sv-listbox__opt').length
49
49
  expect(rendered).toBeGreaterThan(0)
50
50
  expect(rendered).toBeLessThan(60) // nowhere near 5000
51
- // A fixed-height sizer keeps the scrollbar honest.
52
- const sizer = target.querySelector<HTMLElement>('.sv-listbox__sizer')!
53
- expect(sizer).not.toBeNull()
54
- expect(sizer.style.height).toBe(`${5000 * 32}px`)
51
+ // Top + bottom spacers + the windowed rows reserve the full scroll height,
52
+ // keeping the scrollbar honest without positioning rows out of flow.
53
+ const spacers = target.querySelectorAll<HTMLElement>('.sv-listbox__spacer')
54
+ expect(spacers.length).toBe(2)
55
+ const spacerPx = [...spacers].reduce((sum, s) => sum + parseFloat(s.style.height || '0'), 0)
56
+ expect(spacerPx + rendered * 32).toBe(5000 * 32)
55
57
  } finally { unmount(app); target.remove() }
56
58
  })
57
59
 
@@ -1,27 +1,27 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { createColumnVirtualizer } from './column-virtualizer'
3
-
4
- describe('column virtualizer', () => {
5
- it('computes horizontal window', () => {
6
- const v = createColumnVirtualizer({
7
- count: 1000,
8
- viewportWidth: 700,
9
- overscan: 3,
10
- estimateSize: () => 140,
11
- })
12
- const items = v.getVirtualItems()
13
- expect(items[0]?.index).toBe(0)
14
- expect(items.length).toBeGreaterThan(0)
15
- })
16
-
17
- it('updates indices on horizontal scroll', () => {
18
- const v = createColumnVirtualizer({
19
- count: 1000,
20
- viewportWidth: 700,
21
- estimateSize: () => 140,
22
- })
23
- v.setHorizontalOffset(2800)
24
- const state = v.getState()
25
- expect(state.startIndex).toBeGreaterThan(0)
26
- })
27
- })
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createColumnVirtualizer } from './column-virtualizer'
3
+
4
+ describe('column virtualizer', () => {
5
+ it('computes horizontal window', () => {
6
+ const v = createColumnVirtualizer({
7
+ count: 1000,
8
+ viewportWidth: 700,
9
+ overscan: 3,
10
+ estimateSize: () => 140,
11
+ })
12
+ const items = v.getVirtualItems()
13
+ expect(items[0]?.index).toBe(0)
14
+ expect(items.length).toBeGreaterThan(0)
15
+ })
16
+
17
+ it('updates indices on horizontal scroll', () => {
18
+ const v = createColumnVirtualizer({
19
+ count: 1000,
20
+ viewportWidth: 700,
21
+ estimateSize: () => 140,
22
+ })
23
+ v.setHorizontalOffset(2800)
24
+ const state = v.getState()
25
+ expect(state.startIndex).toBeGreaterThan(0)
26
+ })
27
+ })
@@ -1,30 +1,30 @@
1
- import { createVirtualizer } from './virtualizer'
2
-
3
- export type ColumnSizeEstimator = (index: number) => number
4
-
5
- export function createColumnVirtualizer(input: {
6
- count: number
7
- viewportWidth: number
8
- scrollOffset?: number
9
- overscan?: number
10
- /** Either a uniform size (number) or a per-column size function. */
11
- estimateSize?: number | ColumnSizeEstimator
12
- }) {
13
- const virtualizer = createVirtualizer({
14
- count: input.count,
15
- estimateSize: input.estimateSize ?? 140,
16
- viewportHeight: input.viewportWidth,
17
- scrollOffset: input.scrollOffset ?? 0,
18
- overscan: input.overscan ?? 4,
19
- })
20
-
21
- return {
22
- ...virtualizer,
23
- setViewportWidth(viewportWidth: number) {
24
- virtualizer.setViewportHeight(viewportWidth)
25
- },
26
- setHorizontalOffset(scrollOffset: number) {
27
- virtualizer.setScrollOffset(scrollOffset)
28
- },
29
- }
30
- }
1
+ import { createVirtualizer } from './virtualizer'
2
+
3
+ export type ColumnSizeEstimator = (index: number) => number
4
+
5
+ export function createColumnVirtualizer(input: {
6
+ count: number
7
+ viewportWidth: number
8
+ scrollOffset?: number
9
+ overscan?: number
10
+ /** Either a uniform size (number) or a per-column size function. */
11
+ estimateSize?: number | ColumnSizeEstimator
12
+ }) {
13
+ const virtualizer = createVirtualizer({
14
+ count: input.count,
15
+ estimateSize: input.estimateSize ?? 140,
16
+ viewportHeight: input.viewportWidth,
17
+ scrollOffset: input.scrollOffset ?? 0,
18
+ overscan: input.overscan ?? 4,
19
+ })
20
+
21
+ return {
22
+ ...virtualizer,
23
+ setViewportWidth(viewportWidth: number) {
24
+ virtualizer.setViewportHeight(viewportWidth)
25
+ },
26
+ setHorizontalOffset(scrollOffset: number) {
27
+ virtualizer.setScrollOffset(scrollOffset)
28
+ },
29
+ }
30
+ }