decantr 0.9.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 (382) hide show
  1. package/AGENTS.md +868 -0
  2. package/CHANGELOG.md +255 -0
  3. package/CLAUDE.md +178 -0
  4. package/LICENSE +21 -0
  5. package/README.md +229 -0
  6. package/cli/art.js +127 -0
  7. package/cli/commands/a11y.js +61 -0
  8. package/cli/commands/audit.js +225 -0
  9. package/cli/commands/build.js +38 -0
  10. package/cli/commands/dev.js +18 -0
  11. package/cli/commands/doctor.js +197 -0
  12. package/cli/commands/figma-sync.js +48 -0
  13. package/cli/commands/figma-tokens.js +55 -0
  14. package/cli/commands/generate.js +26 -0
  15. package/cli/commands/init.js +116 -0
  16. package/cli/commands/lint.js +209 -0
  17. package/cli/commands/mcp.js +530 -0
  18. package/cli/commands/migrate.js +175 -0
  19. package/cli/commands/test.js +38 -0
  20. package/cli/commands/validate.js +354 -0
  21. package/cli/index.js +113 -0
  22. package/package.json +95 -0
  23. package/reference/atoms.md +517 -0
  24. package/reference/behaviors.md +384 -0
  25. package/reference/build-tooling.md +275 -0
  26. package/reference/color-guidelines.md +965 -0
  27. package/reference/component-lifecycle.md +137 -0
  28. package/reference/compound-spacing.md +95 -0
  29. package/reference/decantation-process.md +499 -0
  30. package/reference/dev-server-routes.md +93 -0
  31. package/reference/form-system.md +253 -0
  32. package/reference/i18n.md +336 -0
  33. package/reference/icons.md +576 -0
  34. package/reference/llm-primer.md +953 -0
  35. package/reference/plugins.md +252 -0
  36. package/reference/registry-consumption.md +76 -0
  37. package/reference/router.md +217 -0
  38. package/reference/shells.md +116 -0
  39. package/reference/spatial-guidelines.md +541 -0
  40. package/reference/ssr.md +234 -0
  41. package/reference/state-data.md +215 -0
  42. package/reference/state-patterns.md +166 -0
  43. package/reference/state.md +194 -0
  44. package/reference/style-system.md +110 -0
  45. package/reference/tokens.md +460 -0
  46. package/src/app.js +19 -0
  47. package/src/chart/_animate.js +266 -0
  48. package/src/chart/_base.js +109 -0
  49. package/src/chart/_data.js +209 -0
  50. package/src/chart/_format.js +106 -0
  51. package/src/chart/_interact.js +364 -0
  52. package/src/chart/_palette.js +105 -0
  53. package/src/chart/_renderer.js +52 -0
  54. package/src/chart/_scene.js +262 -0
  55. package/src/chart/_shared.js +371 -0
  56. package/src/chart/index.js +637 -0
  57. package/src/chart/layouts/_layout-base.js +328 -0
  58. package/src/chart/layouts/cartesian.js +148 -0
  59. package/src/chart/layouts/hierarchy.js +562 -0
  60. package/src/chart/layouts/polar.js +101 -0
  61. package/src/chart/renderers/canvas.js +179 -0
  62. package/src/chart/renderers/svg.js +256 -0
  63. package/src/chart/renderers/webgpu.js +715 -0
  64. package/src/chart/types/_type-base.js +26 -0
  65. package/src/chart/types/area.js +134 -0
  66. package/src/chart/types/bar.js +173 -0
  67. package/src/chart/types/box-plot.js +125 -0
  68. package/src/chart/types/bubble.js +63 -0
  69. package/src/chart/types/candlestick.js +115 -0
  70. package/src/chart/types/chord.js +85 -0
  71. package/src/chart/types/combination.js +108 -0
  72. package/src/chart/types/funnel.js +68 -0
  73. package/src/chart/types/gauge.js +163 -0
  74. package/src/chart/types/heatmap.js +98 -0
  75. package/src/chart/types/histogram.js +71 -0
  76. package/src/chart/types/line.js +111 -0
  77. package/src/chart/types/org-chart.js +93 -0
  78. package/src/chart/types/pie.js +81 -0
  79. package/src/chart/types/radar.js +96 -0
  80. package/src/chart/types/radial.js +68 -0
  81. package/src/chart/types/range-area.js +55 -0
  82. package/src/chart/types/range-bar.js +61 -0
  83. package/src/chart/types/sankey.js +73 -0
  84. package/src/chart/types/scatter.js +66 -0
  85. package/src/chart/types/sparkline.js +81 -0
  86. package/src/chart/types/sunburst.js +69 -0
  87. package/src/chart/types/swimlane.js +88 -0
  88. package/src/chart/types/treemap.js +62 -0
  89. package/src/chart/types/waterfall.js +100 -0
  90. package/src/components/_base.js +1658 -0
  91. package/src/components/_behaviors.js +1140 -0
  92. package/src/components/_primitives.js +534 -0
  93. package/src/components/_qr-encoder.js +539 -0
  94. package/src/components/accordion.js +207 -0
  95. package/src/components/affix.js +62 -0
  96. package/src/components/alert-dialog.js +75 -0
  97. package/src/components/alert.js +47 -0
  98. package/src/components/aspect-ratio.js +24 -0
  99. package/src/components/avatar-group.js +55 -0
  100. package/src/components/avatar.js +38 -0
  101. package/src/components/back-top.js +75 -0
  102. package/src/components/badge.js +74 -0
  103. package/src/components/banner.js +68 -0
  104. package/src/components/breadcrumb.js +162 -0
  105. package/src/components/button.js +115 -0
  106. package/src/components/calendar.js +131 -0
  107. package/src/components/card.js +192 -0
  108. package/src/components/carousel.js +98 -0
  109. package/src/components/cascader.js +261 -0
  110. package/src/components/checkbox.js +80 -0
  111. package/src/components/chip.js +81 -0
  112. package/src/components/code-block.js +82 -0
  113. package/src/components/collapsible.js +50 -0
  114. package/src/components/color-palette.js +438 -0
  115. package/src/components/color-picker.js +314 -0
  116. package/src/components/combobox.js +181 -0
  117. package/src/components/command.js +174 -0
  118. package/src/components/comment.js +206 -0
  119. package/src/components/context-menu.js +76 -0
  120. package/src/components/data-table.js +724 -0
  121. package/src/components/date-picker.js +217 -0
  122. package/src/components/date-range-picker.js +244 -0
  123. package/src/components/datetime-picker.js +271 -0
  124. package/src/components/descriptions.js +68 -0
  125. package/src/components/drawer.js +179 -0
  126. package/src/components/dropdown.js +88 -0
  127. package/src/components/empty.js +41 -0
  128. package/src/components/float-button.js +90 -0
  129. package/src/components/form.js +106 -0
  130. package/src/components/hover-card.js +49 -0
  131. package/src/components/icon.js +87 -0
  132. package/src/components/image.js +97 -0
  133. package/src/components/index.js +117 -0
  134. package/src/components/input-group.js +75 -0
  135. package/src/components/input-number.js +155 -0
  136. package/src/components/input-otp.js +178 -0
  137. package/src/components/input.js +91 -0
  138. package/src/components/kbd.js +36 -0
  139. package/src/components/label.js +25 -0
  140. package/src/components/list.js +118 -0
  141. package/src/components/masked-input.js +236 -0
  142. package/src/components/mentions.js +165 -0
  143. package/src/components/menu.js +259 -0
  144. package/src/components/message.js +80 -0
  145. package/src/components/modal.js +147 -0
  146. package/src/components/navigation-menu.js +166 -0
  147. package/src/components/notification.js +84 -0
  148. package/src/components/pagination.js +104 -0
  149. package/src/components/placeholder.js +132 -0
  150. package/src/components/popconfirm.js +70 -0
  151. package/src/components/popover.js +58 -0
  152. package/src/components/progress.js +61 -0
  153. package/src/components/qrcode.js +251 -0
  154. package/src/components/radiogroup.js +120 -0
  155. package/src/components/range-slider.js +176 -0
  156. package/src/components/rate.js +186 -0
  157. package/src/components/resizable.js +83 -0
  158. package/src/components/result.js +57 -0
  159. package/src/components/scroll-area.js +43 -0
  160. package/src/components/segmented.js +97 -0
  161. package/src/components/select.js +165 -0
  162. package/src/components/separator.js +31 -0
  163. package/src/components/shell.js +407 -0
  164. package/src/components/skeleton.js +39 -0
  165. package/src/components/slider.js +141 -0
  166. package/src/components/sortable-list.js +176 -0
  167. package/src/components/space.js +42 -0
  168. package/src/components/spinner.js +112 -0
  169. package/src/components/splitter.js +147 -0
  170. package/src/components/statistic.js +136 -0
  171. package/src/components/steps.js +99 -0
  172. package/src/components/switch.js +95 -0
  173. package/src/components/table.js +44 -0
  174. package/src/components/tabs.js +216 -0
  175. package/src/components/tag.js +115 -0
  176. package/src/components/textarea.js +82 -0
  177. package/src/components/time-picker.js +153 -0
  178. package/src/components/time-range-picker.js +170 -0
  179. package/src/components/timeline.js +226 -0
  180. package/src/components/toast.js +71 -0
  181. package/src/components/toggle.js +213 -0
  182. package/src/components/tooltip.js +57 -0
  183. package/src/components/tour.js +159 -0
  184. package/src/components/transfer.js +163 -0
  185. package/src/components/tree-select.js +274 -0
  186. package/src/components/tree.js +141 -0
  187. package/src/components/typography.js +136 -0
  188. package/src/components/upload.js +118 -0
  189. package/src/components/visually-hidden.js +20 -0
  190. package/src/components/watermark.js +124 -0
  191. package/src/core/index.js +539 -0
  192. package/src/core/lifecycle.js +69 -0
  193. package/src/css/atoms.js +651 -0
  194. package/src/css/components.js +940 -0
  195. package/src/css/derive.js +1296 -0
  196. package/src/css/index.js +265 -0
  197. package/src/css/runtime.js +268 -0
  198. package/src/css/styles/addons/bioluminescent.js +93 -0
  199. package/src/css/styles/addons/clay.js +70 -0
  200. package/src/css/styles/addons/clean.js +57 -0
  201. package/src/css/styles/addons/command-center.js +143 -0
  202. package/src/css/styles/addons/dopamine.js +83 -0
  203. package/src/css/styles/addons/editorial.js +80 -0
  204. package/src/css/styles/addons/glassmorphism.js +99 -0
  205. package/src/css/styles/addons/liquid-glass.js +105 -0
  206. package/src/css/styles/addons/prismatic.js +100 -0
  207. package/src/css/styles/addons/retro.js +63 -0
  208. package/src/css/styles/auradecantism.js +96 -0
  209. package/src/css/theme-registry.js +444 -0
  210. package/src/data/entity.js +281 -0
  211. package/src/data/index.js +13 -0
  212. package/src/data/persist.js +225 -0
  213. package/src/data/query.js +839 -0
  214. package/src/data/realtime.js +299 -0
  215. package/src/data/url.js +177 -0
  216. package/src/data/worker.js +134 -0
  217. package/src/explorer/archetypes.js +243 -0
  218. package/src/explorer/atoms.js +228 -0
  219. package/src/explorer/charts.js +497 -0
  220. package/src/explorer/components.js +129 -0
  221. package/src/explorer/foundations.js +949 -0
  222. package/src/explorer/icons.js +178 -0
  223. package/src/explorer/patterns.js +247 -0
  224. package/src/explorer/recipes.js +194 -0
  225. package/src/explorer/shared/pattern-examples.js +1337 -0
  226. package/src/explorer/shared/showcase-renderer.js +958 -0
  227. package/src/explorer/shared/spec-table.js +41 -0
  228. package/src/explorer/shared/usage-links.js +87 -0
  229. package/src/explorer/shell-config.js +10 -0
  230. package/src/explorer/shells.js +551 -0
  231. package/src/explorer/styles.js +161 -0
  232. package/src/explorer/tokens.js +262 -0
  233. package/src/explorer/tools.js +525 -0
  234. package/src/form/index.js +804 -0
  235. package/src/i18n/index.js +251 -0
  236. package/src/icons/essential.js +479 -0
  237. package/src/icons/index.js +53 -0
  238. package/src/plugins/index.js +282 -0
  239. package/src/registry/archetypes/content-site.json +71 -0
  240. package/src/registry/archetypes/docs-explorer.json +23 -0
  241. package/src/registry/archetypes/ecommerce.json +104 -0
  242. package/src/registry/archetypes/financial-dashboard.json +77 -0
  243. package/src/registry/archetypes/index.json +41 -0
  244. package/src/registry/archetypes/portfolio.json +82 -0
  245. package/src/registry/archetypes/recipe-community.json +159 -0
  246. package/src/registry/archetypes/saas-dashboard.json +86 -0
  247. package/src/registry/architect/cross-cutting.json +45 -0
  248. package/src/registry/architect/domains/ecommerce.json +294 -0
  249. package/src/registry/architect/domains/financial-services.json +302 -0
  250. package/src/registry/architect/index.json +26 -0
  251. package/src/registry/architect/traits.json +379 -0
  252. package/src/registry/atoms.json +16 -0
  253. package/src/registry/chart-showcase.json +160 -0
  254. package/src/registry/chart.json +136 -0
  255. package/src/registry/components.json +8616 -0
  256. package/src/registry/core.json +216 -0
  257. package/src/registry/css.json +319 -0
  258. package/src/registry/data.json +135 -0
  259. package/src/registry/foundations.json +11 -0
  260. package/src/registry/icons.json +463 -0
  261. package/src/registry/index.json +101 -0
  262. package/src/registry/patterns/activity-feed.json +37 -0
  263. package/src/registry/patterns/article-content.json +27 -0
  264. package/src/registry/patterns/auth-form.json +37 -0
  265. package/src/registry/patterns/author-card.json +20 -0
  266. package/src/registry/patterns/card-grid.json +127 -0
  267. package/src/registry/patterns/category-nav.json +26 -0
  268. package/src/registry/patterns/chart-grid.json +36 -0
  269. package/src/registry/patterns/chat-interface.json +37 -0
  270. package/src/registry/patterns/checklist-card.json +55 -0
  271. package/src/registry/patterns/comparison-panel.json +27 -0
  272. package/src/registry/patterns/component-showcase.json +24 -0
  273. package/src/registry/patterns/contact-form.json +31 -0
  274. package/src/registry/patterns/cta-section.json +20 -0
  275. package/src/registry/patterns/data-table.json +37 -0
  276. package/src/registry/patterns/detail-header.json +83 -0
  277. package/src/registry/patterns/detail-panel.json +27 -0
  278. package/src/registry/patterns/explorer-shell.json +22 -0
  279. package/src/registry/patterns/filter-bar.json +33 -0
  280. package/src/registry/patterns/filter-sidebar.json +27 -0
  281. package/src/registry/patterns/form-sections.json +110 -0
  282. package/src/registry/patterns/goal-tracker.json +27 -0
  283. package/src/registry/patterns/hero.json +107 -0
  284. package/src/registry/patterns/index.json +47 -0
  285. package/src/registry/patterns/kpi-grid.json +36 -0
  286. package/src/registry/patterns/media-gallery.json +20 -0
  287. package/src/registry/patterns/order-history.json +20 -0
  288. package/src/registry/patterns/pagination.json +19 -0
  289. package/src/registry/patterns/photo-to-recipe.json +36 -0
  290. package/src/registry/patterns/pipeline-tracker.json +28 -0
  291. package/src/registry/patterns/post-list.json +27 -0
  292. package/src/registry/patterns/pricing-table.json +32 -0
  293. package/src/registry/patterns/scorecard.json +28 -0
  294. package/src/registry/patterns/search-bar.json +20 -0
  295. package/src/registry/patterns/specimen-grid.json +19 -0
  296. package/src/registry/patterns/stat-card.json +55 -0
  297. package/src/registry/patterns/stats-bar.json +55 -0
  298. package/src/registry/patterns/steps-card.json +55 -0
  299. package/src/registry/patterns/table-of-contents.json +19 -0
  300. package/src/registry/patterns/testimonials.json +21 -0
  301. package/src/registry/patterns/timeline.json +27 -0
  302. package/src/registry/patterns/token-inspector.json +21 -0
  303. package/src/registry/patterns/wizard.json +27 -0
  304. package/src/registry/recipe-auradecantism.json +69 -0
  305. package/src/registry/recipe-clean.json +65 -0
  306. package/src/registry/recipe-command-center.json +78 -0
  307. package/src/registry/router.json +73 -0
  308. package/src/registry/schema/README.md +197 -0
  309. package/src/registry/skeletons.json +259 -0
  310. package/src/registry/state.json +137 -0
  311. package/src/registry/tokens.json +40 -0
  312. package/src/router/hash.js +17 -0
  313. package/src/router/history.js +18 -0
  314. package/src/router/index.js +598 -0
  315. package/src/ssr/index.js +922 -0
  316. package/src/state/arrays.js +181 -0
  317. package/src/state/devtools.js +647 -0
  318. package/src/state/index.js +498 -0
  319. package/src/state/middleware.js +288 -0
  320. package/src/state/scheduler.js +206 -0
  321. package/src/state/store.js +300 -0
  322. package/src/tags/index.js +19 -0
  323. package/src/tannins/auth.js +396 -0
  324. package/src/test/dom.js +352 -0
  325. package/src/test/index.js +62 -0
  326. package/src/test/state.js +306 -0
  327. package/tools/a11y-audit.js +487 -0
  328. package/tools/analyzer.js +315 -0
  329. package/tools/audit.js +706 -0
  330. package/tools/builder.js +1422 -0
  331. package/tools/css-extract.js +188 -0
  332. package/tools/dev-server.js +316 -0
  333. package/tools/dts-gen.js +1260 -0
  334. package/tools/figma-components.js +329 -0
  335. package/tools/figma-patterns.js +516 -0
  336. package/tools/figma-plugin/code.js +453 -0
  337. package/tools/figma-plugin/manifest.json +14 -0
  338. package/tools/figma-plugin/ui.html +268 -0
  339. package/tools/figma-render.js +293 -0
  340. package/tools/figma-tokens.js +712 -0
  341. package/tools/figma-upload.js +318 -0
  342. package/tools/generate.js +738 -0
  343. package/tools/icons.js +133 -0
  344. package/tools/init-templates.js +265 -0
  345. package/tools/install-hooks.sh +5 -0
  346. package/tools/migrations/0.5.0.js +53 -0
  347. package/tools/migrations/0.6.0.js +95 -0
  348. package/tools/minify.js +170 -0
  349. package/tools/pre-commit +4 -0
  350. package/tools/registry.js +662 -0
  351. package/tools/reset-playground.js +61 -0
  352. package/tools/starter-templates/content-site/app.js +49 -0
  353. package/tools/starter-templates/content-site/essence.js +19 -0
  354. package/tools/starter-templates/content-site/pages.js +31 -0
  355. package/tools/starter-templates/ecommerce/app.js +50 -0
  356. package/tools/starter-templates/ecommerce/essence.js +19 -0
  357. package/tools/starter-templates/ecommerce/pages.js +31 -0
  358. package/tools/starter-templates/landing-page/app.js +38 -0
  359. package/tools/starter-templates/landing-page/essence.js +18 -0
  360. package/tools/starter-templates/landing-page/pages.js +21 -0
  361. package/tools/starter-templates/portfolio/app.js +45 -0
  362. package/tools/starter-templates/portfolio/essence.js +19 -0
  363. package/tools/starter-templates/portfolio/pages.js +33 -0
  364. package/tools/starter-templates/saas-dashboard/app.js +70 -0
  365. package/tools/starter-templates/saas-dashboard/essence.js +19 -0
  366. package/tools/starter-templates/saas-dashboard/pages.js +31 -0
  367. package/tools/verify-pack.js +203 -0
  368. package/types/chart.d.ts +77 -0
  369. package/types/components.d.ts +587 -0
  370. package/types/core.d.ts +89 -0
  371. package/types/css.d.ts +149 -0
  372. package/types/data.d.ts +238 -0
  373. package/types/form.d.ts +164 -0
  374. package/types/i18n.d.ts +51 -0
  375. package/types/icons.d.ts +27 -0
  376. package/types/index.d.ts +13 -0
  377. package/types/router.d.ts +116 -0
  378. package/types/ssr.d.ts +102 -0
  379. package/types/state.d.ts +83 -0
  380. package/types/tags.d.ts +62 -0
  381. package/types/tannins.d.ts +63 -0
  382. package/types/test.d.ts +48 -0
@@ -0,0 +1,1296 @@
1
+ /**
2
+ * Decantr Design System v2 — Derivation Engine
3
+ * Expands 8-12 seed values + personality traits into 280+ semantic design tokens.
4
+ * Pure functions, zero side effects, zero dependencies.
5
+ * Color math uses OKLCH (perceptually uniform color space).
6
+ *
7
+ * @module derive
8
+ */
9
+
10
+ // ============================================================
11
+ // Color Math (OKLCH — perceptually uniform color space)
12
+ // ============================================================
13
+
14
+ function hexToRgb(hex) {
15
+ hex = hex.replace('#', '');
16
+ if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
17
+ return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)];
18
+ }
19
+
20
+ function rgbToHex(r, g, b) {
21
+ return '#' + [r, g, b].map(c => Math.round(Math.max(0, Math.min(255, c))).toString(16).padStart(2, '0')).join('');
22
+ }
23
+
24
+ /** sRGB [0-255] → linear [0-1] (remove gamma) */
25
+ function linearize(v) {
26
+ v /= 255;
27
+ return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
28
+ }
29
+
30
+ /** linear [0-1] → sRGB [0-255] (apply gamma) */
31
+ function delinearize(v) {
32
+ v = Math.max(0, Math.min(1, v));
33
+ return Math.round((v <= 0.0031308 ? v * 12.92 : 1.055 * v ** (1 / 2.4) - 0.055) * 255);
34
+ }
35
+
36
+ /**
37
+ * Convert sRGB [0-255] to OKLCH [L:0-1, C:0-~0.37, H:0-360].
38
+ * Pipeline: sRGB → linear RGB → LMS (M1) → cube root → OKLAB (M2) → OKLCH.
39
+ */
40
+ function rgbToOklch(r, g, b) {
41
+ const lr = linearize(r), lg = linearize(g), lb = linearize(b);
42
+ // Linear RGB → LMS (M1 matrix, Oklab spec)
43
+ const l = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb;
44
+ const m = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb;
45
+ const s = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb;
46
+ // Cube root
47
+ const l_ = Math.cbrt(l), m_ = Math.cbrt(m), s_ = Math.cbrt(s);
48
+ // LMS' → OKLAB (M2 matrix)
49
+ const L = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_;
50
+ const a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_;
51
+ const bk = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_;
52
+ // OKLAB → OKLCH
53
+ const C = Math.sqrt(a * a + bk * bk);
54
+ let H = Math.atan2(bk, a) * 180 / Math.PI;
55
+ if (H < 0) H += 360;
56
+ return [L, C, H];
57
+ }
58
+
59
+ /**
60
+ * Convert OKLCH [L:0-1, C:0-~0.37, H:0-360] to sRGB [0-255].
61
+ * Pipeline: OKLCH → OKLAB → LMS' (M2 inv) → cube → LMS → linear RGB (M1 inv) → sRGB.
62
+ */
63
+ function oklchToRgb(L, C, H) {
64
+ const hRad = H * Math.PI / 180;
65
+ const a = C * Math.cos(hRad), bk = C * Math.sin(hRad);
66
+ // OKLAB → LMS' (M2 inverse)
67
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * bk;
68
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * bk;
69
+ const s_ = L - 0.0894841775 * a - 1.2914855480 * bk;
70
+ // Cube
71
+ const l = l_ * l_ * l_, m = m_ * m_ * m_, s = s_ * s_ * s_;
72
+ // LMS → linear RGB (M1 inverse)
73
+ const lr = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
74
+ const lg = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
75
+ const lb = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
76
+ return [delinearize(lr), delinearize(lg), delinearize(lb)];
77
+ }
78
+
79
+ /** Clamp OKLCH to sRGB gamut by reducing chroma via binary search */
80
+ function gamutMap(L, C, H) {
81
+ if (C < 0.001) return [L, 0, H];
82
+ const hRad = H * Math.PI / 180;
83
+ const cosH = Math.cos(hRad), sinH = Math.sin(hRad);
84
+ const e = 0.001;
85
+ function inGamut(c) {
86
+ const a = c * cosH, b = c * sinH;
87
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
88
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
89
+ const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
90
+ const l = l_ * l_ * l_, m = m_ * m_ * m_, s = s_ * s_ * s_;
91
+ const lr = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
92
+ if (lr < -e || lr > 1 + e) return false;
93
+ const lg = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
94
+ if (lg < -e || lg > 1 + e) return false;
95
+ const lb = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
96
+ return lb >= -e && lb <= 1 + e;
97
+ }
98
+ if (inGamut(C)) return [L, C, H];
99
+ let lo = 0, hi = C;
100
+ for (let i = 0; i < 20; i++) {
101
+ const mid = (lo + hi) / 2;
102
+ if (inGamut(mid)) lo = mid; else hi = mid;
103
+ }
104
+ return [L, lo, H];
105
+ }
106
+
107
+ /** WCAG 2.1 relative luminance (sRGB-based per spec) */
108
+ function luminance(r, g, b) {
109
+ const [rs, gs, bs] = [r, g, b].map(c => {
110
+ c /= 255;
111
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
112
+ });
113
+ return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
114
+ }
115
+
116
+ /** WCAG 2.1 contrast ratio */
117
+ function contrast(rgb1, rgb2) {
118
+ const l1 = luminance(...rgb1), l2 = luminance(...rgb2);
119
+ return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
120
+ }
121
+
122
+ /** Darken by adjusting OKLCH L channel (amount: 0-100 scale, maps to 0-1 L) */
123
+ function darken(hex, amount) {
124
+ const [L, C, H] = rgbToOklch(...hexToRgb(hex));
125
+ const [gL, gC, gH] = gamutMap(Math.max(0, L - amount / 100), C, H);
126
+ return rgbToHex(...oklchToRgb(gL, gC, gH));
127
+ }
128
+
129
+ /** Lighten by adjusting OKLCH L channel (amount: 0-100 scale, maps to 0-1 L) */
130
+ function lighten(hex, amount) {
131
+ const [L, C, H] = rgbToOklch(...hexToRgb(hex));
132
+ const [gL, gC, gH] = gamutMap(Math.min(1, L + amount / 100), C, H);
133
+ return rgbToHex(...oklchToRgb(gL, gC, gH));
134
+ }
135
+
136
+ function alpha(hex, opacity) {
137
+ const [r, g, b] = hexToRgb(hex);
138
+ return `rgba(${r},${g},${b},${opacity})`;
139
+ }
140
+
141
+ /** Parse `rgba(R,G,B,A)` → [r,g,b,a] or null if not rgba */
142
+ function parseRgba(str) {
143
+ if (!str || !str.startsWith('rgba(')) return null;
144
+ const m = str.match(/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)/);
145
+ return m ? [+m[1], +m[2], +m[3], +m[4]] : null;
146
+ }
147
+
148
+ /** Composite an rgba color onto an opaque hex background (Porter-Duff source-over). Returns opaque hex. */
149
+ function compositeOnBg(value, bgHex) {
150
+ if (!value) return value;
151
+ if (value.startsWith('#')) return value;
152
+ const parsed = parseRgba(value);
153
+ if (!parsed) return value;
154
+ const [sr, sg, sb, sa] = parsed;
155
+ const [br, bg, bb] = hexToRgb(bgHex);
156
+ const r = Math.round(sr * sa + br * (1 - sa));
157
+ const g = Math.round(sg * sa + bg * (1 - sa));
158
+ const b = Math.round(sb * sa + bb * (1 - sa));
159
+ return rgbToHex(r, g, b);
160
+ }
161
+
162
+ /** Mix two colors in OKLCH space (perceptually uniform interpolation) */
163
+ function mixColors(hex1, hex2, weight) {
164
+ const [L1, C1, H1] = rgbToOklch(...hexToRgb(hex1));
165
+ const [L2, C2, H2] = rgbToOklch(...hexToRgb(hex2));
166
+ const L = L1 + (L2 - L1) * weight;
167
+ const C = C1 + (C2 - C1) * weight;
168
+ // Hue interpolation along short arc; handle achromatic
169
+ let H;
170
+ if (C1 < 0.001) H = H2;
171
+ else if (C2 < 0.001) H = H1;
172
+ else {
173
+ let dH = H2 - H1;
174
+ if (dH > 180) dH -= 360;
175
+ if (dH < -180) dH += 360;
176
+ H = (H1 + dH * weight + 360) % 360;
177
+ }
178
+ const [gL, gC, gH] = gamutMap(L, C, H);
179
+ return rgbToHex(...oklchToRgb(gL, gC, gH));
180
+ }
181
+
182
+ /** Pick foreground (white or near-black) for WCAG AA on given background */
183
+ function pickForeground(bgHex) {
184
+ const bgRgb = hexToRgb(bgHex);
185
+ return contrast([255, 255, 255], bgRgb) >= 4.5 ? '#ffffff' : '#09090b';
186
+ }
187
+
188
+ /** Rotate hue by degrees in OKLCH space */
189
+ function rotateHue(hex, degrees) {
190
+ const [L, C, H] = rgbToOklch(...hexToRgb(hex));
191
+ const [gL, gC, gH] = gamutMap(L, C, (H + degrees + 360) % 360);
192
+ return rgbToHex(...oklchToRgb(gL, gC, gH));
193
+ }
194
+
195
+ // Export color utils for testing and style overrides
196
+ export { hexToRgb, rgbToHex, rgbToOklch, oklchToRgb, gamutMap, darken, lighten, alpha, parseRgba, compositeOnBg, mixColors, pickForeground, rotateHue, contrast, validateContrast, adjustForContrast, transformSeedsForCVD, deriveChrome };
197
+
198
+ // ============================================================
199
+ // Contrast Validation (WCAG AA)
200
+ // ============================================================
201
+
202
+ /**
203
+ * Validates and auto-adjusts color pairs that fail WCAG AA contrast.
204
+ * Text pairs validated at 4.5:1, non-text (borders) at 3:1.
205
+ * @param {Object} tokens - The full token map
206
+ * @returns {Object} tokens with any necessary adjustments
207
+ */
208
+ function validateContrast(tokens) {
209
+ const PAIRS = [
210
+ // [foreground-token, background-token, min-ratio]
211
+ // --- Text contrast (4.5:1) ---
212
+ ['--d-primary-fg', '--d-primary', 4.5],
213
+ ['--d-accent-fg', '--d-accent', 4.5],
214
+ ['--d-tertiary-fg', '--d-tertiary', 4.5],
215
+ ['--d-success-fg', '--d-success', 4.5],
216
+ ['--d-warning-fg', '--d-warning', 4.5],
217
+ ['--d-error-fg', '--d-error', 4.5],
218
+ ['--d-info-fg', '--d-info', 4.5],
219
+ ['--d-fg', '--d-bg', 4.5],
220
+ ['--d-muted-fg', '--d-bg', 4.5],
221
+ ['--d-surface-0-fg', '--d-surface-0', 4.5],
222
+ ['--d-surface-1-fg', '--d-surface-1', 4.5],
223
+ ['--d-surface-2-fg', '--d-surface-2', 4.5],
224
+ ['--d-surface-3-fg', '--d-surface-3', 4.5],
225
+ // Subtle foreground on subtle background
226
+ ['--d-primary-subtle-fg', '--d-primary-subtle', 4.5],
227
+ ['--d-accent-subtle-fg', '--d-accent-subtle', 4.5],
228
+ ['--d-tertiary-subtle-fg', '--d-tertiary-subtle', 4.5],
229
+ ['--d-error-subtle-fg', '--d-error-subtle', 4.5],
230
+ ['--d-success-subtle-fg', '--d-success-subtle', 4.5],
231
+ ['--d-warning-subtle-fg', '--d-warning-subtle', 4.5],
232
+ ['--d-info-subtle-fg', '--d-info-subtle', 4.5],
233
+ // --- on-subtle text contrast (4.5:1) — role color on subtle bg ---
234
+ ['--d-primary-on-subtle', '--d-primary-subtle', 4.5],
235
+ ['--d-accent-on-subtle', '--d-accent-subtle', 4.5],
236
+ ['--d-tertiary-on-subtle', '--d-tertiary-subtle', 4.5],
237
+ ['--d-success-on-subtle', '--d-success-subtle', 4.5],
238
+ ['--d-warning-on-subtle', '--d-warning-subtle', 4.5],
239
+ ['--d-error-on-subtle', '--d-error-subtle', 4.5],
240
+ ['--d-info-on-subtle', '--d-info-subtle', 4.5],
241
+ // --- Body text on subtle backgrounds (alerts, tables) ---
242
+ ['--d-fg', '--d-primary-subtle', 4.5],
243
+ ['--d-fg', '--d-accent-subtle', 4.5],
244
+ ['--d-fg', '--d-tertiary-subtle', 4.5],
245
+ ['--d-fg', '--d-success-subtle', 4.5],
246
+ ['--d-fg', '--d-warning-subtle', 4.5],
247
+ ['--d-fg', '--d-error-subtle', 4.5],
248
+ ['--d-fg', '--d-info-subtle', 4.5],
249
+ // --- Non-text contrast (3:1, WCAG 1.4.11) ---
250
+ // NOTE: --d-border and --d-surface-N-border are decorative (card edges,
251
+ // dividers, separators) — not "UI component boundaries required for
252
+ // identification" per WCAG 1.4.11. Enforcing 3:1 here overrides the
253
+ // intentionally-subtle rgba values set by glass styles (auradecantism,
254
+ // glassmorphism) making all borders far too bright. Field borders
255
+ // (--d-field-border) handle accessible form controls separately.
256
+ // --- Role border visibility against page (3:1) ---
257
+ ['--d-primary-border', '--d-bg', 3],
258
+ ['--d-accent-border', '--d-bg', 3],
259
+ ['--d-tertiary-border', '--d-bg', 3],
260
+ ['--d-success-border', '--d-bg', 3],
261
+ ['--d-warning-border', '--d-bg', 3],
262
+ ['--d-error-border', '--d-bg', 3],
263
+ ['--d-info-border', '--d-bg', 3],
264
+ // --- Chrome contrast ---
265
+ ['--d-chrome-fg', '--d-chrome-bg', 4.5],
266
+ // --- Field & selection contrast ---
267
+ ['--d-field-placeholder', '--d-field-bg', 3],
268
+ ['--d-selected-fg', '--d-selected-bg', 4.5],
269
+ ];
270
+
271
+ const pageBg = tokens['--d-bg'] || '#ffffff';
272
+ for (const [fgKey, bgKey, minRatio] of PAIRS) {
273
+ const fg = tokens[fgKey];
274
+ const bg = tokens[bgKey];
275
+ if (!fg || !bg) continue;
276
+
277
+ // Resolve rgba values by compositing onto page background
278
+ const effectiveFg = compositeOnBg(fg, pageBg);
279
+ const effectiveBg = compositeOnBg(bg, pageBg);
280
+
281
+ // Skip if either value can't be resolved to hex (e.g. var() references)
282
+ if (!effectiveFg || !effectiveFg.startsWith('#') || !effectiveBg || !effectiveBg.startsWith('#')) continue;
283
+
284
+ const ratio = contrast(hexToRgb(effectiveFg), hexToRgb(effectiveBg));
285
+
286
+ if (ratio < minRatio) {
287
+ if (fg.startsWith('#')) {
288
+ // Hex fg — adjust lightness against effective bg
289
+ tokens[fgKey] = adjustForContrast(fg, effectiveBg, minRatio);
290
+ } else {
291
+ // Rgba fg (border tokens) — bump alpha until contrast passes
292
+ const parsed = parseRgba(fg);
293
+ if (parsed) {
294
+ let [r, g, b, a] = parsed;
295
+ let found = false;
296
+ for (let i = 0; i < 20 && a < 1; i++) {
297
+ a = Math.min(1, a + 0.05);
298
+ const newVal = `rgba(${r},${g},${b},${parseFloat(a.toFixed(2))})`;
299
+ const newFg = compositeOnBg(newVal, pageBg);
300
+ if (newFg.startsWith('#') && contrast(hexToRgb(newFg), hexToRgb(effectiveBg)) >= minRatio) {
301
+ tokens[fgKey] = newVal;
302
+ found = true;
303
+ break;
304
+ }
305
+ }
306
+ // If alpha=1 still fails (bright colors on white), darken the color
307
+ if (!found) {
308
+ const opaqueHex = rgbToHex(r, g, b);
309
+ tokens[fgKey] = adjustForContrast(opaqueHex, effectiveBg, minRatio);
310
+ }
311
+ }
312
+ }
313
+ }
314
+ }
315
+
316
+ return tokens;
317
+ }
318
+
319
+ /** Adjust foreground color via OKLCH L until contrast meets target */
320
+ function adjustForContrast(fgHex, bgHex, targetRatio) {
321
+ const bgRgb = hexToRgb(bgHex);
322
+ const bgLum = luminance(...bgRgb);
323
+ let [L, C, H] = rgbToOklch(...hexToRgb(fgHex));
324
+
325
+ // If bg is dark, lighten fg; if bg is light, darken fg
326
+ const direction = bgLum < 0.5 ? 1 : -1;
327
+
328
+ for (let i = 0; i < 50; i++) {
329
+ L = Math.max(0, Math.min(1, L + direction * 0.02));
330
+ const [gL, gC, gH] = gamutMap(L, C, H);
331
+ const adjusted = rgbToHex(...oklchToRgb(gL, gC, gH));
332
+ const adjRgb = hexToRgb(adjusted);
333
+ if (contrast(adjRgb, bgRgb) >= targetRatio) return adjusted;
334
+ }
335
+
336
+ return bgLum < 0.5 ? '#ffffff' : '#09090b';
337
+ }
338
+
339
+ // ============================================================
340
+ // Personality Presets
341
+ // ============================================================
342
+
343
+ const RADIUS = {
344
+ sharp: { sm: '2px', default: '0', lg: '0', full: '9999px', panel: '0', inner: '0' },
345
+ rounded: { sm: '4px', default: '8px', lg: '12px', full: '9999px', panel: '8px', inner: '6px' },
346
+ pill: { sm: '9999px', default: '9999px', lg: '24px', full: '9999px', panel: '16px', inner: '14px' },
347
+ };
348
+
349
+ const ELEVATION = {
350
+ flat: {
351
+ light: ['none', 'none', 'none', 'none'],
352
+ dark: ['none', 'none', 'none', 'none'],
353
+ },
354
+ subtle: {
355
+ light: [
356
+ 'none',
357
+ '0 1px 3px rgba(0,0,0,0.12),0 1px 2px rgba(0,0,0,0.06)',
358
+ '0 4px 12px rgba(0,0,0,0.12),0 2px 4px rgba(0,0,0,0.06)',
359
+ '0 12px 32px rgba(0,0,0,0.18),0 4px 8px rgba(0,0,0,0.09)',
360
+ ],
361
+ dark: [
362
+ 'none',
363
+ '0 1px 3px rgba(0,0,0,0.3),0 1px 2px rgba(0,0,0,0.2)',
364
+ '0 4px 12px rgba(0,0,0,0.3),0 2px 4px rgba(0,0,0,0.2)',
365
+ '0 12px 32px rgba(0,0,0,0.4),0 4px 8px rgba(0,0,0,0.3)',
366
+ ],
367
+ },
368
+ raised: {
369
+ light: [
370
+ 'none',
371
+ '0 2px 6px rgba(0,0,0,0.15),0 1px 3px rgba(0,0,0,0.09)',
372
+ '0 8px 24px rgba(0,0,0,0.18),0 4px 8px rgba(0,0,0,0.09)',
373
+ '0 20px 48px rgba(0,0,0,0.24),0 8px 16px rgba(0,0,0,0.12)',
374
+ ],
375
+ dark: [
376
+ 'none',
377
+ '0 2px 6px rgba(0,0,0,0.4),0 1px 3px rgba(0,0,0,0.3)',
378
+ '0 8px 24px rgba(0,0,0,0.4),0 4px 8px rgba(0,0,0,0.3)',
379
+ '0 20px 48px rgba(0,0,0,0.5),0 8px 16px rgba(0,0,0,0.4)',
380
+ ],
381
+ },
382
+ glass: {
383
+ light: [
384
+ 'none',
385
+ '0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06)',
386
+ '0 4px 16px rgba(0,0,0,0.12), 0 2px 4px rgba(0,0,0,0.06)',
387
+ '0 12px 40px rgba(0,0,0,0.16), 0 4px 8px rgba(0,0,0,0.08)',
388
+ ],
389
+ dark: [
390
+ 'none',
391
+ '0 1px 3px rgba(0,0,0,0.2)',
392
+ '0 4px 16px rgba(0,0,0,0.3)',
393
+ '0 12px 40px rgba(0,0,0,0.4)',
394
+ ],
395
+ },
396
+ brutalist: {
397
+ light: ['none', '2px 2px 0 var(--d-fg)', '4px 4px 0 var(--d-fg)', '6px 6px 0 var(--d-fg)'],
398
+ dark: ['none', '2px 2px 0 var(--d-fg)', '4px 4px 0 var(--d-fg)', '6px 6px 0 var(--d-fg)'],
399
+ },
400
+ clay: {
401
+ light: [
402
+ 'none',
403
+ 'inset 0 -4px 6px rgba(0,0,0,0.06), 0 4px 8px rgba(0,0,0,0.08)',
404
+ 'inset 0 -6px 10px rgba(0,0,0,0.08), 0 8px 16px rgba(0,0,0,0.1)',
405
+ 'inset 0 -8px 14px rgba(0,0,0,0.1), 0 12px 24px rgba(0,0,0,0.12)',
406
+ ],
407
+ dark: [
408
+ 'none',
409
+ 'inset 0 -4px 6px rgba(0,0,0,0.2), 0 4px 8px rgba(0,0,0,0.25)',
410
+ 'inset 0 -6px 10px rgba(0,0,0,0.25), 0 8px 16px rgba(0,0,0,0.3)',
411
+ 'inset 0 -8px 14px rgba(0,0,0,0.3), 0 12px 24px rgba(0,0,0,0.35)',
412
+ ],
413
+ },
414
+ glow: {
415
+ light: [
416
+ 'none',
417
+ '0 0 8px rgba(0,0,0,0.08), 0 0 2px rgba(0,0,0,0.04)',
418
+ '0 0 16px rgba(0,0,0,0.1), 0 0 4px rgba(0,0,0,0.06)',
419
+ '0 0 32px rgba(0,0,0,0.12), 0 0 8px rgba(0,0,0,0.08)',
420
+ ],
421
+ dark: [
422
+ 'none',
423
+ '0 0 8px rgba(0,255,213,0.12), 0 0 2px rgba(0,255,213,0.08)',
424
+ '0 0 16px rgba(0,255,213,0.18), 0 0 4px rgba(0,255,213,0.12)',
425
+ '0 0 32px rgba(0,255,213,0.22), 0 0 8px rgba(0,255,213,0.15)',
426
+ ],
427
+ },
428
+ };
429
+
430
+ const SURFACE_BLUR = {
431
+ flat: ['none', 'none', 'none'],
432
+ subtle: ['none', 'none', 'none'],
433
+ raised: ['none', 'none', 'none'],
434
+ glass: ['blur(8px)', 'blur(12px)', 'blur(16px)'],
435
+ brutalist: ['none', 'none', 'none'],
436
+ clay: ['none', 'none', 'none'],
437
+ glow: ['none', 'none', 'none'],
438
+ };
439
+
440
+ const MOTION = {
441
+ instant: {
442
+ instant: '0ms', fast: '0ms', normal: '50ms', slow: '100ms', spin: '200ms',
443
+ standard: 'ease', decelerate: 'ease-out', accelerate: 'ease-in', bounce: 'steps(1)',
444
+ },
445
+ snappy: {
446
+ instant: '30ms', fast: '80ms', normal: '120ms', slow: '200ms', spin: '500ms',
447
+ standard: 'ease', decelerate: 'ease-out', accelerate: 'ease-in', bounce: 'steps(2)',
448
+ },
449
+ smooth: {
450
+ instant: '50ms', fast: '150ms', normal: '250ms', slow: '400ms', spin: '850ms',
451
+ standard: 'cubic-bezier(0.4,0,0.2,1)', decelerate: 'cubic-bezier(0,0,0.2,1)',
452
+ accelerate: 'cubic-bezier(0.4,0,1,1)', bounce: 'cubic-bezier(0.34,1.56,0.64,1)',
453
+ },
454
+ bouncy: {
455
+ instant: '50ms', fast: '150ms', normal: '300ms', slow: '500ms', spin: '1000ms',
456
+ standard: 'cubic-bezier(0.22,1,0.36,1)', decelerate: 'cubic-bezier(0,0,0.2,1)',
457
+ accelerate: 'cubic-bezier(0.4,0,1,1)', bounce: 'cubic-bezier(0.34,1.56,0.64,1)',
458
+ },
459
+ };
460
+
461
+ /** Interaction feel — derived from elevation personality */
462
+ const INTERACTION = {
463
+ subtle: {
464
+ hoverTranslate: '0, -1px', hoverBrightness: '1',
465
+ activeScale: '0.98', activeTranslate: '0, 0',
466
+ },
467
+ raised: {
468
+ hoverTranslate: '0, -2px', hoverBrightness: '1.02',
469
+ activeScale: '0.97', activeTranslate: '0, 1px',
470
+ },
471
+ flat: {
472
+ hoverTranslate: '0, 0', hoverBrightness: '1.05',
473
+ activeScale: '0.98', activeTranslate: '0, 0',
474
+ },
475
+ brutalist: {
476
+ hoverTranslate: '-2px, -2px', hoverBrightness: '1',
477
+ activeScale: '1', activeTranslate: '2px, 2px',
478
+ },
479
+ clay: {
480
+ hoverTranslate: '0, -2px', hoverBrightness: '1.03',
481
+ activeScale: '0.96', activeTranslate: '0, 2px',
482
+ },
483
+ glow: {
484
+ hoverTranslate: '0, -1px', hoverBrightness: '1.05',
485
+ activeScale: '0.98', activeTranslate: '0, 0',
486
+ },
487
+ };
488
+
489
+ const ELEVATION_TO_INTERACTION = {
490
+ flat: 'flat', subtle: 'subtle', raised: 'raised', glass: 'subtle', brutalist: 'brutalist', clay: 'clay', glow: 'glow',
491
+ };
492
+
493
+ const BORDER = {
494
+ none: { width: '0', widthStrong: '0', style: 'none' },
495
+ thin: { width: '1px', widthStrong: '2px', style: 'solid' },
496
+ bold: { width: '2px', widthStrong: '3px', style: 'solid' },
497
+ };
498
+
499
+ /** Palette derivation tuning per elevation type — controls hover/active shifts and alpha values */
500
+ const PALETTE_TUNING = {
501
+ flat: { hoverShift: 8, activeShift: 15, subtleAlphaLight: 0.10, subtleAlphaDark: 0.15, borderAlphaLight: 0.30, borderAlphaDark: 0.40 },
502
+ subtle: { hoverShift: 8, activeShift: 15, subtleAlphaLight: 0.10, subtleAlphaDark: 0.15, borderAlphaLight: 0.30, borderAlphaDark: 0.40 },
503
+ raised: { hoverShift: 8, activeShift: 15, subtleAlphaLight: 0.10, subtleAlphaDark: 0.15, borderAlphaLight: 0.30, borderAlphaDark: 0.40 },
504
+ glass: { hoverShift: 5, activeShift: 10, subtleAlphaLight: 0.20, subtleAlphaDark: 0.25, borderAlphaLight: 0.20, borderAlphaDark: 0.30 },
505
+ brutalist: { hoverShift: 12, activeShift: 20, subtleAlphaLight: 0.15, subtleAlphaDark: 0.20, borderAlphaLight: 0.80, borderAlphaDark: 0.80 },
506
+ clay: { hoverShift: 6, activeShift: 12, subtleAlphaLight: 0.12, subtleAlphaDark: 0.18, borderAlphaLight: 0.15, borderAlphaDark: 0.20 },
507
+ glow: { hoverShift: 8, activeShift: 15, subtleAlphaLight: 0.10, subtleAlphaDark: 0.15, borderAlphaLight: 0.25, borderAlphaDark: 0.35 },
508
+ };
509
+
510
+ const DENSITY = {
511
+ compact: {
512
+ padX: 'var(--d-field-px-sm)', padY: 'var(--d-field-py-sm)', gap: 'var(--d-field-gap-sm)',
513
+ minH: 'var(--d-field-h-sm)', text: 'var(--d-field-text-sm)',
514
+ compoundPad: 'var(--d-sp-3)', compoundGap: 'var(--d-sp-2)',
515
+ },
516
+ comfortable: {
517
+ padX: 'var(--d-field-px-md)', padY: 'var(--d-field-py-md)', gap: 'var(--d-field-gap-md)',
518
+ minH: 'var(--d-field-h-md)', text: 'var(--d-field-text-md)',
519
+ compoundPad: 'var(--d-sp-5)', compoundGap: 'var(--d-sp-3)',
520
+ },
521
+ spacious: {
522
+ padX: 'var(--d-field-px-lg)', padY: 'var(--d-field-py-lg)', gap: 'var(--d-field-gap-lg)',
523
+ minH: 'var(--d-field-h-lg)', text: 'var(--d-field-text-lg)',
524
+ compoundPad: 'var(--d-sp-8)', compoundGap: 'var(--d-sp-4)',
525
+ },
526
+ };
527
+
528
+ // ============================================================
529
+ // Defaults
530
+ // ============================================================
531
+
532
+ export const defaultSeed = {
533
+ primary: '#1366D9',
534
+ accent: '#7c3aed',
535
+ tertiary: '#0891b2',
536
+ neutral: '#71717a',
537
+ success: '#22c55e',
538
+ warning: '#f59e0b',
539
+ error: '#ef4444',
540
+ info: '#3b82f6',
541
+ bg: '#ffffff',
542
+ bgDark: '#0a0a0a',
543
+ };
544
+
545
+ export const defaultPersonality = {
546
+ radius: 'rounded',
547
+ elevation: 'subtle',
548
+ motion: 'smooth',
549
+ borders: 'thin',
550
+ density: 'comfortable',
551
+ gradient: 'none',
552
+ palette: 'standard',
553
+ };
554
+
555
+ /**
556
+ * Derive monochrome seed colors from a single primary hue.
557
+ * All 7 role colors are constrained within ±20° of the primary hue in OKLCH space,
558
+ * distinguished by lightness and chroma shifts.
559
+ * @param {string} primaryHex - The base color hex
560
+ * @returns {Object} Seed overrides for accent, tertiary, success, warning, error, info
561
+ */
562
+ export function deriveMonochromeSeed(primaryHex) {
563
+ const [L, C, H] = rgbToOklch(...hexToRgb(primaryHex));
564
+ const gc = (l, c, h) => { const [gL, gC, gH] = gamutMap(l, c, (h + 360) % 360); return rgbToHex(...oklchToRgb(gL, gC, gH)); };
565
+ return {
566
+ accent: gc(Math.min(1, L + 0.08), C * 0.8, H + 15),
567
+ tertiary: gc(Math.max(0, L - 0.05), C * 0.7, H - 15),
568
+ success: gc(Math.min(1, L + 0.12), C * 0.9, H + 10),
569
+ warning: gc(Math.min(1, L + 0.05), C * 1.1, H - 8),
570
+ error: gc(Math.max(0, L - 0.03), C * 1.2, H - 20),
571
+ info: gc(Math.min(1, L + 0.15), C * 0.6, H + 5),
572
+ };
573
+ }
574
+
575
+ // ============================================================
576
+ // Shared Typography & Spacing (stable across styles/modes)
577
+ // ============================================================
578
+
579
+ const TYPOGRAPHY = {
580
+ '--d-font': 'system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif',
581
+ '--d-font-mono': 'ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace',
582
+ '--d-text-xs': '0.625rem', '--d-text-sm': '0.75rem', '--d-text-base': '0.875rem',
583
+ '--d-text-md': '1rem', '--d-text-lg': '1.125rem', '--d-text-xl': '1.25rem',
584
+ '--d-text-2xl': '1.5rem', '--d-text-3xl': '2rem', '--d-text-4xl': '2.5rem',
585
+ '--d-lh-none': '1', '--d-lh-tight': '1.1', '--d-lh-snug': '1.25',
586
+ '--d-lh-normal': '1.5', '--d-lh-relaxed': '1.6', '--d-lh-loose': '1.75',
587
+ '--d-fw-heading': '700', '--d-fw-title': '600', '--d-fw-medium': '500',
588
+ '--d-ls-heading': '-0.025em',
589
+ '--d-ls-caps': '0.05em',
590
+ };
591
+
592
+ const SPACING = {
593
+ '--d-sp-0-5': '0.125rem', '--d-sp-1': '0.25rem', '--d-sp-1-5': '0.375rem', '--d-sp-2': '0.5rem',
594
+ '--d-sp-2-5': '0.625rem', '--d-sp-3': '0.75rem', '--d-sp-4': '1rem',
595
+ '--d-sp-5': '1.25rem', '--d-sp-6': '1.5rem', '--d-sp-8': '2rem',
596
+ '--d-sp-10': '2.5rem', '--d-sp-12': '3rem', '--d-sp-16': '4rem',
597
+ '--d-pad': '1.25rem',
598
+ // Compound component spacing contract (Card, Modal, AlertDialog, Drawer)
599
+ '--d-compound-gap': 'var(--d-sp-3)',
600
+ '--d-compound-pad': 'var(--d-pad)',
601
+ // Popup offset tokens — distance between trigger and floating layer
602
+ '--d-offset-dropdown': '2px',
603
+ '--d-offset-menu': '4px',
604
+ '--d-offset-tooltip': '6px',
605
+ '--d-offset-popover': '8px',
606
+ '--d-offset-tour': '12px',
607
+ // Dropdown panel max-height — unified for all select-like components
608
+ '--d-panel-max-h': '240px',
609
+ // Tree indentation per level
610
+ '--d-tree-indent': '1em',
611
+ // ── Field sizing contract (height-first, 4-tier scale) ──
612
+ // Heights — primary constraint; padding derives from these
613
+ '--d-field-h-xs': '1.5rem', // 24px
614
+ '--d-field-h-sm': '1.75rem', // 28px
615
+ '--d-field-h-md': '2.25rem', // 36px (= density comfortable)
616
+ '--d-field-h-lg': '2.75rem', // 44px (= density spacious)
617
+ // Vertical padding per tier
618
+ '--d-field-py-xs': 'var(--d-sp-1)', // 4px
619
+ '--d-field-py-sm': 'var(--d-sp-1)', // 4px
620
+ '--d-field-py-md': 'var(--d-sp-2)', // 8px
621
+ '--d-field-py-lg': 'var(--d-sp-2-5)', // 10px
622
+ // Horizontal padding per tier
623
+ '--d-field-px-xs': 'var(--d-sp-2)', // 8px
624
+ '--d-field-px-sm': 'var(--d-sp-2-5)', // 10px
625
+ '--d-field-px-md': 'var(--d-sp-4)', // 16px
626
+ '--d-field-px-lg': 'var(--d-sp-6)', // 24px
627
+ // Font size per tier
628
+ '--d-field-text-xs': 'var(--d-text-xs)', // 10px
629
+ '--d-field-text-sm': 'var(--d-text-sm)', // 12px
630
+ '--d-field-text-md': 'var(--d-text-base)', // 14px
631
+ '--d-field-text-lg': 'var(--d-text-md)', // 16px
632
+ // Gap per tier
633
+ '--d-field-gap-xs': 'var(--d-sp-1)', // 4px
634
+ '--d-field-gap-sm': 'var(--d-sp-1-5)', // 6px
635
+ '--d-field-gap-md': 'var(--d-sp-2)', // 8px
636
+ '--d-field-gap-lg': 'var(--d-sp-2-5)', // 10px
637
+ // ── Switch track/thumb dimensions per tier ──
638
+ '--d-switch-w-xs': '1.5rem', '--d-switch-h-xs': '0.875rem', '--d-switch-thumb-xs': '0.625rem',
639
+ '--d-switch-w-sm': '1.75rem', '--d-switch-h-sm': '1rem', '--d-switch-thumb-sm': '0.75rem',
640
+ '--d-switch-w': '2.5rem', '--d-switch-h': '1.375rem', '--d-switch-thumb': '1rem',
641
+ '--d-switch-w-lg': '3.25rem', '--d-switch-h-lg': '1.75rem', '--d-switch-thumb-lg': '1.25rem',
642
+ // ── Checkbox/Radio indicator size per tier ──
643
+ '--d-checkbox-size-xs': '0.875rem', // 14px
644
+ '--d-checkbox-size-sm': '1rem', // 16px
645
+ '--d-checkbox-size': '1.125rem', // 18px (default)
646
+ '--d-checkbox-size-lg': '1.375rem', // 22px
647
+ // ── Rate star size per tier ──
648
+ '--d-rate-size-sm': 'var(--d-text-md)', // 1rem
649
+ '--d-rate-size': 'var(--d-text-xl)', // 1.25rem
650
+ '--d-rate-size-lg': '1.75rem', // between text-2xl and text-3xl
651
+ '--d-rate-gap-sm': 'var(--d-sp-0-5)', // tighter gap for small
652
+ // ── OTP slot dimensions per tier ──
653
+ '--d-otp-w-sm': '2rem', '--d-otp-h-sm': '2rem', '--d-otp-text-sm': 'var(--d-text-base)',
654
+ '--d-otp-w': '2.5rem', '--d-otp-h': '2.5rem', '--d-otp-text': 'var(--d-text-lg)',
655
+ '--d-otp-w-lg': '3rem', '--d-otp-h-lg': '3rem', '--d-otp-text-lg': 'var(--d-text-xl)',
656
+ // InputNumber stepper button width
657
+ '--d-stepper-w': '2rem',
658
+ // ── Component anatomy tokens ──
659
+ // Avatar sizes
660
+ '--d-avatar-size-sm': '24px', '--d-avatar-size': '36px',
661
+ '--d-avatar-size-lg': '48px', '--d-avatar-size-xl': '64px',
662
+ // Spinner sizes
663
+ '--d-spinner-size-xs': '12px', '--d-spinner-size-sm': '16px',
664
+ '--d-spinner-size': '20px',
665
+ '--d-spinner-size-lg': '28px', '--d-spinner-size-xl': '36px',
666
+ // Progress bar heights
667
+ '--d-progress-h': '8px', '--d-progress-h-sm': '4px',
668
+ '--d-progress-h-md': '16px', '--d-progress-h-lg': '24px',
669
+ // Slider geometry
670
+ '--d-slider-thumb': '18px', '--d-slider-track-h': '6px',
671
+ // Indicator dots
672
+ '--d-badge-dot': '8px', '--d-carousel-dot': '8px',
673
+ // Action buttons
674
+ '--d-float-btn-size': '48px', '--d-backtop-size': '40px',
675
+ // Step icon
676
+ '--d-step-icon-size': '2rem',
677
+ // ColorPicker anatomy
678
+ '--d-timepicker-column-h': '200px', '--d-datepicker-day-lh': '2',
679
+ '--d-colorpicker-swatch': '24px', '--d-colorpicker-thumb': '14px',
680
+ '--d-colorpicker-preset': '24px', '--d-colorpicker-sat-h': '150px',
681
+ '--d-colorpicker-bar-h': '12px',
682
+ // ColorPalette anatomy
683
+ '--d-colorpalette-swatch-w': '140px', '--d-colorpalette-swatch-h': '100px',
684
+ '--d-colorpalette-swatch-w-sm': '90px', '--d-colorpalette-swatch-h-sm': '64px',
685
+ '--d-colorpalette-swatch-w-lg': '180px', '--d-colorpalette-swatch-h-lg': '120px',
686
+ '--d-colorpalette-shade-h': '20px',
687
+ // Timeline geometry
688
+ '--d-timeline-dot': '10px', '--d-timeline-dot-lg': '24px',
689
+ '--d-timeline-sm-dot': '8px', '--d-timeline-sm-dot-lg': '20px',
690
+ '--d-timeline-lg-dot': '32px', '--d-timeline-lg-dot-lg': '40px',
691
+ '--d-timeline-line-w': '2px',
692
+ '--d-timeline-h-min-w': '120px',
693
+ // RangeSlider thumb
694
+ '--d-rangeslider-thumb': '16px',
695
+ // Animation slide distance
696
+ '--d-slide-distance': '8px',
697
+ };
698
+
699
+ const Z_INDEX = {
700
+ '--d-z-dropdown': '1000',
701
+ '--d-z-sticky': '1100',
702
+ '--d-z-modal': '1200',
703
+ '--d-z-popover': '1300',
704
+ '--d-z-toast': '1400',
705
+ '--d-z-tooltip': '1500',
706
+ };
707
+
708
+ // ============================================================
709
+ // Colorblind Mode (CVD — Color Vision Deficiency)
710
+ // ============================================================
711
+
712
+ /** Wong/Okabe-Ito adapted chart palettes for CVD safety */
713
+ const CVD_CHART_PALETTES = {
714
+ protanopia: ['#0072B2', '#E69F00', '#56B4E9', '#009E73', '#F0E442', '#D55E00', '#CC79A7', '#000000'],
715
+ deuteranopia: ['#0072B2', '#E69F00', '#56B4E9', '#009E73', '#F0E442', '#D55E00', '#CC79A7', '#000000'],
716
+ tritanopia: ['#D55E00', '#CC79A7', '#009E73', '#000000', '#E69F00', '#56B4E9', '#0072B2', '#F0E442'],
717
+ };
718
+
719
+ /**
720
+ * Transform seed colors for color vision deficiency safety.
721
+ * Shifts hues that are confusable for the given CVD type.
722
+ * Called BEFORE derive() processes seeds, so all derived tokens automatically adapt.
723
+ * @param {Object} seed - Complete seed object
724
+ * @param {'off'|'protanopia'|'deuteranopia'|'tritanopia'} type - CVD type
725
+ * @returns {Object} Transformed seed
726
+ */
727
+ function transformSeedsForCVD(seed, type) {
728
+ if (type === 'off') return seed;
729
+ const transformed = { ...seed };
730
+
731
+ if (type === 'protanopia' || type === 'deuteranopia') {
732
+ // Red-green CVD (98% of all CVD): shift error red → magenta, success green → teal
733
+ const errRgb = hexToRgb(seed.error || defaultSeed.error);
734
+ const [eL, eC, eH] = rgbToOklch(...errRgb);
735
+ // OKLCH red zone: H ~15-50°
736
+ if (eH >= 10 && eH <= 55) {
737
+ const [gL, gC, gH] = gamutMap(eL, eC, 345);
738
+ transformed.error = rgbToHex(...oklchToRgb(gL, gC, gH));
739
+ }
740
+
741
+ const sucRgb = hexToRgb(seed.success || defaultSeed.success);
742
+ const [sL, sC, sH] = rgbToOklch(...sucRgb);
743
+ // OKLCH green zone: H ~125-170°
744
+ if (sH >= 120 && sH <= 175) {
745
+ const [gL, gC, gH] = gamutMap(sL, sC, 190);
746
+ transformed.success = rgbToHex(...oklchToRgb(gL, gC, gH));
747
+ }
748
+
749
+ // Shift primary/accent/tertiary if in red or green danger zones
750
+ for (const key of ['primary', 'accent', 'tertiary']) {
751
+ const hex = seed[key] || defaultSeed[key];
752
+ const [pL, pC, pH] = rgbToOklch(...hexToRgb(hex));
753
+ if (pH >= 120 && pH <= 175) {
754
+ // Green → shift toward blue
755
+ const [gL, gC, gH] = gamutMap(pL, pC, pH + 40);
756
+ transformed[key] = rgbToHex(...oklchToRgb(gL, gC, gH));
757
+ } else if (pH >= 10 && pH <= 40) {
758
+ // Red → shift toward magenta
759
+ const [gL, gC, gH] = gamutMap(pL, pC, (pH + 320) % 360);
760
+ transformed[key] = rgbToHex(...oklchToRgb(gL, gC, gH));
761
+ }
762
+ }
763
+ } else if (type === 'tritanopia') {
764
+ // Blue-yellow CVD: shift info blue → teal, warning yellow → orange
765
+ const infRgb = hexToRgb(seed.info || defaultSeed.info);
766
+ const [iL, iC, iH] = rgbToOklch(...infRgb);
767
+ // OKLCH blue zone: H ~240-290°
768
+ if (iH >= 230 && iH <= 295) {
769
+ const [gL, gC, gH] = gamutMap(iL, iC, 170);
770
+ transformed.info = rgbToHex(...oklchToRgb(gL, gC, gH));
771
+ }
772
+
773
+ const wrnRgb = hexToRgb(seed.warning || defaultSeed.warning);
774
+ const [wL, wC, wH] = rgbToOklch(...wrnRgb);
775
+ // OKLCH yellow/amber zone: H ~60-125° (amber starts ~65°, yellow extends to ~120°)
776
+ if (wH >= 60 && wH <= 125) {
777
+ const [gL, gC, gH] = gamutMap(wL, wC, 50);
778
+ transformed.warning = rgbToHex(...oklchToRgb(gL, gC, gH));
779
+ }
780
+
781
+ // Shift primary if in blue danger zone
782
+ const hex = seed.primary || defaultSeed.primary;
783
+ const [pL, pC, pH] = rgbToOklch(...hexToRgb(hex));
784
+ if (pH >= 230 && pH <= 295) {
785
+ const [gL, gC, gH] = gamutMap(pL, pC, pH - 60);
786
+ transformed.primary = rgbToHex(...oklchToRgb(gL, gC, gH));
787
+ }
788
+ }
789
+
790
+ return transformed;
791
+ }
792
+
793
+ // ============================================================
794
+ // Sub-Derivation Functions
795
+ // ============================================================
796
+
797
+ /** Derive 8 tokens for a single palette color role */
798
+ function derivePaletteColor(hex, mode, bgHex, personality) {
799
+ const isDark = mode === 'dark';
800
+ const elev = (personality && personality.elevation) || 'subtle';
801
+ const t = PALETTE_TUNING[elev] || PALETTE_TUNING.subtle;
802
+ const subtleValue = alpha(hex, isDark ? t.subtleAlphaDark : t.subtleAlphaLight);
803
+ // Compute contrast-safe text color for use on subtle backgrounds
804
+ const effectiveSubtleBg = compositeOnBg(subtleValue, bgHex);
805
+ let onSubtle = hex;
806
+ if (effectiveSubtleBg.startsWith('#')) {
807
+ const ratio = contrast(hexToRgb(hex), hexToRgb(effectiveSubtleBg));
808
+ if (ratio < 4.5) {
809
+ onSubtle = adjustForContrast(hex, effectiveSubtleBg, 4.5);
810
+ }
811
+ }
812
+ return {
813
+ base: hex,
814
+ fg: pickForeground(hex),
815
+ hover: isDark ? lighten(hex, t.hoverShift) : darken(hex, t.hoverShift),
816
+ active: isDark ? lighten(hex, Math.round(t.hoverShift / 2)) : darken(hex, t.activeShift),
817
+ subtle: subtleValue,
818
+ subtleFg: isDark ? lighten(hex, 15) : darken(hex, 10),
819
+ border: alpha(hex, isDark ? t.borderAlphaDark : t.borderAlphaLight),
820
+ onSubtle,
821
+ };
822
+ }
823
+
824
+ /** Derive neutral palette (bg, fg, muted, border, etc.) */
825
+ function deriveNeutral(neutralHex, bgHex, mode) {
826
+ const isDark = mode === 'dark';
827
+ return {
828
+ bg: bgHex,
829
+ fg: isDark ? '#fafafa' : '#09090b',
830
+ muted: isDark ? lighten(neutralHex, 10) : neutralHex,
831
+ mutedFg: isDark ? lighten(neutralHex, 25) : darken(neutralHex, 15),
832
+ border: isDark ? lighten(bgHex, 12) : darken(bgHex, 22),
833
+ borderStrong: isDark ? lighten(bgHex, 25) : darken(bgHex, 40),
834
+ overlay: isDark ? 'rgba(0,0,0,0.7)' : 'rgba(0,0,0,0.5)',
835
+ };
836
+ }
837
+
838
+ /** Derive 4 surface levels (bg, fg, border, filter) */
839
+ function deriveSurfaces(neutralHex, bgHex, fgHex, mode, elevationType) {
840
+ const isDark = mode === 'dark';
841
+ const isGlass = elevationType === 'glass';
842
+
843
+ const s0 = bgHex;
844
+ // Light non-glass: canvas → cutout model (S1 = white card, S2/S3 progressively tinted)
845
+ // Light glass: same inversion with alpha (S1 = opaque white, S2 = translucent white, S3 = tinted)
846
+ const s1 = isGlass
847
+ ? (isDark ? alpha(lighten(bgHex, 8), 0.7) : alpha('#ffffff', 0.85))
848
+ : (isDark ? lighten(bgHex, 4) : '#ffffff');
849
+ const s2 = isGlass
850
+ ? (isDark ? alpha(lighten(bgHex, 10), 0.8) : alpha('#ffffff', 0.7))
851
+ : (isDark ? lighten(bgHex, 7) : mixColors(bgHex, neutralHex, 0.08));
852
+ // Dark non-glass S3: monotonic (+10L > S2's +7L). Glass S3: opacity-based depth.
853
+ const s3 = isGlass
854
+ ? (isDark ? alpha(lighten(bgHex, 6), 0.85) : alpha(bgHex, 0.92))
855
+ : (isDark ? lighten(bgHex, 10) : mixColors(bgHex, neutralHex, 0.15));
856
+
857
+ const blur = SURFACE_BLUR[elevationType] || SURFACE_BLUR.subtle;
858
+
859
+ return {
860
+ '--d-surface-0': s0, '--d-surface-0-fg': fgHex, '--d-surface-0-border': isDark ? lighten(bgHex, 12) : darken(bgHex, 18),
861
+ '--d-surface-1': s1, '--d-surface-1-fg': fgHex, '--d-surface-1-border': isDark ? lighten(bgHex, 15) : darken(bgHex, 15),
862
+ '--d-surface-2': s2, '--d-surface-2-fg': fgHex, '--d-surface-2-border': isDark ? lighten(bgHex, 18) : darken(bgHex, 20),
863
+ '--d-surface-3': s3, '--d-surface-3-fg': fgHex, '--d-surface-3-border': isDark ? lighten(bgHex, 21) : darken(bgHex, 25),
864
+ '--d-surface-1-filter': blur[0],
865
+ '--d-surface-2-filter': blur[1],
866
+ '--d-surface-3-filter': blur[2],
867
+ };
868
+ }
869
+
870
+ /** Derive chrome tokens for header/sidebar (inverted in light mode) */
871
+ function deriveChrome(primaryHex, bgHex, bgDarkHex, neutralHex, mode, elevationType) {
872
+ const isDark = mode === 'dark';
873
+ if (isDark) {
874
+ // Chrome blends with surface hierarchy
875
+ const chromeBg = lighten(bgHex, 4);
876
+ return {
877
+ '--d-chrome-bg': chromeBg,
878
+ '--d-chrome-fg': '#fafafa',
879
+ '--d-chrome-border': lighten(bgHex, 12),
880
+ '--d-chrome-muted': lighten(neutralHex, 15),
881
+ '--d-chrome-hover': lighten(chromeBg, 6),
882
+ '--d-chrome-active': lighten(chromeBg, 10),
883
+ };
884
+ }
885
+ // Light mode: chrome inverts to dark, tinted 12% toward primary for brand identity
886
+ const chromeBg = mixColors(bgDarkHex, primaryHex, 0.12);
887
+ const chromeFg = pickForeground(chromeBg);
888
+ return {
889
+ '--d-chrome-bg': chromeBg,
890
+ '--d-chrome-fg': chromeFg,
891
+ '--d-chrome-border': lighten(chromeBg, 10),
892
+ '--d-chrome-muted': lighten(chromeBg, 30),
893
+ '--d-chrome-hover': lighten(chromeBg, 6),
894
+ '--d-chrome-active': lighten(chromeBg, 12),
895
+ };
896
+ }
897
+
898
+ // ============================================================
899
+ // Main Derivation
900
+ // ============================================================
901
+
902
+ /**
903
+ * Derive full token set from seed + personality + mode.
904
+ * @param {Object} seed - Color seed values (8-12 properties)
905
+ * @param {Object} personality - Visual personality traits
906
+ * @param {'light'|'dark'} mode - Color mode
907
+ * @param {Object} [typography] - Typography overrides (e.g. retro's heavier weights)
908
+ * @param {Object} [overrides] - Per-mode token overrides
909
+ * @param {Object} [options] - Additional options
910
+ * @param {'off'|'protanopia'|'deuteranopia'|'tritanopia'} [options.colorblind='off'] - Colorblind mode
911
+ * @returns {Record<string, string>} Complete token map (~340 CSS custom properties)
912
+ */
913
+ export function derive(seed, personality, mode, typography, overrides, options) {
914
+ const opts = options || {};
915
+ const cbType = opts.colorblind || 'off';
916
+
917
+ // Merge seeds, then apply CVD transformation if active
918
+ const rawSeed = { ...defaultSeed, ...seed };
919
+ const s = cbType !== 'off' ? transformSeedsForCVD(rawSeed, cbType) : rawSeed;
920
+ const p = { ...defaultPersonality, ...personality };
921
+ const isDark = mode === 'dark';
922
+
923
+ // Auto-derive missing seed colors
924
+ if (!seed.accent) s.accent = rotateHue(s.primary, 60);
925
+ if (!seed.tertiary) s.tertiary = rotateHue(s.primary, -60);
926
+ if (!seed.info) s.info = rotateHue(s.primary, 20);
927
+
928
+ // Monochrome palette: derive all role colors from primary hue
929
+ if (p.palette === 'monochrome') {
930
+ const mono = deriveMonochromeSeed(s.primary);
931
+ for (const k of ['accent', 'tertiary', 'success', 'warning', 'error', 'info']) s[k] = mono[k];
932
+ }
933
+
934
+ // Resolve bg per mode
935
+ const bgHex = isDark ? (s.bgDark || '#0a0a0a') : (s.bg || '#ffffff');
936
+ const fgHex = isDark ? '#fafafa' : '#09090b';
937
+
938
+ // --- Palette colors (7 roles × 7 modifiers = 49 tokens) ---
939
+ const palette = {};
940
+ const roles = ['primary', 'accent', 'tertiary', 'success', 'warning', 'error', 'info'];
941
+ for (const role of roles) {
942
+ const d = derivePaletteColor(s[role], mode, bgHex, p);
943
+ palette[`--d-${role}`] = d.base;
944
+ palette[`--d-${role}-fg`] = d.fg;
945
+ palette[`--d-${role}-hover`] = d.hover;
946
+ palette[`--d-${role}-active`] = d.active;
947
+ palette[`--d-${role}-subtle`] = d.subtle;
948
+ palette[`--d-${role}-subtle-fg`] = d.subtleFg;
949
+ palette[`--d-${role}-border`] = d.border;
950
+ palette[`--d-${role}-on-subtle`] = d.onSubtle;
951
+ }
952
+
953
+ // --- Neutral (8 tokens) ---
954
+ const n = deriveNeutral(s.neutral, bgHex, mode);
955
+ const neutralTokens = {
956
+ '--d-bg': n.bg,
957
+ '--d-fg': n.fg,
958
+ '--d-muted': n.muted,
959
+ '--d-muted-fg': n.mutedFg,
960
+ '--d-border': n.border,
961
+ '--d-border-strong': n.borderStrong,
962
+ '--d-ring': 'var(--d-primary)',
963
+ '--d-overlay': n.overlay,
964
+ };
965
+
966
+ // --- Surfaces (15 tokens) ---
967
+ const surfaces = deriveSurfaces(s.neutral, bgHex, fgHex, mode, p.elevation);
968
+
969
+ // --- Chrome (6 tokens) ---
970
+ const bgDarkHex = s.bgDark || '#0a0a0a';
971
+ const chrome = deriveChrome(s.primary, bgHex, bgDarkHex, s.neutral, mode, p.elevation);
972
+
973
+ // --- Elevation (4 tokens) ---
974
+ const elev = (ELEVATION[p.elevation] || ELEVATION.subtle)[mode] || ELEVATION.subtle.light;
975
+ const elevation = {
976
+ '--d-elevation-0': elev[0],
977
+ '--d-elevation-1': elev[1],
978
+ '--d-elevation-2': elev[2],
979
+ '--d-elevation-3': elev[3],
980
+ };
981
+
982
+ // --- Interaction (12 tokens) ---
983
+ const interType = ELEVATION_TO_INTERACTION[p.elevation] || 'subtle';
984
+ const inter = INTERACTION[interType];
985
+ const hoverShadow = elev[1] === 'none' ? 'none' : elev[2];
986
+ const activeShadow = elev[0] === 'none' ? 'none' : elev[1];
987
+ const interaction = {
988
+ '--d-hover-translate': inter.hoverTranslate,
989
+ '--d-hover-shadow': hoverShadow,
990
+ '--d-hover-brightness': inter.hoverBrightness,
991
+ '--d-active-scale': inter.activeScale,
992
+ '--d-active-translate': inter.activeTranslate,
993
+ '--d-active-shadow': activeShadow,
994
+ '--d-focus-ring-width': p.borders === 'bold' ? '3px' : '2px',
995
+ '--d-focus-ring-color': 'var(--d-ring)',
996
+ '--d-focus-ring-offset': '2px',
997
+ '--d-focus-ring-style': p.elevation === 'brutalist' ? 'dashed' : 'solid',
998
+ '--d-selection-bg': 'var(--d-primary-subtle)',
999
+ '--d-selection-fg': 'var(--d-primary)',
1000
+ '--d-selection-shadow': 'var(--d-elevation-1)',
1001
+ };
1002
+
1003
+ // --- Motion (8 tokens) ---
1004
+ const m = MOTION[p.motion] || MOTION.smooth;
1005
+ const motion = {
1006
+ '--d-duration-instant': m.instant,
1007
+ '--d-duration-fast': m.fast,
1008
+ '--d-duration-normal': m.normal,
1009
+ '--d-duration-slow': m.slow,
1010
+ '--d-duration-spin': m.spin,
1011
+ '--d-easing-standard': m.standard,
1012
+ '--d-easing-decelerate': m.decelerate,
1013
+ '--d-easing-accelerate': m.accelerate,
1014
+ '--d-easing-bounce': m.bounce,
1015
+ };
1016
+
1017
+ // --- Radius (6 tokens) ---
1018
+ const rad = RADIUS[p.radius] || RADIUS.rounded;
1019
+ const radius = {
1020
+ '--d-radius-sm': rad.sm,
1021
+ '--d-radius': rad.default,
1022
+ '--d-radius-lg': rad.lg,
1023
+ '--d-radius-full': rad.full,
1024
+ '--d-radius-panel': rad.panel,
1025
+ '--d-radius-inner': rad.inner,
1026
+ };
1027
+
1028
+ // --- Checkbox (2 tokens) ---
1029
+ const checkbox = {
1030
+ '--d-checkbox-size': '1.125rem',
1031
+ '--d-checkbox-radius': rad.sm,
1032
+ };
1033
+
1034
+ // --- Border (3 tokens) ---
1035
+ const brd = BORDER[p.borders] || BORDER.thin;
1036
+ const border = {
1037
+ '--d-border-width': brd.width,
1038
+ '--d-border-width-strong': brd.widthStrong,
1039
+ '--d-border-style': brd.style,
1040
+ };
1041
+
1042
+ // --- Density (5 tokens) ---
1043
+ const den = DENSITY[p.density] || DENSITY.comfortable;
1044
+ const density = {
1045
+ '--d-density-pad-x': den.padX,
1046
+ '--d-density-pad-y': den.padY,
1047
+ '--d-density-gap': den.gap,
1048
+ '--d-density-min-h': den.minH,
1049
+ '--d-density-text': den.text,
1050
+ };
1051
+
1052
+ // --- Gradients (10 tokens) ---
1053
+ const gNone = p.gradient === 'none';
1054
+ const angle = 'var(--d-gradient-angle)';
1055
+ const gradients = {
1056
+ '--d-gradient-angle': '135deg',
1057
+ '--d-gradient-intensity': gNone ? '0' : '1',
1058
+ '--d-gradient-brand': gNone
1059
+ ? 'var(--d-primary)' : `linear-gradient(${angle},var(--d-primary),var(--d-accent))`,
1060
+ '--d-gradient-brand-alt': gNone
1061
+ ? 'var(--d-accent)' : `linear-gradient(${angle},var(--d-accent),var(--d-tertiary))`,
1062
+ '--d-gradient-brand-full': gNone
1063
+ ? 'var(--d-primary)' : `linear-gradient(${angle},var(--d-primary),var(--d-accent),var(--d-tertiary))`,
1064
+ '--d-gradient-surface': gNone
1065
+ ? 'var(--d-surface-0)' : 'linear-gradient(180deg,var(--d-surface-0),var(--d-surface-1))',
1066
+ '--d-gradient-overlay': 'linear-gradient(180deg,transparent,var(--d-overlay))',
1067
+ '--d-gradient-subtle': gNone
1068
+ ? 'transparent' : 'linear-gradient(180deg,transparent,var(--d-primary-subtle))',
1069
+ '--d-gradient-text': gNone
1070
+ ? 'var(--d-primary)' : `linear-gradient(${angle},var(--d-primary),var(--d-accent))`,
1071
+ '--d-gradient-text-alt': gNone
1072
+ ? 'var(--d-accent)' : `linear-gradient(${angle},var(--d-accent),var(--d-tertiary))`,
1073
+ };
1074
+
1075
+ // --- Charts (9 base + 24 extended + 4 chart UI tokens) ---
1076
+ const chartBase = [s.primary, s.accent, s.tertiary, s.success, s.warning, s.error, s.info, isDark ? lighten(s.neutral, 10) : s.neutral];
1077
+ const charts = {};
1078
+ for (let i = 0; i < 8; i++) {
1079
+ charts[`--d-chart-${i}`] = chartBase[i];
1080
+ // Extended palette: 3 variations per base (lightness/hue shifts)
1081
+ charts[`--d-chart-${i}-ext-1`] = isDark ? lighten(chartBase[i], 15) : darken(chartBase[i], 15);
1082
+ charts[`--d-chart-${i}-ext-2`] = rotateHue(chartBase[i], 30);
1083
+ charts[`--d-chart-${i}-ext-3`] = isDark ? lighten(rotateHue(chartBase[i], -30), 10) : darken(rotateHue(chartBase[i], -30), 10);
1084
+ }
1085
+
1086
+ // Override chart tokens with CVD-safe palettes when colorblind mode is active
1087
+ if (cbType !== 'off' && CVD_CHART_PALETTES[cbType]) {
1088
+ const cbPalette = CVD_CHART_PALETTES[cbType];
1089
+ for (let i = 0; i < 8; i++) {
1090
+ charts[`--d-chart-${i}`] = cbPalette[i];
1091
+ charts[`--d-chart-${i}-ext-1`] = isDark ? lighten(cbPalette[i], 15) : darken(cbPalette[i], 15);
1092
+ charts[`--d-chart-${i}-ext-2`] = rotateHue(cbPalette[i], 30);
1093
+ charts[`--d-chart-${i}-ext-3`] = isDark ? lighten(rotateHue(cbPalette[i], -30), 10) : darken(rotateHue(cbPalette[i], -30), 10);
1094
+ }
1095
+ }
1096
+
1097
+ charts['--d-chart-tooltip-bg'] = 'var(--d-surface-2)';
1098
+ charts['--d-chart-grid'] = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)';
1099
+ charts['--d-chart-axis'] = isDark ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.3)';
1100
+ charts['--d-chart-crosshair'] = isDark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.3)';
1101
+ charts['--d-chart-selection'] = alpha(s.primary, 0.15);
1102
+
1103
+ // --- Field tokens (15 tokens) ---
1104
+ const isGlass = p.elevation === 'glass';
1105
+ const fieldTokens = {
1106
+ '--d-field-bg': 'var(--d-bg)',
1107
+ '--d-field-bg-hover': isGlass ? alpha(isDark ? '#ffffff' : '#000000', 0.03) : 'var(--d-surface-1)',
1108
+ '--d-field-bg-disabled': alpha(isDark ? '#ffffff' : '#000000', 0.05),
1109
+ '--d-field-bg-readonly': alpha(isDark ? '#ffffff' : '#000000', 0.03),
1110
+ '--d-field-bg-error': alpha(s.error, 0.06),
1111
+ '--d-field-bg-success': alpha(s.success, 0.06),
1112
+ '--d-field-border': 'var(--d-border)',
1113
+ '--d-field-border-hover': 'var(--d-border-strong)',
1114
+ '--d-field-border-focus': 'var(--d-primary)',
1115
+ '--d-field-border-error': 'var(--d-error)',
1116
+ '--d-field-border-success': 'var(--d-success)',
1117
+ '--d-field-border-disabled': alpha(isDark ? n.border : n.border, 0.5),
1118
+ '--d-field-border-width': 'var(--d-border-width)',
1119
+ '--d-field-ring': '0 0 0 var(--d-focus-ring-width) var(--d-ring)',
1120
+ '--d-field-ring-error': `0 0 0 var(--d-focus-ring-width) ${alpha(s.error, 0.25)}`,
1121
+ '--d-field-ring-success': `0 0 0 var(--d-focus-ring-width) ${alpha(s.success, 0.25)}`,
1122
+ '--d-field-radius': 'var(--d-radius)',
1123
+ '--d-field-placeholder': 'var(--d-muted)',
1124
+ };
1125
+
1126
+ // --- Interactive state tokens (9 tokens) ---
1127
+ const interactiveState = {
1128
+ '--d-item-hover-bg': isGlass ? alpha(isDark ? '#ffffff' : '#000000', 0.06) : 'var(--d-surface-1)',
1129
+ '--d-item-active-bg': 'var(--d-primary-subtle)',
1130
+ '--d-selected-bg': 'var(--d-primary-subtle)',
1131
+ '--d-selected-fg': 'var(--d-primary)',
1132
+ '--d-selected-border': 'var(--d-primary-border)',
1133
+ '--d-disabled-opacity': '0.5',
1134
+ '--d-disabled-opacity-soft': '0.35',
1135
+ '--d-icon-muted': '0.55',
1136
+ '--d-icon-subtle': '0.35',
1137
+ };
1138
+
1139
+ // --- Overlay tokens (2 tokens) ---
1140
+ const overlayTokens = {
1141
+ '--d-overlay-light': isDark ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.2)',
1142
+ '--d-overlay-heavy': isDark ? 'rgba(0,0,0,0.85)' : 'rgba(0,0,0,0.7)',
1143
+ };
1144
+
1145
+ // --- Focus ring offset inset (1 token) ---
1146
+ const focusInset = {
1147
+ '--d-focus-ring-offset-inset': 'calc(var(--d-focus-ring-offset) * -1)',
1148
+ };
1149
+
1150
+ // --- Table tokens (4 tokens) ---
1151
+ const tableTokens = {
1152
+ '--d-table-stripe-bg': alpha(isDark ? '#ffffff' : '#000000', isDark ? 0.03 : 0.02),
1153
+ '--d-table-header-bg': 'var(--d-surface-1)',
1154
+ '--d-table-hover-bg': 'var(--d-item-hover-bg)',
1155
+ '--d-table-selected-bg': 'var(--d-selected-bg)',
1156
+ };
1157
+
1158
+ // --- Semantic motion tokens (3 tokens) ---
1159
+ const motionSemantic = {
1160
+ '--d-motion-enter': 'var(--d-duration-normal) var(--d-easing-decelerate)',
1161
+ '--d-motion-exit': 'var(--d-duration-fast) var(--d-easing-accelerate)',
1162
+ '--d-motion-state': 'var(--d-duration-fast) var(--d-easing-standard)',
1163
+ };
1164
+
1165
+ // --- Typography semantic roles (5 tokens) ---
1166
+ const typographySemantic = {
1167
+ '--d-text-helper': 'var(--d-text-xs)',
1168
+ '--d-text-error': 'var(--d-text-xs)',
1169
+ '--d-prose-width': '75ch',
1170
+ '--d-ls-tight': '-0.01em',
1171
+ '--d-ls-wide': '0.025em',
1172
+ };
1173
+
1174
+ // --- Content width / layout tokens (7 tokens) ---
1175
+ const layoutTokens = {
1176
+ '--d-content-width-prose': '75ch',
1177
+ '--d-content-width-standard': '960px',
1178
+ '--d-sidebar-width-sm': '220px',
1179
+ '--d-sidebar-width': '260px',
1180
+ '--d-sidebar-width-lg': '320px',
1181
+ '--d-drawer-width': '360px',
1182
+ '--d-drawer-bottom-max-h': '85vh',
1183
+ };
1184
+
1185
+ // --- Chart UI tokens (4 tokens) ---
1186
+ const chartUI = {
1187
+ '--d-chart-tooltip-shadow': isDark ? '0 2px 8px rgba(0,0,0,0.3)' : '0 2px 8px rgba(0,0,0,0.12)',
1188
+ '--d-chart-axis-opacity': '0.3',
1189
+ '--d-chart-grid-opacity': isDark ? '0.08' : '0.06',
1190
+ '--d-chart-legend-gap': 'var(--d-sp-3)',
1191
+ };
1192
+
1193
+ // --- Backdrop blur tokens (3 tokens) ---
1194
+ const glassBlur = {
1195
+ '--d-glass-blur-sm': 'blur(8px)',
1196
+ '--d-glass-blur': 'blur(16px)',
1197
+ '--d-glass-blur-lg': 'blur(24px)',
1198
+ };
1199
+
1200
+ // --- Scrollbar tokens (4 tokens) ---
1201
+ const scrollbar = {
1202
+ '--d-scrollbar-w': '8px',
1203
+ '--d-scrollbar-track': 'transparent',
1204
+ '--d-scrollbar-thumb': 'var(--d-border)',
1205
+ '--d-scrollbar-thumb-hover': 'var(--d-border-strong)',
1206
+ };
1207
+
1208
+ // --- Skeleton tokens (2 tokens) ---
1209
+ const skeleton = {
1210
+ '--d-skeleton-bg': 'var(--d-muted)',
1211
+ '--d-skeleton-shine': `linear-gradient(90deg,transparent,${alpha(isDark ? '#ffffff' : '#000000', 0.04)},transparent)`,
1212
+ };
1213
+
1214
+ // --- Legacy compat (remove after _base.js migration to --d-* tokens) ---
1215
+ const legacy = {
1216
+ '--d-transition': `all ${m.normal} ${m.standard}`,
1217
+ '--d-shadow': elev[1],
1218
+ '--d-radius-lg-compat': rad.lg, // old --d-radius-lg
1219
+ };
1220
+
1221
+ // --- Merge all layers ---
1222
+ const tokens = {
1223
+ ...palette,
1224
+ ...neutralTokens,
1225
+ ...surfaces,
1226
+ ...chrome,
1227
+ ...elevation,
1228
+ ...interaction,
1229
+ ...fieldTokens,
1230
+ ...interactiveState,
1231
+ ...overlayTokens,
1232
+ ...focusInset,
1233
+ ...tableTokens,
1234
+ ...motionSemantic,
1235
+ ...motion,
1236
+ ...radius,
1237
+ ...checkbox,
1238
+ ...border,
1239
+ ...density,
1240
+ ...Z_INDEX,
1241
+ ...gradients,
1242
+ ...charts,
1243
+ ...chartUI,
1244
+ ...glassBlur,
1245
+ ...scrollbar,
1246
+ ...skeleton,
1247
+ ...TYPOGRAPHY,
1248
+ ...typographySemantic,
1249
+ ...SPACING,
1250
+ ...layoutTokens,
1251
+ ...legacy,
1252
+ // Style-specific typography overrides (e.g. retro's heavier weights)
1253
+ ...(typography || {}),
1254
+ };
1255
+
1256
+ // Apply per-mode overrides last (highest priority)
1257
+ if (overrides) {
1258
+ Object.assign(tokens, overrides);
1259
+ }
1260
+
1261
+ // Validate and auto-adjust contrast for WCAG AA compliance
1262
+ validateContrast(tokens);
1263
+
1264
+ return tokens;
1265
+ }
1266
+
1267
+ /**
1268
+ * Generate density class CSS for the three density presets.
1269
+ * Injected once into the d.base layer.
1270
+ * @returns {string} CSS rules for .d-compact, .d-comfortable, .d-spacious
1271
+ */
1272
+ export function densityCSS() {
1273
+ let css = '';
1274
+ for (const [name, d] of Object.entries(DENSITY)) {
1275
+ css += `.d-${name}{--d-density-pad-x:${d.padX};--d-density-pad-y:${d.padY};--d-density-gap:${d.gap};--d-density-min-h:${d.minH};--d-density-text:${d.text};--d-compound-pad:${d.compoundPad};--d-compound-gap:${d.compoundGap}}`;
1276
+ }
1277
+ return css;
1278
+ }
1279
+
1280
+ /**
1281
+ * Get the radius token values for a given shape preset.
1282
+ * @param {string} shape - 'sharp', 'rounded', or 'pill'
1283
+ * @returns {{ '--d-radius-sm': string, '--d-radius': string, '--d-radius-lg': string, '--d-checkbox-radius': string }|null}
1284
+ */
1285
+ export function getShapeTokens(shape) {
1286
+ const r = RADIUS[shape];
1287
+ if (!r) return null;
1288
+ return {
1289
+ '--d-radius-sm': r.sm,
1290
+ '--d-radius': r.default,
1291
+ '--d-radius-lg': r.lg,
1292
+ '--d-radius-panel': r.panel,
1293
+ '--d-radius-inner': r.inner,
1294
+ '--d-checkbox-radius': r.sm,
1295
+ };
1296
+ }