@svgrid/grid 2.6.22 → 2.7.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 (331) hide show
  1. package/CHANGELOG.md +88 -88
  2. package/README.md +199 -199
  3. package/dist/FlexRender.svelte +96 -96
  4. package/dist/GridFooter.svelte +181 -181
  5. package/dist/GridMenus.svelte +184 -6
  6. package/dist/SvAutoComplete.svelte +169 -165
  7. package/dist/SvAvatar.svelte +75 -75
  8. package/dist/SvCalendar.svelte +503 -503
  9. package/dist/SvCarousel.svelte +141 -141
  10. package/dist/SvCheckBox.svelte +102 -102
  11. package/dist/SvCircularProgress.svelte +109 -109
  12. package/dist/SvColorInput.svelte +181 -181
  13. package/dist/SvComboBox.svelte +279 -275
  14. package/dist/SvContextMenu.svelte +116 -116
  15. package/dist/SvCountryInput.svelte +163 -163
  16. package/dist/SvDateRangeInput.svelte +25 -3
  17. package/dist/SvDrawer.svelte +254 -254
  18. package/dist/SvDropDownList.svelte +378 -374
  19. package/dist/SvDurationInput.svelte +126 -126
  20. package/dist/SvField.svelte +293 -293
  21. package/dist/SvForm.svelte +437 -437
  22. package/dist/SvGrid.controller.svelte.d.ts +48 -1
  23. package/dist/SvGrid.controller.svelte.js +214 -52
  24. package/dist/SvGrid.css +221 -0
  25. package/dist/SvGrid.helpers.d.ts +55 -0
  26. package/dist/SvGrid.helpers.js +83 -0
  27. package/dist/SvGrid.svelte +145 -0
  28. package/dist/SvGrid.types.d.ts +105 -0
  29. package/dist/SvGridCellEditor.svelte +744 -662
  30. package/dist/SvGridChart.svelte +1716 -1716
  31. package/dist/SvGridChartPanel.svelte +485 -485
  32. package/dist/SvGridChartView.svelte +70 -70
  33. package/dist/SvGridDropdown.svelte +728 -728
  34. package/dist/SvGridSelect.svelte +270 -266
  35. package/dist/SvGridSelect.svelte.d.ts +1 -1
  36. package/dist/SvGroupCell.svelte +116 -116
  37. package/dist/SvListBox.svelte +334 -326
  38. package/dist/SvMaskedInput.svelte +122 -122
  39. package/dist/SvMenu.svelte +124 -124
  40. package/dist/SvMenuList.svelte +146 -146
  41. package/dist/SvMultiSelect.svelte +293 -289
  42. package/dist/SvMultiSelect.svelte.d.ts +1 -1
  43. package/dist/SvNumberInput.svelte +172 -168
  44. package/dist/SvOtpInput.svelte +158 -158
  45. package/dist/SvPasswordInput.svelte +151 -151
  46. package/dist/SvPhoneInput.svelte +133 -133
  47. package/dist/SvPopover.svelte +197 -197
  48. package/dist/SvProgress.svelte +116 -116
  49. package/dist/SvRadioGroup.svelte +107 -107
  50. package/dist/SvRating.svelte +112 -112
  51. package/dist/SvResult.svelte +73 -73
  52. package/dist/SvRichCell.svelte +125 -0
  53. package/dist/SvRichCell.svelte.d.ts +15 -0
  54. package/dist/SvRichText.svelte +211 -211
  55. package/dist/SvRowGroupPanel.svelte +170 -170
  56. package/dist/SvScrollArea.svelte +61 -61
  57. package/dist/SvSlider.svelte +203 -203
  58. package/dist/SvSwitchButton.svelte +108 -108
  59. package/dist/SvTagsInput.svelte +115 -115
  60. package/dist/SvTextArea.svelte +5 -1
  61. package/dist/SvTextInput.svelte +147 -147
  62. package/dist/SvTimePicker.svelte +245 -245
  63. package/dist/SvToaster.svelte +159 -159
  64. package/dist/SvToggleButton.svelte +85 -85
  65. package/dist/SvTooltip.svelte +161 -161
  66. package/dist/SvTour.svelte +208 -208
  67. package/dist/SvTree.svelte +444 -444
  68. package/dist/SvTreeSelect.svelte +239 -235
  69. package/dist/SvTreeSelect.svelte.d.ts +1 -1
  70. package/dist/builtin-editors.d.ts +2 -2
  71. package/dist/builtin-editors.js +19 -3
  72. package/dist/cdn/GridMenus-C4oLsJyj.js +600 -0
  73. package/dist/cdn/GridMenus-yEivjNwl.js +601 -0
  74. package/dist/cdn/SvDateRangeInput-Bh0A0JkF.js +199 -0
  75. package/dist/cdn/SvDateRangeInput-DflbiP7N.js +198 -0
  76. package/dist/cdn/SvDateTimePicker-BWwpfB_o.js +812 -0
  77. package/dist/cdn/SvDateTimePicker-Bivn8dAP.js +816 -0
  78. package/dist/cdn/SvGridCellEditor-AXL8cHGO.js +550 -0
  79. package/dist/cdn/SvGridCellEditor-Ba7rY3eu.js +549 -0
  80. package/dist/cdn/{SvGridChart-BwVos976.js → SvGridChart-Bs2GIR2Q.js} +1 -1
  81. package/dist/cdn/{SvGridChart-BEJmNNx9.js → SvGridChart-CwhFz7GV.js} +1 -1
  82. package/dist/cdn/{SvGridChartPanel-BOknOkrP.js → SvGridChartPanel-1WXuStzM.js} +1 -1
  83. package/dist/cdn/{SvGridChartPanel-D2PjII4O.js → SvGridChartPanel-e5_nqNP9.js} +1 -1
  84. package/dist/cdn/{SvGridChartView-00LPUHL1.js → SvGridChartView-SmPW10dI.js} +1 -1
  85. package/dist/cdn/{SvGridChartView-BhUEJ5ki.js → SvGridChartView-eSPuJE6g.js} +1 -1
  86. package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-B1ZcLpgs.js} +1 -1
  87. package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CtdbHcIS.js} +1 -1
  88. package/dist/cdn/date-format-CcP1tafP.js +1395 -0
  89. package/dist/cdn/date-format-DZT7T1wf.js +1393 -0
  90. package/dist/cdn/{editor-registry-CiI0xlKg.js → editor-registry-BhgE3S84.js} +51 -25
  91. package/dist/cdn/{src-C9Hihx1W.js → src-COC0vnpg.js} +7233 -6701
  92. package/dist/cdn/{src-D1lXwq1l.js → src-pYGV2rMR.js} +5747 -5215
  93. package/dist/cdn/svgrid.js +15 -13
  94. package/dist/cdn/svgrid.svelte-external.js +15 -13
  95. package/dist/cell-values.d.ts +16 -0
  96. package/dist/cell-values.js +28 -0
  97. package/dist/chart-export.js +8 -8
  98. package/dist/clipboard.d.ts +22 -0
  99. package/dist/clipboard.js +405 -27
  100. package/dist/editing.js +12 -7
  101. package/dist/editors/cell-editors.d.ts +11 -0
  102. package/dist/editors/cell-editors.js +18 -1
  103. package/dist/grid-messages.d.ts +15 -0
  104. package/dist/grid-messages.js +15 -0
  105. package/dist/index.d.ts +4 -0
  106. package/dist/index.js +4 -0
  107. package/dist/list-option.d.ts +6 -0
  108. package/dist/menus.js +40 -7
  109. package/dist/sanitize-html.d.ts +37 -0
  110. package/dist/sanitize-html.js +234 -0
  111. package/dist/selection-bar-view.svelte.d.ts +25 -0
  112. package/dist/selection-bar-view.svelte.js +13 -0
  113. package/dist/selection.d.ts +2 -1
  114. package/dist/selection.js +129 -13
  115. package/package.json +11 -11
  116. package/src/FlexRender.svelte +96 -96
  117. package/src/GridFooter.svelte +181 -181
  118. package/src/GridMenus.svelte +184 -6
  119. package/src/SvAutoComplete.svelte +169 -165
  120. package/src/SvAvatar.svelte +75 -75
  121. package/src/SvCalendar.svelte +503 -503
  122. package/src/SvCalendar.test.ts +226 -226
  123. package/src/SvCarousel.svelte +141 -141
  124. package/src/SvCheckBox.svelte +102 -102
  125. package/src/SvCircularProgress.svelte +109 -109
  126. package/src/SvColorInput.svelte +181 -181
  127. package/src/SvComboBox.svelte +279 -275
  128. package/src/SvContextMenu.svelte +116 -116
  129. package/src/SvCountryInput.svelte +163 -163
  130. package/src/SvDateRangeInput.svelte +25 -3
  131. package/src/SvDrawer.svelte +254 -254
  132. package/src/SvDropDownList.svelte +378 -374
  133. package/src/SvDurationInput.svelte +126 -126
  134. package/src/SvField.svelte +293 -293
  135. package/src/SvForm.svelte +437 -437
  136. package/src/SvForm.test.ts +411 -411
  137. package/src/SvGrid.controller.svelte.ts +251 -61
  138. package/src/SvGrid.css +221 -0
  139. package/src/SvGrid.helpers.ts +87 -0
  140. package/src/SvGrid.svelte +145 -0
  141. package/src/SvGrid.types.ts +2092 -1980
  142. package/src/SvGridCellEditor.svelte +744 -662
  143. package/src/SvGridChart.svelte +1716 -1716
  144. package/src/SvGridChartPanel.svelte +485 -485
  145. package/src/SvGridChartView.svelte +70 -70
  146. package/src/SvGridDropdown.svelte +728 -728
  147. package/src/SvGridSelect.svelte +270 -266
  148. package/src/SvGroupCell.svelte +116 -116
  149. package/src/SvListBox.svelte +334 -326
  150. package/src/SvMaskedInput.svelte +122 -122
  151. package/src/SvMenu.svelte +124 -124
  152. package/src/SvMenu.test.ts +97 -97
  153. package/src/SvMenuList.svelte +146 -146
  154. package/src/SvMultiSelect.svelte +293 -289
  155. package/src/SvNumberInput.svelte +172 -168
  156. package/src/SvOtpInput.svelte +158 -158
  157. package/src/SvPasswordInput.svelte +151 -151
  158. package/src/SvPhoneInput.svelte +133 -133
  159. package/src/SvPopover.svelte +197 -197
  160. package/src/SvProgress.svelte +116 -116
  161. package/src/SvRadioGroup.svelte +107 -107
  162. package/src/SvRating.svelte +112 -112
  163. package/src/SvResult.svelte +73 -73
  164. package/src/SvRichCell.svelte +125 -0
  165. package/src/SvRichText.svelte +211 -211
  166. package/src/SvRowGroupPanel.svelte +170 -170
  167. package/src/SvScrollArea.svelte +61 -61
  168. package/src/SvSlider.svelte +203 -203
  169. package/src/SvSwitchButton.svelte +108 -108
  170. package/src/SvTagsInput.svelte +115 -115
  171. package/src/SvTextArea.svelte +5 -1
  172. package/src/SvTextInput.svelte +147 -147
  173. package/src/SvTimePicker.svelte +245 -245
  174. package/src/SvToaster.svelte +159 -159
  175. package/src/SvToaster.test.ts +95 -95
  176. package/src/SvToggleButton.svelte +85 -85
  177. package/src/SvTooltip.svelte +161 -161
  178. package/src/SvTour.svelte +208 -208
  179. package/src/SvTree.svelte +444 -444
  180. package/src/SvTreeSelect.svelte +239 -235
  181. package/src/a11y/dismissable.test.ts +119 -119
  182. package/src/a11y/dismissable.ts +114 -114
  183. package/src/a11y.contract.test.ts +49 -49
  184. package/src/a11y.test.ts +59 -59
  185. package/src/a11y.ts +61 -61
  186. package/src/ai.test.ts +502 -502
  187. package/src/ai.ts +1419 -1419
  188. package/src/build-api.coverage.test.ts +633 -633
  189. package/src/build-api.ts +846 -846
  190. package/src/builtin-editors.grid.test.ts +83 -83
  191. package/src/builtin-editors.test.ts +18 -1
  192. package/src/builtin-editors.ts +19 -3
  193. package/src/cell-formatting.ts +171 -171
  194. package/src/cell-render.test.ts +513 -513
  195. package/src/cell-render.ts +496 -496
  196. package/src/cell-values.ts +148 -118
  197. package/src/chart-export.ts +202 -202
  198. package/src/chart-view.svelte.ts +36 -36
  199. package/src/chart.ts +2321 -2321
  200. package/src/clipboard.test.ts +93 -20
  201. package/src/clipboard.ts +433 -33
  202. package/src/collaboration.test.ts +104 -104
  203. package/src/collaboration.ts +167 -167
  204. package/src/column-groups.ts +78 -78
  205. package/src/core.performance.test.ts +30 -30
  206. package/src/core.ts +1865 -1865
  207. package/src/createAutocomplete.svelte.ts +132 -132
  208. package/src/createCombobox.svelte.ts +191 -191
  209. package/src/createCountryInput.svelte.ts +157 -157
  210. package/src/createDropdownList.svelte.ts +168 -168
  211. package/src/createForm.svelte.ts +386 -386
  212. package/src/createGrid.svelte.ts +42 -42
  213. package/src/createGrid.test.ts +10 -10
  214. package/src/createGridState.svelte.ts +17 -17
  215. package/src/createListbox.svelte.ts +250 -250
  216. package/src/createMenu.svelte.ts +224 -224
  217. package/src/createPopoverSelect.svelte.ts +213 -213
  218. package/src/createSlider.svelte.ts +191 -191
  219. package/src/createTooltip.svelte.ts +144 -144
  220. package/src/createTree.svelte.ts +322 -322
  221. package/src/date-string-column.test.ts +87 -0
  222. package/src/datetime/date-core.ts +206 -206
  223. package/src/datetime/date-restrict.ts +61 -61
  224. package/src/datetime/timezone.ts +135 -135
  225. package/src/dock-manager-model.ts +596 -596
  226. package/src/dock-model.ts +374 -374
  227. package/src/editing.test.ts +974 -949
  228. package/src/editing.ts +609 -606
  229. package/src/editor-block-prop.test.ts +132 -0
  230. package/src/editor-contract.ts +171 -171
  231. package/src/editor-registry.grid.test.ts +144 -144
  232. package/src/editor-registry.ts +122 -122
  233. package/src/editors/cell-editors.ts +27 -1
  234. package/src/export-data-api.test.ts +126 -126
  235. package/src/export-format.test.ts +107 -107
  236. package/src/export-format.ts +601 -601
  237. package/src/filter-operators.ts +160 -160
  238. package/src/filtering/excel-filters.ts +325 -325
  239. package/src/flex-render.ts +3 -3
  240. package/src/form-field.ts +127 -127
  241. package/src/grid-messages.ts +34 -0
  242. package/src/group-display.test.ts +167 -167
  243. package/src/group-display.ts +200 -200
  244. package/src/headless.ts +87 -87
  245. package/src/index.ts +13 -0
  246. package/src/js-scroller.svelte.ts +173 -173
  247. package/src/keyboard-handlers.ts +270 -270
  248. package/src/keyboard.test.ts +59 -59
  249. package/src/keyboard.ts +97 -97
  250. package/src/list-nav.test.ts +49 -49
  251. package/src/list-nav.ts +29 -29
  252. package/src/list-option.test.ts +56 -56
  253. package/src/list-option.ts +179 -173
  254. package/src/menus.test.ts +43 -0
  255. package/src/menus.ts +597 -567
  256. package/src/merge-objects.ts +48 -48
  257. package/src/move-cells.test.ts +850 -0
  258. package/src/number-editor-literal.test.ts +84 -0
  259. package/src/number-editor.grid.test.ts +160 -0
  260. package/src/overlays.test.ts +90 -90
  261. package/src/positioning.ts +268 -268
  262. package/src/render-component.ts +28 -28
  263. package/src/row-drag-touch.ts +211 -211
  264. package/src/row-drag.test.ts +353 -353
  265. package/src/row-drag.ts +415 -415
  266. package/src/row-resize.test.ts +524 -524
  267. package/src/row-resize.ts +228 -228
  268. package/src/sanitize-html.test.ts +182 -0
  269. package/src/sanitize-html.ts +235 -0
  270. package/src/scheduler-ical.ts +181 -181
  271. package/src/scheduler-model.test.ts +562 -562
  272. package/src/scheduler-model.ts +873 -873
  273. package/src/selection-bar-view.svelte.ts +36 -0
  274. package/src/selection.test.ts +900 -743
  275. package/src/selection.ts +131 -13
  276. package/src/server-data-source.test.ts +383 -383
  277. package/src/server-data-source.ts +469 -469
  278. package/src/sparkline.test.ts +68 -68
  279. package/src/sparkline.ts +169 -169
  280. package/src/spreadsheet.test.ts +488 -488
  281. package/src/spreadsheet.ts +312 -312
  282. package/src/static-functions.ts +11 -11
  283. package/src/subscribe.ts +38 -38
  284. package/src/summaries.ts +113 -113
  285. package/src/svgrid-wrapper.types.ts +563 -563
  286. package/src/svgrid.async-editor-options.test.ts +273 -273
  287. package/src/svgrid.auto-row-height.test.ts +204 -204
  288. package/src/svgrid.behavior.test.ts +910 -910
  289. package/src/svgrid.charting.test.ts +534 -534
  290. package/src/svgrid.comments-autocomplete.test.ts +127 -127
  291. package/src/svgrid.context-menu.test.ts +147 -147
  292. package/src/svgrid.features.test.ts +157 -157
  293. package/src/svgrid.filter-depth.test.ts +163 -163
  294. package/src/svgrid.filter-menu-listbox.svelte.test.ts +377 -331
  295. package/src/svgrid.filter-menu-scroll.test.ts +112 -112
  296. package/src/svgrid.grand-total.test.ts +188 -188
  297. package/src/svgrid.group-display-mode.test.ts +171 -171
  298. package/src/svgrid.group-footers.test.ts +121 -121
  299. package/src/svgrid.group-pagination.test.ts +153 -153
  300. package/src/svgrid.in-suggest-snapshot.svelte.test.ts +91 -0
  301. package/src/svgrid.menu-scroll-close.test.ts +204 -0
  302. package/src/svgrid.new-features.wrapper.test.ts +251 -251
  303. package/src/svgrid.selection-bar-seam.test.ts +185 -0
  304. package/src/svgrid.tree-data.test.ts +186 -186
  305. package/src/svgrid.upsell-license.test.ts +117 -0
  306. package/src/svgrid.wrapper.test.ts +63 -63
  307. package/src/svgriddropdown.async-panel.svelte.test.ts +195 -195
  308. package/src/test-setup.ts +62 -62
  309. package/src/themes/index.ts +288 -288
  310. package/src/toast-store.svelte.ts +250 -250
  311. package/src/toast-store.test.ts +147 -147
  312. package/src/tree-row-model.test.ts +168 -168
  313. package/src/ui-buttons.test.ts +144 -144
  314. package/src/ui-inputs.test.ts +118 -118
  315. package/src/ui-localization.test.ts +113 -113
  316. package/src/ui-range.test.ts +70 -70
  317. package/src/ui-selection.test.ts +155 -155
  318. package/src/ui-tier1.test.ts +142 -142
  319. package/src/virtual.test.ts +88 -88
  320. package/src/virtualization/column-virtualizer.test.ts +27 -27
  321. package/src/virtualization/column-virtualizer.ts +30 -30
  322. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  323. package/src/virtualization/types.ts +30 -30
  324. package/src/virtualization/virtualizer.test.ts +47 -47
  325. package/src/virtualization/virtualizer.ts +322 -322
  326. package/dist/cdn/GridMenus-BuoBPqxx.js +0 -493
  327. package/dist/cdn/GridMenus-n4llxoOI.js +0 -494
  328. package/dist/cdn/SvDateTimePicker-CHnUkWcH.js +0 -2206
  329. package/dist/cdn/SvDateTimePicker-CRQH_U7E.js +0 -2201
  330. package/dist/cdn/SvGridCellEditor-BGUCzuPB.js +0 -523
  331. package/dist/cdn/SvGridCellEditor-G8IMWIws.js +0 -522
@@ -1,288 +1,288 @@
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
- /* Optional refinements. A preset that leaves these out keeps the derived
20
- behaviour it had before they existed, so adding one to a single preset
21
- never shifts the others. */
22
- /** Inset surface (master-detail region, generated auth pages). Defaults to `headerBg`. */
23
- bgSubtle?: string
24
- /** Underline below the leaf header row, when it should read stronger than a cell border. */
25
- headerBorder?: string
26
- /** Header label color, when it should differ from `headerFg`. */
27
- headerLabel?: string
28
- /** Frozen-column body cells. Without it the grid derives a tint from `headerBg` + `accent`. */
29
- pinnedBg?: string
30
- /** Frozen-column header cells. */
31
- pinnedHeaderBg?: string
32
- /** The 1px line on a pinned column's inside edge. Defaults to `border`. */
33
- pinnedDivider?: string
34
- /** Top/bottom border of pinned ROWS. Defaults to a translucent accent. */
35
- pinnedBorder?: string
36
- /** Drop shadow cast by a pinned column into the scroll area. */
37
- pinnedShadow?: string
38
- /* Semantic status colors. The UI kit reads these for destructive buttons,
39
- validation errors, alert variants, badges and the rating star. A preset
40
- that omits them gets the mode-appropriate defaults below, which match the
41
- inline fallbacks the components shipped with - so adding these changed
42
- nothing for existing presets, it only made them themeable. */
43
- /** Errors, destructive actions, invalid cells. */
44
- danger?: string
45
- /** Confirmations, positive deltas. */
46
- success?: string
47
- /** Cautions, pending states, the rating star. */
48
- warning?: string
49
- /** Neutral informational callouts. */
50
- info?: string
51
- /** Focus outline color. Defaults to the preset's accent. */
52
- focusRing?: string
53
- }
54
-
55
- /** Status-color defaults per mode - dark mode lifts them so they stay legible
56
- * on a dark ground. Values match the components' own inline fallbacks. */
57
- const STATUS_DEFAULTS: Record<ThemeMode, { danger: string; success: string; warning: string; info: string }> = {
58
- light: { danger: '#dc2626', success: '#16a34a', warning: '#d97706', info: '#0891b2' },
59
- dark: { danger: '#f87171', success: '#4ade80', warning: '#fbbf24', info: '#22d3ee' },
60
- }
61
-
62
- export type ThemePreset = {
63
- /** Stable id, e.g. used as a CSS file name (`themes/<id>.css`). */
64
- id: string
65
- /** Display name shown on a theme picker. */
66
- name: string
67
- /** Control corner rounding (px). */
68
- radius: number
69
- /** Font stack applied to the app surface. */
70
- font: string
71
- /** Header label typography. Mode-independent, like `radius` and `font`. */
72
- headerType?: { weight?: string; size?: string; transform?: string; tracking?: string }
73
- light: ThemePalette
74
- dark: ThemePalette
75
- }
76
-
77
- /** The built-in theme presets, in gallery order (SvGrid's own theme first). */
78
- export const themePresets: ThemePreset[] = [
79
- // Ember is SvGrid's signature look - the palette svgrid.com itself runs on, so
80
- // a screenshot of the site's grids is reproducible with one stylesheet import.
81
- // It is shaped like the shadcn/ui preset below - white ground, stone neutrals,
82
- // 6px rounding, a medium-weight muted header row, no zebra - with SvGrid's
83
- // orange standing in for shadcn's near-black primary. Stone rather than zinc
84
- // because the neutral has to sit next to orange without going cold, which is
85
- // the same pairing shadcn's own orange base color makes.
86
- // Orange is spent only on selection, focus and the active-cell ring; nothing
87
- // in the chrome is tinted. The frozen columns name a neutral explicitly
88
- // instead of letting the grid derive a tint from the accent (that tint landed
89
- // right on top of the selection color).
90
- { id: 'ember', name: 'Ember', radius: 6, font: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
91
- headerType: { weight: '500' },
92
- light: { bg: '#ffffff', fg: '#1c1917', muted: '#78716c', border: '#e7e5e4', headerBg: '#fafaf9', headerFg: '#78716c', accent: '#ff3e00', rowAlt: '#ffffff', rowHover: '#f5f5f4', selectionBg: '#ffece3',
93
- bgSubtle: '#fafaf9',
94
- pinnedBg: '#fafaf9', pinnedHeaderBg: '#f5f5f4', pinnedDivider: '#e7e5e4', pinnedBorder: '#e7e5e4', pinnedShadow: 'rgba(28, 25, 23, 0.10)' },
95
- dark: { bg: '#0c0a09', fg: '#fafaf9', muted: '#a8a29e', border: '#292524', headerBg: '#1c1917', headerFg: '#a8a29e', accent: '#ff5a1f', rowAlt: '#0c0a09', rowHover: '#1c1917', selectionBg: '#3a2318',
96
- bgSubtle: '#1c1917',
97
- pinnedBg: '#151312', pinnedHeaderBg: '#1c1917', pinnedDivider: '#292524', pinnedBorder: '#292524', pinnedShadow: 'rgba(0, 0, 0, 0.50)' } },
98
- { id: 'shadcn', name: 'shadcn/ui', radius: 6, font: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
99
- light: { bg: '#ffffff', fg: '#09090b', muted: '#71717a', border: '#e4e4e7', headerBg: '#fafafa', headerFg: '#71717a', accent: '#18181b', rowAlt: '#ffffff', rowHover: '#f4f4f5', selectionBg: '#f4f4f5' },
100
- dark: { bg: '#0a0a0a', fg: '#fafafa', muted: '#a1a1aa', border: '#27272a', headerBg: '#18181b', headerFg: '#a1a1aa', accent: '#fafafa', rowAlt: '#0a0a0a', rowHover: '#18181b', selectionBg: '#27272a' } },
101
- { id: 'tailwind', name: 'Tailwind', radius: 6, font: 'Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
102
- light: { bg: '#ffffff', fg: '#0f172a', muted: '#64748b', border: '#e2e8f0', headerBg: '#f8fafc', headerFg: '#0f172a', accent: '#4f46e5', rowAlt: '#ffffff', rowHover: '#f1f5f9', selectionBg: '#e0e7ff' },
103
- dark: { bg: '#0f172a', fg: '#f8fafc', muted: '#94a3b8', border: '#334155', headerBg: '#1e293b', headerFg: '#f8fafc', accent: '#818cf8', rowAlt: '#0f172a', rowHover: '#1e293b', selectionBg: '#312e81' } },
104
- { id: 'material', name: 'Material 3', radius: 8, font: '"Roboto Flex", Roboto, "Segoe UI", system-ui, sans-serif',
105
- light: { bg: '#fef7ff', fg: '#1d1b20', muted: '#49454f', border: '#cac4d0', headerBg: '#f3edf7', headerFg: '#1d1b20', accent: '#6750a4', rowAlt: '#fffbfe', rowHover: '#ece6f0', selectionBg: '#e8def8' },
106
- dark: { bg: '#141218', fg: '#e6e0e9', muted: '#cac4d0', border: '#49454f', headerBg: '#211f26', headerFg: '#e6e0e9', accent: '#d0bcff', rowAlt: '#141218', rowHover: '#2b2930', selectionBg: '#4a4458' } },
107
- { id: 'excel', name: 'Excel', radius: 0, font: '"Aptos", "Segoe UI", Calibri, Arial, sans-serif',
108
- 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)' },
109
- 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)' } },
110
- { id: 'fluent', name: 'Fluent 2', radius: 4, font: '"Segoe UI Variable", "Segoe UI", system-ui, sans-serif',
111
- light: { bg: '#ffffff', fg: '#242424', muted: '#616161', border: '#e0e0e0', headerBg: '#f5f5f5', headerFg: '#242424', accent: '#0f6cbd', rowAlt: '#fafafa', rowHover: '#f0f0f0', selectionBg: '#ebebeb' },
112
- dark: { bg: '#1f1f1f', fg: '#ffffff', muted: '#d6d6d6', border: '#666666', headerBg: '#141414', headerFg: '#ffffff', accent: '#479ef5', rowAlt: '#1a1a1a', rowHover: '#3d3d3d', selectionBg: '#383838' } },
113
- { id: 'carbon', name: 'Carbon', radius: 0, font: '"IBM Plex Sans", "Helvetica Neue", Arial, sans-serif',
114
- light: { bg: '#ffffff', fg: '#161616', muted: '#525252', border: '#e0e0e0', headerBg: '#e0e0e0', headerFg: '#161616', accent: '#0f62fe', rowAlt: '#f4f4f4', rowHover: '#e8e8e8', selectionBg: '#d0e2ff' },
115
- dark: { bg: '#161616', fg: '#f4f4f4', muted: '#c6c6c6', border: '#393939', headerBg: '#393939', headerFg: '#f4f4f4', accent: '#78a9ff', rowAlt: '#262626', rowHover: '#333333', selectionBg: '#393939' } },
116
- { id: 'sap', name: 'SAP Fiori', radius: 6, font: '"72", "72full", "Arial", sans-serif',
117
- light: { bg: '#f5f6f7', fg: '#131e29', muted: '#556b82', border: '#e5e5e5', headerBg: '#ffffff', headerFg: '#131e29', accent: '#0070f2', rowAlt: '#f5f6f7', rowHover: '#eaecee', selectionBg: '#ebf8ff' },
118
- dark: { bg: '#12171c', fg: '#f5f6f7', muted: '#8396a8', border: '#2e3742', headerBg: '#1d232a', headerFg: '#f5f6f7', accent: '#4db1ff', rowAlt: '#12171c', rowHover: '#222b35', selectionBg: '#1d2d3e' } },
119
- { id: 'salesforce', name: 'Salesforce', radius: 4, font: '"Salesforce Sans", "SF Pro", Helvetica, Arial, sans-serif',
120
- light: { bg: '#ffffff', fg: '#181818', muted: '#757575', border: '#c9c9c9', headerBg: '#f3f3f3', headerFg: '#181818', accent: '#0176d3', rowAlt: '#ffffff', rowHover: '#f3f3f3', selectionBg: '#ecebea' },
121
- dark: { bg: '#181818', fg: '#ffffff', muted: '#c9c9c9', border: '#3e3e3c', headerBg: '#2e2e2e', headerFg: '#ffffff', accent: '#1b96ff', rowAlt: '#181818', rowHover: '#2e2e2e', selectionBg: '#1e4d8e' } },
122
- { id: 'atlassian', name: 'Atlassian', radius: 3, font: '"Charlie Display", "Atlassian Sans", -apple-system, "Segoe UI", sans-serif',
123
- light: { bg: '#ffffff', fg: '#172b4d', muted: '#44546f', border: '#dcdfe4', headerBg: '#fafbfc', headerFg: '#44546f', accent: '#0c66e4', rowAlt: '#ffffff', rowHover: '#f1f2f4', selectionBg: '#e9f2ff' },
124
- dark: { bg: '#1d2125', fg: '#b6c2cf', muted: '#8590a2', border: '#38414a', headerBg: '#22272b', headerFg: '#8590a2', accent: '#579dff', rowAlt: '#1d2125', rowHover: '#2c333a', selectionBg: '#09326c' } },
125
- { id: 'github', name: 'GitHub', radius: 6, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif',
126
- light: { bg: '#ffffff', fg: '#1f2328', muted: '#59636e', border: '#d1d9e0', headerBg: '#f6f8fa', headerFg: '#59636e', accent: '#0969da', rowAlt: '#ffffff', rowHover: '#f6f8fa', selectionBg: '#ddf4ff' },
127
- 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)' } },
128
- { id: 'antd', name: 'Ant Design', radius: 6, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", sans-serif',
129
- 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: '#066dff', rowAlt: '#ffffff', rowHover: '#f5f5f5', selectionBg: '#e6f4ff' },
130
- 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: '#1678d3', rowAlt: '#141414', rowHover: '#262626', selectionBg: '#111a2c' } },
131
- { id: 'ag-alpine', name: 'Alpine', radius: 3, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
132
- 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)' },
133
- 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)' } },
134
- { id: 'bootstrap', name: 'Bootstrap 5', radius: 6, font: 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
135
- light: { bg: '#ffffff', fg: '#212529', muted: '#6c757d', border: '#dee2e6', headerBg: '#f8f9fa', headerFg: '#212529', accent: '#0d6efd', rowAlt: '#fcfcfd', rowHover: '#f2f2f2', selectionBg: '#cfe2ff' },
136
- dark: { bg: '#212529', fg: '#dee2e6', muted: '#adb5bd', border: '#495057', headerBg: '#2b3035', headerFg: '#dee2e6', accent: '#0d6efd', rowAlt: '#25292e', rowHover: '#2c3034', selectionBg: '#031633' } },
137
- { id: 'vercel', name: 'Vercel', radius: 6, font: '"Geist", "Geist Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
138
- light: { bg: '#ffffff', fg: '#000000', muted: '#666666', border: '#eaeaea', headerBg: '#fafafa', headerFg: '#666666', accent: '#0070f3', rowAlt: '#fafafa', rowHover: '#f5f5f5', selectionBg: '#e8f0fe' },
139
- dark: { bg: '#000000', fg: '#ffffff', muted: '#888888', border: '#333333', headerBg: '#111111', headerFg: '#888888', accent: '#3291ff', rowAlt: '#0a0a0a', rowHover: '#1a1a1a', selectionBg: '#0d2847' } },
140
- { id: 'linear', name: 'Linear', radius: 6, font: '"Inter Variable", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
141
- light: { bg: '#ffffff', fg: '#282a30', muted: '#6b6f76', border: '#e9e9eb', headerBg: '#f7f8f8', headerFg: '#6b6f76', accent: '#5e6ad2', rowAlt: '#fbfbfc', rowHover: '#f4f4f5', selectionBg: '#e8eafd' },
142
- dark: { bg: '#08090a', fg: '#f7f8f8', muted: '#8a8f98', border: '#23252a', headerBg: '#0f1011', headerFg: '#8a8f98', accent: '#5e6ad2', rowAlt: '#0f1011', rowHover: '#1a1b1e', selectionBg: '#2a2d4a' } },
143
- { id: 'notion', name: 'Notion', radius: 4, font: 'ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif',
144
- // muted + headerFg darkened from #787774, which sat at 4.48:1 on white and
145
- // 4.14:1 on the header fill - both just under AA. #6b6a67 keeps the warm
146
- // grey and clears it at 5.41:1 / 5:1.
147
- light: { bg: '#ffffff', fg: '#37352f', muted: '#6b6a67', border: '#e9e9e7', headerBg: '#f7f6f3', headerFg: '#6b6a67', accent: '#2383e2', rowAlt: '#fbfbfa', rowHover: '#f1f1ef', selectionBg: '#d9eaf7' },
148
- dark: { bg: '#191919', fg: '#d4d4d4', muted: '#9b9b9b', border: '#2f2f2f', headerBg: '#202020', headerFg: '#9b9b9b', accent: '#529cca', rowAlt: '#1c1c1c', rowHover: '#252525', selectionBg: '#20415c' } },
149
- { id: 'nord', name: 'Nord', radius: 4, font: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
150
- light: { bg: '#eceff4', fg: '#2e3440', muted: '#4c566a', border: '#d8dee9', headerBg: '#e5e9f0', headerFg: '#2e3440', accent: '#5579a5', rowAlt: '#e5e9f0', rowHover: '#dde3ec', selectionBg: '#d8dee9' },
151
- // muted lightened from Nord's #4c566a (nord3): on the nord0 background that
152
- // is 1.69:1, effectively invisible. #97a0b0 keeps the Nord hue at 4.74:1.
153
- dark: { bg: '#2e3440', fg: '#d8dee9', muted: '#97a0b0', border: '#434c5e', headerBg: '#3b4252', headerFg: '#e5e9f0', accent: '#88c0d0', rowAlt: '#333b48', rowHover: '#3b4252', selectionBg: '#434c5e' } },
154
- { id: 'dracula', name: 'Dracula', radius: 6, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
155
- light: { bg: '#fffbeb', fg: '#1f1f1f', muted: '#6c664b', border: '#dedccf', headerBg: '#ece9df', headerFg: '#1f1f1f', accent: '#644ac9', rowAlt: '#fffdf5', rowHover: '#f2efe3', selectionBg: '#cfcfde' },
156
- // muted lightened from Dracula's comment colour #6272a4 (3.03:1) to 5.04:1.
157
- dark: { bg: '#282a36', fg: '#f8f8f2', muted: '#8b98c9', border: '#44475a', headerBg: '#21222c', headerFg: '#f8f8f2', accent: '#bd93f9', rowAlt: '#2d2f3b', rowHover: '#343746', selectionBg: '#44475a' } },
158
- { id: 'catppuccin', name: 'Catppuccin', radius: 8, font: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
159
- // muted moved from Latte subtext0 (#6c6f85, 4.37:1) to subtext1 (#5c5f77),
160
- // still an official Catppuccin colour, at 5.53:1.
161
- light: { bg: '#eff1f5', fg: '#4c4f69', muted: '#5c5f77', border: '#ccd0da', headerBg: '#e6e9ef', headerFg: '#4c4f69', accent: '#8839ef', rowAlt: '#e6e9ef', rowHover: '#dce0e8', selectionBg: '#dce0e8' },
162
- dark: { bg: '#1e1e2e', fg: '#cdd6f4', muted: '#a6adc8', border: '#313244', headerBg: '#181825', headerFg: '#cdd6f4', accent: '#cba6f7', rowAlt: '#1a1a28', rowHover: '#313244', selectionBg: '#45475a' } },
163
- ]
164
-
165
- /** The default preset (used when nothing is chosen): Ember, the house theme the
166
- * demo gallery and svgrid.com open on, so a scaffolded or Studio-generated app
167
- * looks like the demos unless a preset is picked. */
168
- export const defaultThemePreset: ThemePreset = themePresets.find((t) => t.id === 'ember') ?? themePresets[0]!
169
-
170
- /** Look up a preset by id. */
171
- export const getThemePreset = (id?: string): ThemePreset | undefined =>
172
- id ? themePresets.find((t) => t.id === id) : undefined
173
-
174
- /** WCAG 2.1 relative luminance. */
175
- function relativeLuminance(hex: string): number {
176
- const n = parseInt(hex, 16)
177
- const chan = (v: number) => {
178
- const c = v / 255
179
- return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
180
- }
181
- return 0.2126 * chan((n >> 16) & 255) + 0.7152 * chan((n >> 8) & 255) + 0.0722 * chan(n & 255)
182
- }
183
-
184
- /** WCAG contrast ratio between two 6-digit hex colors, 1 (identical) to 21. */
185
- function contrastRatio(a: string, b: string): number {
186
- const [x, y] = [relativeLuminance(a), relativeLuminance(b)]
187
- return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05)
188
- }
189
-
190
- /**
191
- * A readable (near-black / white) foreground for text placed on the accent.
192
- *
193
- * Picks whichever candidate has the higher WCAG contrast against the accent.
194
- * This used to threshold a YIQ luminance approximation at 0.6, which handed
195
- * white text to every mid-tone accent - Tailwind's indigo in dark mode scored
196
- * 2.98:1 against white where AA wants 4.5. Comparing real contrast ratios fixes
197
- * that class of preset without touching any palette.
198
- */
199
- function onAccent(accent: string): string {
200
- const m = /^#?([0-9a-f]{6})$/i.exec(accent.trim())
201
- if (!m) return '#ffffff'
202
- const hex = m[1]!
203
- return contrastRatio('18181b', hex) >= contrastRatio('ffffff', hex) ? '#18181b' : '#ffffff'
204
- }
205
-
206
- /** Resolve the effective `--sg-*` tokens for a preset in a given mode, with an
207
- * optional accent override applied. */
208
- export function resolveThemeTokens(preset: ThemePreset | undefined, mode: ThemeMode, accentOverride?: string): Record<string, string> {
209
- const resolved = preset ?? defaultThemePreset
210
- const p = resolved[mode]
211
- const accent = accentOverride || p.accent
212
- const status = STATUS_DEFAULTS[mode]
213
- return {
214
- '--sg-bg': p.bg,
215
- '--sg-fg': p.fg,
216
- '--sg-muted': p.muted,
217
- '--sg-border': p.border,
218
- '--sg-header-bg': p.headerBg,
219
- '--sg-header-fg': p.headerFg,
220
- ...(p.headerBorder ? { '--sg-header-border': p.headerBorder } : {}),
221
- ...(p.headerLabel ? { '--sg-header-label-color': p.headerLabel } : {}),
222
- ...(resolved.headerType?.weight ? { '--sg-header-weight': resolved.headerType.weight } : {}),
223
- ...(resolved.headerType?.size ? { '--sg-header-size': resolved.headerType.size } : {}),
224
- ...(resolved.headerType?.transform ? { '--sg-header-transform': resolved.headerType.transform } : {}),
225
- ...(resolved.headerType?.tracking ? { '--sg-header-tracking': resolved.headerType.tracking } : {}),
226
- // A subtle inset surface (used by e.g. the master-detail region behind the
227
- // nested grid). Themed so it isn't a fixed light grey on dark presets.
228
- // Presets whose header sits almost on the page ground set `bgSubtle`
229
- // separately, otherwise their inset surfaces would vanish.
230
- '--sg-bg-subtle': p.bgSubtle ?? p.headerBg,
231
- '--sg-row-alt-bg': p.rowAlt,
232
- '--sg-row-hover-bg': p.rowHover,
233
- '--sg-selection-bg': p.selectionBg,
234
- // Frozen columns/rows. Left unset the grid mixes the accent into the header
235
- // color, which suits presets with a cool, low-chroma accent; a preset with a
236
- // saturated accent should name these instead.
237
- ...(p.pinnedBg ? { '--sg-pinned-bg': p.pinnedBg } : {}),
238
- ...(p.pinnedHeaderBg ? { '--sg-pinned-header-bg': p.pinnedHeaderBg } : {}),
239
- ...(p.pinnedDivider ? { '--sg-pinned-divider': p.pinnedDivider } : {}),
240
- ...(p.pinnedBorder ? { '--sg-pinned-border': p.pinnedBorder } : {}),
241
- ...(p.pinnedShadow ? { '--sg-pinned-shadow-color': p.pinnedShadow } : {}),
242
- // Inputs/checkboxes: pin to the theme's own surface so they never borrow the
243
- // host page's input tokens (which is how a themed grid ended up with dark
244
- // checkboxes on a light page when the host mode differed).
245
- '--sg-input-bg': p.bg,
246
- '--sg-input-border': p.border,
247
- '--sg-accent': accent,
248
- '--sg-on-accent': onAccent(accent),
249
- '--sg-radius': `${resolved.radius}px`,
250
- '--sg-font': resolved.font,
251
- // Semantic status colors. Same reasoning as the scrollbar tokens below:
252
- // the UI kit consumes these (destructive buttons, alerts, validation,
253
- // badges, the rating star), so a preset that doesn't define them leaves
254
- // every status color at a hard-coded red/green/amber that ignores the
255
- // theme and, on dark presets, sits at the wrong lightness.
256
- '--sg-danger': p.danger ?? status.danger,
257
- '--sg-success': p.success ?? status.success,
258
- '--sg-warning': p.warning ?? status.warning,
259
- '--sg-info': p.info ?? status.info,
260
- // A COLOR, not a shadow - consumed as `outline: 2px solid var(...)`.
261
- '--sg-focus-ring': p.focusRing ?? accent,
262
- // Derived surfaces + shape the kit reads. Deriving from the palette keeps
263
- // them following the preset and the light/dark mode.
264
- '--sg-muted-bg': p.rowHover,
265
- '--sg-skeleton-bg': p.rowHover,
266
- '--sg-radius-lg': `${resolved.radius * 2}px`,
267
- '--sg-rating-on': p.warning ?? status.warning,
268
- '--sg-rating-empty': p.border,
269
- '--sg-invalid-fg': p.danger ?? status.danger,
270
- // The grid's custom scrollbar is a shadow-DOM web component that reads its
271
- // OWN --sg-scrollbar-* tokens (with fixed light-mode fallbacks). Without
272
- // these it ignores the theme entirely - a light scrollbar left on a dark
273
- // grid. CSS custom properties pierce shadow DOM, so defining them here (on
274
- // the themed container) reaches the scrollbar. Derive them from the palette
275
- // so the scrollbar tracks the preset + light/dark mode like everything else.
276
- '--sg-scrollbar-bg': p.bgSubtle ?? p.headerBg,
277
- '--sg-scrollbar-border': p.border,
278
- '--sg-scrollbar-thumb': p.muted,
279
- '--sg-scrollbar-thumb-hover': p.fg,
280
- '--sg-scrollbar-thumb-active': p.fg,
281
- '--sg-scrollbar-arrow': p.muted,
282
- '--sg-scrollbar-arrow-hover': p.fg,
283
- '--sg-scrollbar-arrow-hover-bg': p.rowHover,
284
- '--sg-scrollbar-arrow-active': p.fg,
285
- '--sg-scrollbar-arrow-active-bg': p.selectionBg,
286
- '--sg-scrollbar-arrow-disabled': p.border,
287
- }
288
- }
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
+ /* Optional refinements. A preset that leaves these out keeps the derived
20
+ behaviour it had before they existed, so adding one to a single preset
21
+ never shifts the others. */
22
+ /** Inset surface (master-detail region, generated auth pages). Defaults to `headerBg`. */
23
+ bgSubtle?: string
24
+ /** Underline below the leaf header row, when it should read stronger than a cell border. */
25
+ headerBorder?: string
26
+ /** Header label color, when it should differ from `headerFg`. */
27
+ headerLabel?: string
28
+ /** Frozen-column body cells. Without it the grid derives a tint from `headerBg` + `accent`. */
29
+ pinnedBg?: string
30
+ /** Frozen-column header cells. */
31
+ pinnedHeaderBg?: string
32
+ /** The 1px line on a pinned column's inside edge. Defaults to `border`. */
33
+ pinnedDivider?: string
34
+ /** Top/bottom border of pinned ROWS. Defaults to a translucent accent. */
35
+ pinnedBorder?: string
36
+ /** Drop shadow cast by a pinned column into the scroll area. */
37
+ pinnedShadow?: string
38
+ /* Semantic status colors. The UI kit reads these for destructive buttons,
39
+ validation errors, alert variants, badges and the rating star. A preset
40
+ that omits them gets the mode-appropriate defaults below, which match the
41
+ inline fallbacks the components shipped with - so adding these changed
42
+ nothing for existing presets, it only made them themeable. */
43
+ /** Errors, destructive actions, invalid cells. */
44
+ danger?: string
45
+ /** Confirmations, positive deltas. */
46
+ success?: string
47
+ /** Cautions, pending states, the rating star. */
48
+ warning?: string
49
+ /** Neutral informational callouts. */
50
+ info?: string
51
+ /** Focus outline color. Defaults to the preset's accent. */
52
+ focusRing?: string
53
+ }
54
+
55
+ /** Status-color defaults per mode - dark mode lifts them so they stay legible
56
+ * on a dark ground. Values match the components' own inline fallbacks. */
57
+ const STATUS_DEFAULTS: Record<ThemeMode, { danger: string; success: string; warning: string; info: string }> = {
58
+ light: { danger: '#dc2626', success: '#16a34a', warning: '#d97706', info: '#0891b2' },
59
+ dark: { danger: '#f87171', success: '#4ade80', warning: '#fbbf24', info: '#22d3ee' },
60
+ }
61
+
62
+ export type ThemePreset = {
63
+ /** Stable id, e.g. used as a CSS file name (`themes/<id>.css`). */
64
+ id: string
65
+ /** Display name shown on a theme picker. */
66
+ name: string
67
+ /** Control corner rounding (px). */
68
+ radius: number
69
+ /** Font stack applied to the app surface. */
70
+ font: string
71
+ /** Header label typography. Mode-independent, like `radius` and `font`. */
72
+ headerType?: { weight?: string; size?: string; transform?: string; tracking?: string }
73
+ light: ThemePalette
74
+ dark: ThemePalette
75
+ }
76
+
77
+ /** The built-in theme presets, in gallery order (SvGrid's own theme first). */
78
+ export const themePresets: ThemePreset[] = [
79
+ // Ember is SvGrid's signature look - the palette svgrid.com itself runs on, so
80
+ // a screenshot of the site's grids is reproducible with one stylesheet import.
81
+ // It is shaped like the shadcn/ui preset below - white ground, stone neutrals,
82
+ // 6px rounding, a medium-weight muted header row, no zebra - with SvGrid's
83
+ // orange standing in for shadcn's near-black primary. Stone rather than zinc
84
+ // because the neutral has to sit next to orange without going cold, which is
85
+ // the same pairing shadcn's own orange base color makes.
86
+ // Orange is spent only on selection, focus and the active-cell ring; nothing
87
+ // in the chrome is tinted. The frozen columns name a neutral explicitly
88
+ // instead of letting the grid derive a tint from the accent (that tint landed
89
+ // right on top of the selection color).
90
+ { id: 'ember', name: 'Ember', radius: 6, font: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
91
+ headerType: { weight: '500' },
92
+ light: { bg: '#ffffff', fg: '#1c1917', muted: '#78716c', border: '#e7e5e4', headerBg: '#fafaf9', headerFg: '#78716c', accent: '#ff3e00', rowAlt: '#ffffff', rowHover: '#f5f5f4', selectionBg: '#ffece3',
93
+ bgSubtle: '#fafaf9',
94
+ pinnedBg: '#fafaf9', pinnedHeaderBg: '#f5f5f4', pinnedDivider: '#e7e5e4', pinnedBorder: '#e7e5e4', pinnedShadow: 'rgba(28, 25, 23, 0.10)' },
95
+ dark: { bg: '#0c0a09', fg: '#fafaf9', muted: '#a8a29e', border: '#292524', headerBg: '#1c1917', headerFg: '#a8a29e', accent: '#ff5a1f', rowAlt: '#0c0a09', rowHover: '#1c1917', selectionBg: '#3a2318',
96
+ bgSubtle: '#1c1917',
97
+ pinnedBg: '#151312', pinnedHeaderBg: '#1c1917', pinnedDivider: '#292524', pinnedBorder: '#292524', pinnedShadow: 'rgba(0, 0, 0, 0.50)' } },
98
+ { id: 'shadcn', name: 'shadcn/ui', radius: 6, font: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
99
+ light: { bg: '#ffffff', fg: '#09090b', muted: '#71717a', border: '#e4e4e7', headerBg: '#fafafa', headerFg: '#71717a', accent: '#18181b', rowAlt: '#ffffff', rowHover: '#f4f4f5', selectionBg: '#f4f4f5' },
100
+ dark: { bg: '#0a0a0a', fg: '#fafafa', muted: '#a1a1aa', border: '#27272a', headerBg: '#18181b', headerFg: '#a1a1aa', accent: '#fafafa', rowAlt: '#0a0a0a', rowHover: '#18181b', selectionBg: '#27272a' } },
101
+ { id: 'tailwind', name: 'Tailwind', radius: 6, font: 'Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
102
+ light: { bg: '#ffffff', fg: '#0f172a', muted: '#64748b', border: '#e2e8f0', headerBg: '#f8fafc', headerFg: '#0f172a', accent: '#4f46e5', rowAlt: '#ffffff', rowHover: '#f1f5f9', selectionBg: '#e0e7ff' },
103
+ dark: { bg: '#0f172a', fg: '#f8fafc', muted: '#94a3b8', border: '#334155', headerBg: '#1e293b', headerFg: '#f8fafc', accent: '#818cf8', rowAlt: '#0f172a', rowHover: '#1e293b', selectionBg: '#312e81' } },
104
+ { id: 'material', name: 'Material 3', radius: 8, font: '"Roboto Flex", Roboto, "Segoe UI", system-ui, sans-serif',
105
+ light: { bg: '#fef7ff', fg: '#1d1b20', muted: '#49454f', border: '#cac4d0', headerBg: '#f3edf7', headerFg: '#1d1b20', accent: '#6750a4', rowAlt: '#fffbfe', rowHover: '#ece6f0', selectionBg: '#e8def8' },
106
+ dark: { bg: '#141218', fg: '#e6e0e9', muted: '#cac4d0', border: '#49454f', headerBg: '#211f26', headerFg: '#e6e0e9', accent: '#d0bcff', rowAlt: '#141218', rowHover: '#2b2930', selectionBg: '#4a4458' } },
107
+ { id: 'excel', name: 'Excel', radius: 0, font: '"Aptos", "Segoe UI", Calibri, Arial, sans-serif',
108
+ 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)' },
109
+ 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)' } },
110
+ { id: 'fluent', name: 'Fluent 2', radius: 4, font: '"Segoe UI Variable", "Segoe UI", system-ui, sans-serif',
111
+ light: { bg: '#ffffff', fg: '#242424', muted: '#616161', border: '#e0e0e0', headerBg: '#f5f5f5', headerFg: '#242424', accent: '#0f6cbd', rowAlt: '#fafafa', rowHover: '#f0f0f0', selectionBg: '#ebebeb' },
112
+ dark: { bg: '#1f1f1f', fg: '#ffffff', muted: '#d6d6d6', border: '#666666', headerBg: '#141414', headerFg: '#ffffff', accent: '#479ef5', rowAlt: '#1a1a1a', rowHover: '#3d3d3d', selectionBg: '#383838' } },
113
+ { id: 'carbon', name: 'Carbon', radius: 0, font: '"IBM Plex Sans", "Helvetica Neue", Arial, sans-serif',
114
+ light: { bg: '#ffffff', fg: '#161616', muted: '#525252', border: '#e0e0e0', headerBg: '#e0e0e0', headerFg: '#161616', accent: '#0f62fe', rowAlt: '#f4f4f4', rowHover: '#e8e8e8', selectionBg: '#d0e2ff' },
115
+ dark: { bg: '#161616', fg: '#f4f4f4', muted: '#c6c6c6', border: '#393939', headerBg: '#393939', headerFg: '#f4f4f4', accent: '#78a9ff', rowAlt: '#262626', rowHover: '#333333', selectionBg: '#393939' } },
116
+ { id: 'sap', name: 'SAP Fiori', radius: 6, font: '"72", "72full", "Arial", sans-serif',
117
+ light: { bg: '#f5f6f7', fg: '#131e29', muted: '#556b82', border: '#e5e5e5', headerBg: '#ffffff', headerFg: '#131e29', accent: '#0070f2', rowAlt: '#f5f6f7', rowHover: '#eaecee', selectionBg: '#ebf8ff' },
118
+ dark: { bg: '#12171c', fg: '#f5f6f7', muted: '#8396a8', border: '#2e3742', headerBg: '#1d232a', headerFg: '#f5f6f7', accent: '#4db1ff', rowAlt: '#12171c', rowHover: '#222b35', selectionBg: '#1d2d3e' } },
119
+ { id: 'salesforce', name: 'Salesforce', radius: 4, font: '"Salesforce Sans", "SF Pro", Helvetica, Arial, sans-serif',
120
+ light: { bg: '#ffffff', fg: '#181818', muted: '#757575', border: '#c9c9c9', headerBg: '#f3f3f3', headerFg: '#181818', accent: '#0176d3', rowAlt: '#ffffff', rowHover: '#f3f3f3', selectionBg: '#ecebea' },
121
+ dark: { bg: '#181818', fg: '#ffffff', muted: '#c9c9c9', border: '#3e3e3c', headerBg: '#2e2e2e', headerFg: '#ffffff', accent: '#1b96ff', rowAlt: '#181818', rowHover: '#2e2e2e', selectionBg: '#1e4d8e' } },
122
+ { id: 'atlassian', name: 'Atlassian', radius: 3, font: '"Charlie Display", "Atlassian Sans", -apple-system, "Segoe UI", sans-serif',
123
+ light: { bg: '#ffffff', fg: '#172b4d', muted: '#44546f', border: '#dcdfe4', headerBg: '#fafbfc', headerFg: '#44546f', accent: '#0c66e4', rowAlt: '#ffffff', rowHover: '#f1f2f4', selectionBg: '#e9f2ff' },
124
+ dark: { bg: '#1d2125', fg: '#b6c2cf', muted: '#8590a2', border: '#38414a', headerBg: '#22272b', headerFg: '#8590a2', accent: '#579dff', rowAlt: '#1d2125', rowHover: '#2c333a', selectionBg: '#09326c' } },
125
+ { id: 'github', name: 'GitHub', radius: 6, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif',
126
+ light: { bg: '#ffffff', fg: '#1f2328', muted: '#59636e', border: '#d1d9e0', headerBg: '#f6f8fa', headerFg: '#59636e', accent: '#0969da', rowAlt: '#ffffff', rowHover: '#f6f8fa', selectionBg: '#ddf4ff' },
127
+ 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)' } },
128
+ { id: 'antd', name: 'Ant Design', radius: 6, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", sans-serif',
129
+ 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: '#066dff', rowAlt: '#ffffff', rowHover: '#f5f5f5', selectionBg: '#e6f4ff' },
130
+ 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: '#1678d3', rowAlt: '#141414', rowHover: '#262626', selectionBg: '#111a2c' } },
131
+ { id: 'ag-alpine', name: 'Alpine', radius: 3, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
132
+ 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)' },
133
+ 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)' } },
134
+ { id: 'bootstrap', name: 'Bootstrap 5', radius: 6, font: 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
135
+ light: { bg: '#ffffff', fg: '#212529', muted: '#6c757d', border: '#dee2e6', headerBg: '#f8f9fa', headerFg: '#212529', accent: '#0d6efd', rowAlt: '#fcfcfd', rowHover: '#f2f2f2', selectionBg: '#cfe2ff' },
136
+ dark: { bg: '#212529', fg: '#dee2e6', muted: '#adb5bd', border: '#495057', headerBg: '#2b3035', headerFg: '#dee2e6', accent: '#0d6efd', rowAlt: '#25292e', rowHover: '#2c3034', selectionBg: '#031633' } },
137
+ { id: 'vercel', name: 'Vercel', radius: 6, font: '"Geist", "Geist Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
138
+ light: { bg: '#ffffff', fg: '#000000', muted: '#666666', border: '#eaeaea', headerBg: '#fafafa', headerFg: '#666666', accent: '#0070f3', rowAlt: '#fafafa', rowHover: '#f5f5f5', selectionBg: '#e8f0fe' },
139
+ dark: { bg: '#000000', fg: '#ffffff', muted: '#888888', border: '#333333', headerBg: '#111111', headerFg: '#888888', accent: '#3291ff', rowAlt: '#0a0a0a', rowHover: '#1a1a1a', selectionBg: '#0d2847' } },
140
+ { id: 'linear', name: 'Linear', radius: 6, font: '"Inter Variable", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
141
+ light: { bg: '#ffffff', fg: '#282a30', muted: '#6b6f76', border: '#e9e9eb', headerBg: '#f7f8f8', headerFg: '#6b6f76', accent: '#5e6ad2', rowAlt: '#fbfbfc', rowHover: '#f4f4f5', selectionBg: '#e8eafd' },
142
+ dark: { bg: '#08090a', fg: '#f7f8f8', muted: '#8a8f98', border: '#23252a', headerBg: '#0f1011', headerFg: '#8a8f98', accent: '#5e6ad2', rowAlt: '#0f1011', rowHover: '#1a1b1e', selectionBg: '#2a2d4a' } },
143
+ { id: 'notion', name: 'Notion', radius: 4, font: 'ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif',
144
+ // muted + headerFg darkened from #787774, which sat at 4.48:1 on white and
145
+ // 4.14:1 on the header fill - both just under AA. #6b6a67 keeps the warm
146
+ // grey and clears it at 5.41:1 / 5:1.
147
+ light: { bg: '#ffffff', fg: '#37352f', muted: '#6b6a67', border: '#e9e9e7', headerBg: '#f7f6f3', headerFg: '#6b6a67', accent: '#2383e2', rowAlt: '#fbfbfa', rowHover: '#f1f1ef', selectionBg: '#d9eaf7' },
148
+ dark: { bg: '#191919', fg: '#d4d4d4', muted: '#9b9b9b', border: '#2f2f2f', headerBg: '#202020', headerFg: '#9b9b9b', accent: '#529cca', rowAlt: '#1c1c1c', rowHover: '#252525', selectionBg: '#20415c' } },
149
+ { id: 'nord', name: 'Nord', radius: 4, font: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
150
+ light: { bg: '#eceff4', fg: '#2e3440', muted: '#4c566a', border: '#d8dee9', headerBg: '#e5e9f0', headerFg: '#2e3440', accent: '#5579a5', rowAlt: '#e5e9f0', rowHover: '#dde3ec', selectionBg: '#d8dee9' },
151
+ // muted lightened from Nord's #4c566a (nord3): on the nord0 background that
152
+ // is 1.69:1, effectively invisible. #97a0b0 keeps the Nord hue at 4.74:1.
153
+ dark: { bg: '#2e3440', fg: '#d8dee9', muted: '#97a0b0', border: '#434c5e', headerBg: '#3b4252', headerFg: '#e5e9f0', accent: '#88c0d0', rowAlt: '#333b48', rowHover: '#3b4252', selectionBg: '#434c5e' } },
154
+ { id: 'dracula', name: 'Dracula', radius: 6, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
155
+ light: { bg: '#fffbeb', fg: '#1f1f1f', muted: '#6c664b', border: '#dedccf', headerBg: '#ece9df', headerFg: '#1f1f1f', accent: '#644ac9', rowAlt: '#fffdf5', rowHover: '#f2efe3', selectionBg: '#cfcfde' },
156
+ // muted lightened from Dracula's comment colour #6272a4 (3.03:1) to 5.04:1.
157
+ dark: { bg: '#282a36', fg: '#f8f8f2', muted: '#8b98c9', border: '#44475a', headerBg: '#21222c', headerFg: '#f8f8f2', accent: '#bd93f9', rowAlt: '#2d2f3b', rowHover: '#343746', selectionBg: '#44475a' } },
158
+ { id: 'catppuccin', name: 'Catppuccin', radius: 8, font: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
159
+ // muted moved from Latte subtext0 (#6c6f85, 4.37:1) to subtext1 (#5c5f77),
160
+ // still an official Catppuccin colour, at 5.53:1.
161
+ light: { bg: '#eff1f5', fg: '#4c4f69', muted: '#5c5f77', border: '#ccd0da', headerBg: '#e6e9ef', headerFg: '#4c4f69', accent: '#8839ef', rowAlt: '#e6e9ef', rowHover: '#dce0e8', selectionBg: '#dce0e8' },
162
+ dark: { bg: '#1e1e2e', fg: '#cdd6f4', muted: '#a6adc8', border: '#313244', headerBg: '#181825', headerFg: '#cdd6f4', accent: '#cba6f7', rowAlt: '#1a1a28', rowHover: '#313244', selectionBg: '#45475a' } },
163
+ ]
164
+
165
+ /** The default preset (used when nothing is chosen): Ember, the house theme the
166
+ * demo gallery and svgrid.com open on, so a scaffolded or Studio-generated app
167
+ * looks like the demos unless a preset is picked. */
168
+ export const defaultThemePreset: ThemePreset = themePresets.find((t) => t.id === 'ember') ?? themePresets[0]!
169
+
170
+ /** Look up a preset by id. */
171
+ export const getThemePreset = (id?: string): ThemePreset | undefined =>
172
+ id ? themePresets.find((t) => t.id === id) : undefined
173
+
174
+ /** WCAG 2.1 relative luminance. */
175
+ function relativeLuminance(hex: string): number {
176
+ const n = parseInt(hex, 16)
177
+ const chan = (v: number) => {
178
+ const c = v / 255
179
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
180
+ }
181
+ return 0.2126 * chan((n >> 16) & 255) + 0.7152 * chan((n >> 8) & 255) + 0.0722 * chan(n & 255)
182
+ }
183
+
184
+ /** WCAG contrast ratio between two 6-digit hex colors, 1 (identical) to 21. */
185
+ function contrastRatio(a: string, b: string): number {
186
+ const [x, y] = [relativeLuminance(a), relativeLuminance(b)]
187
+ return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05)
188
+ }
189
+
190
+ /**
191
+ * A readable (near-black / white) foreground for text placed on the accent.
192
+ *
193
+ * Picks whichever candidate has the higher WCAG contrast against the accent.
194
+ * This used to threshold a YIQ luminance approximation at 0.6, which handed
195
+ * white text to every mid-tone accent - Tailwind's indigo in dark mode scored
196
+ * 2.98:1 against white where AA wants 4.5. Comparing real contrast ratios fixes
197
+ * that class of preset without touching any palette.
198
+ */
199
+ function onAccent(accent: string): string {
200
+ const m = /^#?([0-9a-f]{6})$/i.exec(accent.trim())
201
+ if (!m) return '#ffffff'
202
+ const hex = m[1]!
203
+ return contrastRatio('18181b', hex) >= contrastRatio('ffffff', hex) ? '#18181b' : '#ffffff'
204
+ }
205
+
206
+ /** Resolve the effective `--sg-*` tokens for a preset in a given mode, with an
207
+ * optional accent override applied. */
208
+ export function resolveThemeTokens(preset: ThemePreset | undefined, mode: ThemeMode, accentOverride?: string): Record<string, string> {
209
+ const resolved = preset ?? defaultThemePreset
210
+ const p = resolved[mode]
211
+ const accent = accentOverride || p.accent
212
+ const status = STATUS_DEFAULTS[mode]
213
+ return {
214
+ '--sg-bg': p.bg,
215
+ '--sg-fg': p.fg,
216
+ '--sg-muted': p.muted,
217
+ '--sg-border': p.border,
218
+ '--sg-header-bg': p.headerBg,
219
+ '--sg-header-fg': p.headerFg,
220
+ ...(p.headerBorder ? { '--sg-header-border': p.headerBorder } : {}),
221
+ ...(p.headerLabel ? { '--sg-header-label-color': p.headerLabel } : {}),
222
+ ...(resolved.headerType?.weight ? { '--sg-header-weight': resolved.headerType.weight } : {}),
223
+ ...(resolved.headerType?.size ? { '--sg-header-size': resolved.headerType.size } : {}),
224
+ ...(resolved.headerType?.transform ? { '--sg-header-transform': resolved.headerType.transform } : {}),
225
+ ...(resolved.headerType?.tracking ? { '--sg-header-tracking': resolved.headerType.tracking } : {}),
226
+ // A subtle inset surface (used by e.g. the master-detail region behind the
227
+ // nested grid). Themed so it isn't a fixed light grey on dark presets.
228
+ // Presets whose header sits almost on the page ground set `bgSubtle`
229
+ // separately, otherwise their inset surfaces would vanish.
230
+ '--sg-bg-subtle': p.bgSubtle ?? p.headerBg,
231
+ '--sg-row-alt-bg': p.rowAlt,
232
+ '--sg-row-hover-bg': p.rowHover,
233
+ '--sg-selection-bg': p.selectionBg,
234
+ // Frozen columns/rows. Left unset the grid mixes the accent into the header
235
+ // color, which suits presets with a cool, low-chroma accent; a preset with a
236
+ // saturated accent should name these instead.
237
+ ...(p.pinnedBg ? { '--sg-pinned-bg': p.pinnedBg } : {}),
238
+ ...(p.pinnedHeaderBg ? { '--sg-pinned-header-bg': p.pinnedHeaderBg } : {}),
239
+ ...(p.pinnedDivider ? { '--sg-pinned-divider': p.pinnedDivider } : {}),
240
+ ...(p.pinnedBorder ? { '--sg-pinned-border': p.pinnedBorder } : {}),
241
+ ...(p.pinnedShadow ? { '--sg-pinned-shadow-color': p.pinnedShadow } : {}),
242
+ // Inputs/checkboxes: pin to the theme's own surface so they never borrow the
243
+ // host page's input tokens (which is how a themed grid ended up with dark
244
+ // checkboxes on a light page when the host mode differed).
245
+ '--sg-input-bg': p.bg,
246
+ '--sg-input-border': p.border,
247
+ '--sg-accent': accent,
248
+ '--sg-on-accent': onAccent(accent),
249
+ '--sg-radius': `${resolved.radius}px`,
250
+ '--sg-font': resolved.font,
251
+ // Semantic status colors. Same reasoning as the scrollbar tokens below:
252
+ // the UI kit consumes these (destructive buttons, alerts, validation,
253
+ // badges, the rating star), so a preset that doesn't define them leaves
254
+ // every status color at a hard-coded red/green/amber that ignores the
255
+ // theme and, on dark presets, sits at the wrong lightness.
256
+ '--sg-danger': p.danger ?? status.danger,
257
+ '--sg-success': p.success ?? status.success,
258
+ '--sg-warning': p.warning ?? status.warning,
259
+ '--sg-info': p.info ?? status.info,
260
+ // A COLOR, not a shadow - consumed as `outline: 2px solid var(...)`.
261
+ '--sg-focus-ring': p.focusRing ?? accent,
262
+ // Derived surfaces + shape the kit reads. Deriving from the palette keeps
263
+ // them following the preset and the light/dark mode.
264
+ '--sg-muted-bg': p.rowHover,
265
+ '--sg-skeleton-bg': p.rowHover,
266
+ '--sg-radius-lg': `${resolved.radius * 2}px`,
267
+ '--sg-rating-on': p.warning ?? status.warning,
268
+ '--sg-rating-empty': p.border,
269
+ '--sg-invalid-fg': p.danger ?? status.danger,
270
+ // The grid's custom scrollbar is a shadow-DOM web component that reads its
271
+ // OWN --sg-scrollbar-* tokens (with fixed light-mode fallbacks). Without
272
+ // these it ignores the theme entirely - a light scrollbar left on a dark
273
+ // grid. CSS custom properties pierce shadow DOM, so defining them here (on
274
+ // the themed container) reaches the scrollbar. Derive them from the palette
275
+ // so the scrollbar tracks the preset + light/dark mode like everything else.
276
+ '--sg-scrollbar-bg': p.bgSubtle ?? p.headerBg,
277
+ '--sg-scrollbar-border': p.border,
278
+ '--sg-scrollbar-thumb': p.muted,
279
+ '--sg-scrollbar-thumb-hover': p.fg,
280
+ '--sg-scrollbar-thumb-active': p.fg,
281
+ '--sg-scrollbar-arrow': p.muted,
282
+ '--sg-scrollbar-arrow-hover': p.fg,
283
+ '--sg-scrollbar-arrow-hover-bg': p.rowHover,
284
+ '--sg-scrollbar-arrow-active': p.fg,
285
+ '--sg-scrollbar-arrow-active-bg': p.selectionBg,
286
+ '--sg-scrollbar-arrow-disabled': p.border,
287
+ }
288
+ }