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.
- package/AGENTS.md +868 -0
- package/CHANGELOG.md +255 -0
- package/CLAUDE.md +178 -0
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/cli/art.js +127 -0
- package/cli/commands/a11y.js +61 -0
- package/cli/commands/audit.js +225 -0
- package/cli/commands/build.js +38 -0
- package/cli/commands/dev.js +18 -0
- package/cli/commands/doctor.js +197 -0
- package/cli/commands/figma-sync.js +48 -0
- package/cli/commands/figma-tokens.js +55 -0
- package/cli/commands/generate.js +26 -0
- package/cli/commands/init.js +116 -0
- package/cli/commands/lint.js +209 -0
- package/cli/commands/mcp.js +530 -0
- package/cli/commands/migrate.js +175 -0
- package/cli/commands/test.js +38 -0
- package/cli/commands/validate.js +354 -0
- package/cli/index.js +113 -0
- package/package.json +95 -0
- package/reference/atoms.md +517 -0
- package/reference/behaviors.md +384 -0
- package/reference/build-tooling.md +275 -0
- package/reference/color-guidelines.md +965 -0
- package/reference/component-lifecycle.md +137 -0
- package/reference/compound-spacing.md +95 -0
- package/reference/decantation-process.md +499 -0
- package/reference/dev-server-routes.md +93 -0
- package/reference/form-system.md +253 -0
- package/reference/i18n.md +336 -0
- package/reference/icons.md +576 -0
- package/reference/llm-primer.md +953 -0
- package/reference/plugins.md +252 -0
- package/reference/registry-consumption.md +76 -0
- package/reference/router.md +217 -0
- package/reference/shells.md +116 -0
- package/reference/spatial-guidelines.md +541 -0
- package/reference/ssr.md +234 -0
- package/reference/state-data.md +215 -0
- package/reference/state-patterns.md +166 -0
- package/reference/state.md +194 -0
- package/reference/style-system.md +110 -0
- package/reference/tokens.md +460 -0
- package/src/app.js +19 -0
- package/src/chart/_animate.js +266 -0
- package/src/chart/_base.js +109 -0
- package/src/chart/_data.js +209 -0
- package/src/chart/_format.js +106 -0
- package/src/chart/_interact.js +364 -0
- package/src/chart/_palette.js +105 -0
- package/src/chart/_renderer.js +52 -0
- package/src/chart/_scene.js +262 -0
- package/src/chart/_shared.js +371 -0
- package/src/chart/index.js +637 -0
- package/src/chart/layouts/_layout-base.js +328 -0
- package/src/chart/layouts/cartesian.js +148 -0
- package/src/chart/layouts/hierarchy.js +562 -0
- package/src/chart/layouts/polar.js +101 -0
- package/src/chart/renderers/canvas.js +179 -0
- package/src/chart/renderers/svg.js +256 -0
- package/src/chart/renderers/webgpu.js +715 -0
- package/src/chart/types/_type-base.js +26 -0
- package/src/chart/types/area.js +134 -0
- package/src/chart/types/bar.js +173 -0
- package/src/chart/types/box-plot.js +125 -0
- package/src/chart/types/bubble.js +63 -0
- package/src/chart/types/candlestick.js +115 -0
- package/src/chart/types/chord.js +85 -0
- package/src/chart/types/combination.js +108 -0
- package/src/chart/types/funnel.js +68 -0
- package/src/chart/types/gauge.js +163 -0
- package/src/chart/types/heatmap.js +98 -0
- package/src/chart/types/histogram.js +71 -0
- package/src/chart/types/line.js +111 -0
- package/src/chart/types/org-chart.js +93 -0
- package/src/chart/types/pie.js +81 -0
- package/src/chart/types/radar.js +96 -0
- package/src/chart/types/radial.js +68 -0
- package/src/chart/types/range-area.js +55 -0
- package/src/chart/types/range-bar.js +61 -0
- package/src/chart/types/sankey.js +73 -0
- package/src/chart/types/scatter.js +66 -0
- package/src/chart/types/sparkline.js +81 -0
- package/src/chart/types/sunburst.js +69 -0
- package/src/chart/types/swimlane.js +88 -0
- package/src/chart/types/treemap.js +62 -0
- package/src/chart/types/waterfall.js +100 -0
- package/src/components/_base.js +1658 -0
- package/src/components/_behaviors.js +1140 -0
- package/src/components/_primitives.js +534 -0
- package/src/components/_qr-encoder.js +539 -0
- package/src/components/accordion.js +207 -0
- package/src/components/affix.js +62 -0
- package/src/components/alert-dialog.js +75 -0
- package/src/components/alert.js +47 -0
- package/src/components/aspect-ratio.js +24 -0
- package/src/components/avatar-group.js +55 -0
- package/src/components/avatar.js +38 -0
- package/src/components/back-top.js +75 -0
- package/src/components/badge.js +74 -0
- package/src/components/banner.js +68 -0
- package/src/components/breadcrumb.js +162 -0
- package/src/components/button.js +115 -0
- package/src/components/calendar.js +131 -0
- package/src/components/card.js +192 -0
- package/src/components/carousel.js +98 -0
- package/src/components/cascader.js +261 -0
- package/src/components/checkbox.js +80 -0
- package/src/components/chip.js +81 -0
- package/src/components/code-block.js +82 -0
- package/src/components/collapsible.js +50 -0
- package/src/components/color-palette.js +438 -0
- package/src/components/color-picker.js +314 -0
- package/src/components/combobox.js +181 -0
- package/src/components/command.js +174 -0
- package/src/components/comment.js +206 -0
- package/src/components/context-menu.js +76 -0
- package/src/components/data-table.js +724 -0
- package/src/components/date-picker.js +217 -0
- package/src/components/date-range-picker.js +244 -0
- package/src/components/datetime-picker.js +271 -0
- package/src/components/descriptions.js +68 -0
- package/src/components/drawer.js +179 -0
- package/src/components/dropdown.js +88 -0
- package/src/components/empty.js +41 -0
- package/src/components/float-button.js +90 -0
- package/src/components/form.js +106 -0
- package/src/components/hover-card.js +49 -0
- package/src/components/icon.js +87 -0
- package/src/components/image.js +97 -0
- package/src/components/index.js +117 -0
- package/src/components/input-group.js +75 -0
- package/src/components/input-number.js +155 -0
- package/src/components/input-otp.js +178 -0
- package/src/components/input.js +91 -0
- package/src/components/kbd.js +36 -0
- package/src/components/label.js +25 -0
- package/src/components/list.js +118 -0
- package/src/components/masked-input.js +236 -0
- package/src/components/mentions.js +165 -0
- package/src/components/menu.js +259 -0
- package/src/components/message.js +80 -0
- package/src/components/modal.js +147 -0
- package/src/components/navigation-menu.js +166 -0
- package/src/components/notification.js +84 -0
- package/src/components/pagination.js +104 -0
- package/src/components/placeholder.js +132 -0
- package/src/components/popconfirm.js +70 -0
- package/src/components/popover.js +58 -0
- package/src/components/progress.js +61 -0
- package/src/components/qrcode.js +251 -0
- package/src/components/radiogroup.js +120 -0
- package/src/components/range-slider.js +176 -0
- package/src/components/rate.js +186 -0
- package/src/components/resizable.js +83 -0
- package/src/components/result.js +57 -0
- package/src/components/scroll-area.js +43 -0
- package/src/components/segmented.js +97 -0
- package/src/components/select.js +165 -0
- package/src/components/separator.js +31 -0
- package/src/components/shell.js +407 -0
- package/src/components/skeleton.js +39 -0
- package/src/components/slider.js +141 -0
- package/src/components/sortable-list.js +176 -0
- package/src/components/space.js +42 -0
- package/src/components/spinner.js +112 -0
- package/src/components/splitter.js +147 -0
- package/src/components/statistic.js +136 -0
- package/src/components/steps.js +99 -0
- package/src/components/switch.js +95 -0
- package/src/components/table.js +44 -0
- package/src/components/tabs.js +216 -0
- package/src/components/tag.js +115 -0
- package/src/components/textarea.js +82 -0
- package/src/components/time-picker.js +153 -0
- package/src/components/time-range-picker.js +170 -0
- package/src/components/timeline.js +226 -0
- package/src/components/toast.js +71 -0
- package/src/components/toggle.js +213 -0
- package/src/components/tooltip.js +57 -0
- package/src/components/tour.js +159 -0
- package/src/components/transfer.js +163 -0
- package/src/components/tree-select.js +274 -0
- package/src/components/tree.js +141 -0
- package/src/components/typography.js +136 -0
- package/src/components/upload.js +118 -0
- package/src/components/visually-hidden.js +20 -0
- package/src/components/watermark.js +124 -0
- package/src/core/index.js +539 -0
- package/src/core/lifecycle.js +69 -0
- package/src/css/atoms.js +651 -0
- package/src/css/components.js +940 -0
- package/src/css/derive.js +1296 -0
- package/src/css/index.js +265 -0
- package/src/css/runtime.js +268 -0
- package/src/css/styles/addons/bioluminescent.js +93 -0
- package/src/css/styles/addons/clay.js +70 -0
- package/src/css/styles/addons/clean.js +57 -0
- package/src/css/styles/addons/command-center.js +143 -0
- package/src/css/styles/addons/dopamine.js +83 -0
- package/src/css/styles/addons/editorial.js +80 -0
- package/src/css/styles/addons/glassmorphism.js +99 -0
- package/src/css/styles/addons/liquid-glass.js +105 -0
- package/src/css/styles/addons/prismatic.js +100 -0
- package/src/css/styles/addons/retro.js +63 -0
- package/src/css/styles/auradecantism.js +96 -0
- package/src/css/theme-registry.js +444 -0
- package/src/data/entity.js +281 -0
- package/src/data/index.js +13 -0
- package/src/data/persist.js +225 -0
- package/src/data/query.js +839 -0
- package/src/data/realtime.js +299 -0
- package/src/data/url.js +177 -0
- package/src/data/worker.js +134 -0
- package/src/explorer/archetypes.js +243 -0
- package/src/explorer/atoms.js +228 -0
- package/src/explorer/charts.js +497 -0
- package/src/explorer/components.js +129 -0
- package/src/explorer/foundations.js +949 -0
- package/src/explorer/icons.js +178 -0
- package/src/explorer/patterns.js +247 -0
- package/src/explorer/recipes.js +194 -0
- package/src/explorer/shared/pattern-examples.js +1337 -0
- package/src/explorer/shared/showcase-renderer.js +958 -0
- package/src/explorer/shared/spec-table.js +41 -0
- package/src/explorer/shared/usage-links.js +87 -0
- package/src/explorer/shell-config.js +10 -0
- package/src/explorer/shells.js +551 -0
- package/src/explorer/styles.js +161 -0
- package/src/explorer/tokens.js +262 -0
- package/src/explorer/tools.js +525 -0
- package/src/form/index.js +804 -0
- package/src/i18n/index.js +251 -0
- package/src/icons/essential.js +479 -0
- package/src/icons/index.js +53 -0
- package/src/plugins/index.js +282 -0
- package/src/registry/archetypes/content-site.json +71 -0
- package/src/registry/archetypes/docs-explorer.json +23 -0
- package/src/registry/archetypes/ecommerce.json +104 -0
- package/src/registry/archetypes/financial-dashboard.json +77 -0
- package/src/registry/archetypes/index.json +41 -0
- package/src/registry/archetypes/portfolio.json +82 -0
- package/src/registry/archetypes/recipe-community.json +159 -0
- package/src/registry/archetypes/saas-dashboard.json +86 -0
- package/src/registry/architect/cross-cutting.json +45 -0
- package/src/registry/architect/domains/ecommerce.json +294 -0
- package/src/registry/architect/domains/financial-services.json +302 -0
- package/src/registry/architect/index.json +26 -0
- package/src/registry/architect/traits.json +379 -0
- package/src/registry/atoms.json +16 -0
- package/src/registry/chart-showcase.json +160 -0
- package/src/registry/chart.json +136 -0
- package/src/registry/components.json +8616 -0
- package/src/registry/core.json +216 -0
- package/src/registry/css.json +319 -0
- package/src/registry/data.json +135 -0
- package/src/registry/foundations.json +11 -0
- package/src/registry/icons.json +463 -0
- package/src/registry/index.json +101 -0
- package/src/registry/patterns/activity-feed.json +37 -0
- package/src/registry/patterns/article-content.json +27 -0
- package/src/registry/patterns/auth-form.json +37 -0
- package/src/registry/patterns/author-card.json +20 -0
- package/src/registry/patterns/card-grid.json +127 -0
- package/src/registry/patterns/category-nav.json +26 -0
- package/src/registry/patterns/chart-grid.json +36 -0
- package/src/registry/patterns/chat-interface.json +37 -0
- package/src/registry/patterns/checklist-card.json +55 -0
- package/src/registry/patterns/comparison-panel.json +27 -0
- package/src/registry/patterns/component-showcase.json +24 -0
- package/src/registry/patterns/contact-form.json +31 -0
- package/src/registry/patterns/cta-section.json +20 -0
- package/src/registry/patterns/data-table.json +37 -0
- package/src/registry/patterns/detail-header.json +83 -0
- package/src/registry/patterns/detail-panel.json +27 -0
- package/src/registry/patterns/explorer-shell.json +22 -0
- package/src/registry/patterns/filter-bar.json +33 -0
- package/src/registry/patterns/filter-sidebar.json +27 -0
- package/src/registry/patterns/form-sections.json +110 -0
- package/src/registry/patterns/goal-tracker.json +27 -0
- package/src/registry/patterns/hero.json +107 -0
- package/src/registry/patterns/index.json +47 -0
- package/src/registry/patterns/kpi-grid.json +36 -0
- package/src/registry/patterns/media-gallery.json +20 -0
- package/src/registry/patterns/order-history.json +20 -0
- package/src/registry/patterns/pagination.json +19 -0
- package/src/registry/patterns/photo-to-recipe.json +36 -0
- package/src/registry/patterns/pipeline-tracker.json +28 -0
- package/src/registry/patterns/post-list.json +27 -0
- package/src/registry/patterns/pricing-table.json +32 -0
- package/src/registry/patterns/scorecard.json +28 -0
- package/src/registry/patterns/search-bar.json +20 -0
- package/src/registry/patterns/specimen-grid.json +19 -0
- package/src/registry/patterns/stat-card.json +55 -0
- package/src/registry/patterns/stats-bar.json +55 -0
- package/src/registry/patterns/steps-card.json +55 -0
- package/src/registry/patterns/table-of-contents.json +19 -0
- package/src/registry/patterns/testimonials.json +21 -0
- package/src/registry/patterns/timeline.json +27 -0
- package/src/registry/patterns/token-inspector.json +21 -0
- package/src/registry/patterns/wizard.json +27 -0
- package/src/registry/recipe-auradecantism.json +69 -0
- package/src/registry/recipe-clean.json +65 -0
- package/src/registry/recipe-command-center.json +78 -0
- package/src/registry/router.json +73 -0
- package/src/registry/schema/README.md +197 -0
- package/src/registry/skeletons.json +259 -0
- package/src/registry/state.json +137 -0
- package/src/registry/tokens.json +40 -0
- package/src/router/hash.js +17 -0
- package/src/router/history.js +18 -0
- package/src/router/index.js +598 -0
- package/src/ssr/index.js +922 -0
- package/src/state/arrays.js +181 -0
- package/src/state/devtools.js +647 -0
- package/src/state/index.js +498 -0
- package/src/state/middleware.js +288 -0
- package/src/state/scheduler.js +206 -0
- package/src/state/store.js +300 -0
- package/src/tags/index.js +19 -0
- package/src/tannins/auth.js +396 -0
- package/src/test/dom.js +352 -0
- package/src/test/index.js +62 -0
- package/src/test/state.js +306 -0
- package/tools/a11y-audit.js +487 -0
- package/tools/analyzer.js +315 -0
- package/tools/audit.js +706 -0
- package/tools/builder.js +1422 -0
- package/tools/css-extract.js +188 -0
- package/tools/dev-server.js +316 -0
- package/tools/dts-gen.js +1260 -0
- package/tools/figma-components.js +329 -0
- package/tools/figma-patterns.js +516 -0
- package/tools/figma-plugin/code.js +453 -0
- package/tools/figma-plugin/manifest.json +14 -0
- package/tools/figma-plugin/ui.html +268 -0
- package/tools/figma-render.js +293 -0
- package/tools/figma-tokens.js +712 -0
- package/tools/figma-upload.js +318 -0
- package/tools/generate.js +738 -0
- package/tools/icons.js +133 -0
- package/tools/init-templates.js +265 -0
- package/tools/install-hooks.sh +5 -0
- package/tools/migrations/0.5.0.js +53 -0
- package/tools/migrations/0.6.0.js +95 -0
- package/tools/minify.js +170 -0
- package/tools/pre-commit +4 -0
- package/tools/registry.js +662 -0
- package/tools/reset-playground.js +61 -0
- package/tools/starter-templates/content-site/app.js +49 -0
- package/tools/starter-templates/content-site/essence.js +19 -0
- package/tools/starter-templates/content-site/pages.js +31 -0
- package/tools/starter-templates/ecommerce/app.js +50 -0
- package/tools/starter-templates/ecommerce/essence.js +19 -0
- package/tools/starter-templates/ecommerce/pages.js +31 -0
- package/tools/starter-templates/landing-page/app.js +38 -0
- package/tools/starter-templates/landing-page/essence.js +18 -0
- package/tools/starter-templates/landing-page/pages.js +21 -0
- package/tools/starter-templates/portfolio/app.js +45 -0
- package/tools/starter-templates/portfolio/essence.js +19 -0
- package/tools/starter-templates/portfolio/pages.js +33 -0
- package/tools/starter-templates/saas-dashboard/app.js +70 -0
- package/tools/starter-templates/saas-dashboard/essence.js +19 -0
- package/tools/starter-templates/saas-dashboard/pages.js +31 -0
- package/tools/verify-pack.js +203 -0
- package/types/chart.d.ts +77 -0
- package/types/components.d.ts +587 -0
- package/types/core.d.ts +89 -0
- package/types/css.d.ts +149 -0
- package/types/data.d.ts +238 -0
- package/types/form.d.ts +164 -0
- package/types/i18n.d.ts +51 -0
- package/types/icons.d.ts +27 -0
- package/types/index.d.ts +13 -0
- package/types/router.d.ts +116 -0
- package/types/ssr.d.ts +102 -0
- package/types/state.d.ts +83 -0
- package/types/tags.d.ts +62 -0
- package/types/tannins.d.ts +63 -0
- 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
|
+
}
|