@cueplusplus/ui 0.1.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/dist/charts/_token.js +39 -0
- package/dist/charts/container.d.ts +58 -0
- package/dist/charts/container.js +77 -0
- package/dist/charts/index.d.ts +5 -0
- package/dist/charts/index.js +5 -0
- package/dist/charts/ramps.d.ts +140 -0
- package/dist/charts/ramps.js +615 -0
- package/dist/charts/theme.d.ts +112 -0
- package/dist/charts/theme.js +165 -0
- package/dist/charts/tones.d.ts +111 -0
- package/dist/charts/tones.js +127 -0
- package/dist/chat/_glyphs.js +55 -0
- package/dist/chat/agent-color.d.ts +84 -0
- package/dist/chat/agent-color.js +101 -0
- package/dist/chat/agent-pile.d.ts +55 -0
- package/dist/chat/agent-pile.js +121 -0
- package/dist/chat/ask-box.d.ts +88 -0
- package/dist/chat/ask-box.js +341 -0
- package/dist/chat/composer.d.ts +66 -0
- package/dist/chat/composer.js +144 -0
- package/dist/chat/delegation-card.d.ts +60 -0
- package/dist/chat/delegation-card.js +129 -0
- package/dist/chat/index.d.ts +8 -0
- package/dist/chat/index.js +8 -0
- package/dist/chat/message-list.d.ts +39 -0
- package/dist/chat/message-list.js +105 -0
- package/dist/chat/message.d.ts +47 -0
- package/dist/chat/message.js +108 -0
- package/dist/chrome/_glyphs.js +101 -0
- package/dist/chrome/_tabs-scroll.d.ts +62 -0
- package/dist/chrome/_tabs-scroll.js +68 -0
- package/dist/chrome/_tree.d.ts +75 -0
- package/dist/chrome/_tree.js +49 -0
- package/dist/chrome/empty-state.d.ts +37 -0
- package/dist/chrome/empty-state.js +57 -0
- package/dist/chrome/eyebrow.d.ts +29 -0
- package/dist/chrome/eyebrow.js +36 -0
- package/dist/chrome/index.d.ts +17 -0
- package/dist/chrome/index.js +17 -0
- package/dist/chrome/navigation-menu.d.ts +80 -0
- package/dist/chrome/navigation-menu.js +121 -0
- package/dist/chrome/page-shell.d.ts +38 -0
- package/dist/chrome/page-shell.js +66 -0
- package/dist/chrome/panel.d.ts +55 -0
- package/dist/chrome/panel.js +67 -0
- package/dist/chrome/row.d.ts +82 -0
- package/dist/chrome/row.js +106 -0
- package/dist/chrome/scroll-area.d.ts +39 -0
- package/dist/chrome/scroll-area.js +70 -0
- package/dist/chrome/section-header.d.ts +42 -0
- package/dist/chrome/section-header.js +79 -0
- package/dist/chrome/segmented-control.d.ts +58 -0
- package/dist/chrome/segmented-control.js +76 -0
- package/dist/chrome/status-bar.d.ts +46 -0
- package/dist/chrome/status-bar.js +56 -0
- package/dist/chrome/tabs.d.ts +80 -0
- package/dist/chrome/tabs.js +176 -0
- package/dist/chrome/title-bar.d.ts +37 -0
- package/dist/chrome/title-bar.js +45 -0
- package/dist/chrome/toolbar.d.ts +46 -0
- package/dist/chrome/toolbar.js +62 -0
- package/dist/chrome/tree.d.ts +98 -0
- package/dist/chrome/tree.js +248 -0
- package/dist/color/_checker.js +14 -0
- package/dist/color/_convert.d.ts +52 -0
- package/dist/color/_convert.js +66 -0
- package/dist/color/_glyphs.js +28 -0
- package/dist/color/_thumb.js +15 -0
- package/dist/color/_value.js +28 -0
- package/dist/color/color-area.d.ts +27 -0
- package/dist/color/color-area.js +40 -0
- package/dist/color/color-field.d.ts +32 -0
- package/dist/color/color-field.js +43 -0
- package/dist/color/color-picker.d.ts +71 -0
- package/dist/color/color-picker.js +178 -0
- package/dist/color/color-slider.d.ts +31 -0
- package/dist/color/color-slider.js +51 -0
- package/dist/color/color-swatch.d.ts +32 -0
- package/dist/color/color-swatch.js +44 -0
- package/dist/color/index.d.ts +7 -0
- package/dist/color/index.js +7 -0
- package/dist/configurator/_export.d.ts +78 -0
- package/dist/configurator/_export.js +261 -0
- package/dist/configurator/_glyphs.js +92 -0
- package/dist/configurator/_overrides.d.ts +247 -0
- package/dist/configurator/_overrides.js +514 -0
- package/dist/configurator/configurator.d.ts +71 -0
- package/dist/configurator/configurator.js +279 -0
- package/dist/configurator/export-dialog.d.ts +30 -0
- package/dist/configurator/export-dialog.js +121 -0
- package/dist/configurator/index.d.ts +8 -0
- package/dist/configurator/index.js +8 -0
- package/dist/configurator/panel-sections.d.ts +86 -0
- package/dist/configurator/panel-sections.js +316 -0
- package/dist/configurator/token-editor.d.ts +74 -0
- package/dist/configurator/token-editor.js +149 -0
- package/dist/configurator/use-zoom.d.ts +87 -0
- package/dist/configurator/use-zoom.js +113 -0
- package/dist/date/_glyphs.js +34 -0
- package/dist/date/_segments.js +55 -0
- package/dist/date/_trigger.js +31 -0
- package/dist/date/calendar.d.ts +25 -0
- package/dist/date/calendar.js +125 -0
- package/dist/date/date-field.d.ts +34 -0
- package/dist/date/date-field.js +41 -0
- package/dist/date/date-picker.d.ts +69 -0
- package/dist/date/date-picker.js +89 -0
- package/dist/date/date-range-picker.d.ts +65 -0
- package/dist/date/date-range-picker.js +92 -0
- package/dist/date/index.d.ts +6 -0
- package/dist/date/index.js +6 -0
- package/dist/date/time-field.d.ts +29 -0
- package/dist/date/time-field.js +36 -0
- package/dist/dmx/_ramp.d.ts +77 -0
- package/dist/dmx/_ramp.js +169 -0
- package/dist/dmx/_readout.js +53 -0
- package/dist/dmx/_universe.d.ts +105 -0
- package/dist/dmx/_universe.js +112 -0
- package/dist/dmx/channel-matrix.d.ts +81 -0
- package/dist/dmx/channel-matrix.js +285 -0
- package/dist/dmx/dmx-bar.d.ts +50 -0
- package/dist/dmx/dmx-bar.js +107 -0
- package/dist/dmx/dmx-strip.d.ts +41 -0
- package/dist/dmx/dmx-strip.js +196 -0
- package/dist/dmx/index.d.ts +7 -0
- package/dist/dmx/index.js +7 -0
- package/dist/dmx/universe-grid.d.ts +61 -0
- package/dist/dmx/universe-grid.js +182 -0
- package/dist/flow/_frame.d.ts +52 -0
- package/dist/flow/_frame.js +38 -0
- package/dist/flow/controls.d.ts +41 -0
- package/dist/flow/controls.js +53 -0
- package/dist/flow/flow.css +121 -0
- package/dist/flow/handles.d.ts +40 -0
- package/dist/flow/handles.js +61 -0
- package/dist/flow/index.d.ts +7 -0
- package/dist/flow/index.js +7 -0
- package/dist/flow/node-card.d.ts +71 -0
- package/dist/flow/node-card.js +102 -0
- package/dist/flow/signal-edge.d.ts +60 -0
- package/dist/flow/signal-edge.js +108 -0
- package/dist/flow/use-flow-theme.d.ts +69 -0
- package/dist/flow/use-flow-theme.js +109 -0
- package/dist/forms/_chassis.d.ts +64 -0
- package/dist/forms/_chassis.js +59 -0
- package/dist/forms/_glyphs.js +143 -0
- package/dist/forms/_listbox.js +55 -0
- package/dist/forms/autocomplete.d.ts +61 -0
- package/dist/forms/autocomplete.js +88 -0
- package/dist/forms/button-group.d.ts +47 -0
- package/dist/forms/button-group.js +52 -0
- package/dist/forms/checkbox.d.ts +51 -0
- package/dist/forms/checkbox.js +98 -0
- package/dist/forms/combobox.d.ts +73 -0
- package/dist/forms/combobox.js +108 -0
- package/dist/forms/env-var-input.d.ts +74 -0
- package/dist/forms/env-var-input.js +116 -0
- package/dist/forms/field.d.ts +89 -0
- package/dist/forms/field.js +102 -0
- package/dist/forms/file-upload.d.ts +113 -0
- package/dist/forms/file-upload.js +232 -0
- package/dist/forms/index.d.ts +26 -0
- package/dist/forms/index.js +26 -0
- package/dist/forms/input-group.d.ts +31 -0
- package/dist/forms/input-group.js +54 -0
- package/dist/forms/input.d.ts +33 -0
- package/dist/forms/input.js +36 -0
- package/dist/forms/multi-select.d.ts +62 -0
- package/dist/forms/multi-select.js +107 -0
- package/dist/forms/number-field.d.ts +38 -0
- package/dist/forms/number-field.js +64 -0
- package/dist/forms/otp-field.d.ts +54 -0
- package/dist/forms/otp-field.js +59 -0
- package/dist/forms/password-input.d.ts +46 -0
- package/dist/forms/password-input.js +62 -0
- package/dist/forms/radio-group.d.ts +50 -0
- package/dist/forms/radio-group.js +64 -0
- package/dist/forms/rating.d.ts +69 -0
- package/dist/forms/rating.js +103 -0
- package/dist/forms/scrub-input.d.ts +81 -0
- package/dist/forms/scrub-input.js +249 -0
- package/dist/forms/search-input.d.ts +54 -0
- package/dist/forms/search-input.js +112 -0
- package/dist/forms/select.d.ts +75 -0
- package/dist/forms/select.js +91 -0
- package/dist/forms/slider.d.ts +44 -0
- package/dist/forms/slider.js +65 -0
- package/dist/forms/switch.d.ts +36 -0
- package/dist/forms/switch.js +71 -0
- package/dist/forms/tags-input.d.ts +69 -0
- package/dist/forms/tags-input.js +148 -0
- package/dist/forms/textarea.d.ts +27 -0
- package/dist/forms/textarea.js +37 -0
- package/dist/forms/toggle-group.d.ts +41 -0
- package/dist/forms/toggle-group.js +56 -0
- package/dist/forms/toggle.d.ts +41 -0
- package/dist/forms/toggle.js +100 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +104 -0
- package/dist/instruments/_ansi.js +116 -0
- package/dist/instruments/_glyphs.js +90 -0
- package/dist/instruments/_motion.js +56 -0
- package/dist/instruments/_rem.js +21 -0
- package/dist/instruments/app-window-frame.d.ts +48 -0
- package/dist/instruments/app-window-frame.js +76 -0
- package/dist/instruments/copy-button.d.ts +41 -0
- package/dist/instruments/copy-button.js +112 -0
- package/dist/instruments/data-table.d.ts +168 -0
- package/dist/instruments/data-table.js +212 -0
- package/dist/instruments/group-bar.d.ts +63 -0
- package/dist/instruments/group-bar.js +122 -0
- package/dist/instruments/index.d.ts +13 -0
- package/dist/instruments/index.js +13 -0
- package/dist/instruments/log-viewer.d.ts +74 -0
- package/dist/instruments/log-viewer.js +215 -0
- package/dist/instruments/meter.d.ts +77 -0
- package/dist/instruments/meter.js +96 -0
- package/dist/instruments/progress.d.ts +45 -0
- package/dist/instruments/progress.js +61 -0
- package/dist/instruments/seam-grid.d.ts +66 -0
- package/dist/instruments/seam-grid.js +78 -0
- package/dist/instruments/sparkline.d.ts +39 -0
- package/dist/instruments/sparkline.js +85 -0
- package/dist/instruments/stat.d.ts +35 -0
- package/dist/instruments/stat.js +59 -0
- package/dist/instruments/table.d.ts +51 -0
- package/dist/instruments/table.js +92 -0
- package/dist/instruments/terminal-frame.d.ts +89 -0
- package/dist/instruments/terminal-frame.js +256 -0
- package/dist/instruments/two-step-button.d.ts +47 -0
- package/dist/instruments/two-step-button.js +147 -0
- package/dist/instruments/usage-chart.d.ts +55 -0
- package/dist/instruments/usage-chart.js +235 -0
- package/dist/layout/_collapse.js +46 -0
- package/dist/layout/_glyphs.js +45 -0
- package/dist/layout/accordion.d.ts +52 -0
- package/dist/layout/accordion.js +81 -0
- package/dist/layout/breadcrumb.d.ts +35 -0
- package/dist/layout/breadcrumb.js +67 -0
- package/dist/layout/card.d.ts +51 -0
- package/dist/layout/card.js +76 -0
- package/dist/layout/carousel.d.ts +96 -0
- package/dist/layout/carousel.js +211 -0
- package/dist/layout/collapsible.d.ts +74 -0
- package/dist/layout/collapsible.js +126 -0
- package/dist/layout/container.d.ts +27 -0
- package/dist/layout/container.js +40 -0
- package/dist/layout/description-list.d.ts +24 -0
- package/dist/layout/description-list.js +37 -0
- package/dist/layout/grid.d.ts +40 -0
- package/dist/layout/grid.js +48 -0
- package/dist/layout/index.d.ts +15 -0
- package/dist/layout/index.js +15 -0
- package/dist/layout/item.d.ts +65 -0
- package/dist/layout/item.js +100 -0
- package/dist/layout/link.d.ts +48 -0
- package/dist/layout/link.js +63 -0
- package/dist/layout/pagination.d.ts +54 -0
- package/dist/layout/pagination.js +121 -0
- package/dist/layout/resizable.d.ts +38 -0
- package/dist/layout/resizable.js +68 -0
- package/dist/layout/sidebar.d.ts +67 -0
- package/dist/layout/sidebar.js +129 -0
- package/dist/layout/stack.d.ts +46 -0
- package/dist/layout/stack.js +62 -0
- package/dist/layout/stepper.d.ts +49 -0
- package/dist/layout/stepper.js +81 -0
- package/dist/layout/timeline.d.ts +43 -0
- package/dist/layout/timeline.js +63 -0
- package/dist/lib/aria-class.js +24 -0
- package/dist/lib/cn.js +18 -0
- package/dist/lib/compose.js +38 -0
- package/dist/lib/cva.d.ts +18 -0
- package/dist/lib/cva.js +65 -0
- package/dist/midi/_notes.d.ts +101 -0
- package/dist/midi/_notes.js +183 -0
- package/dist/midi/_ruler.d.ts +100 -0
- package/dist/midi/_ruler.js +105 -0
- package/dist/midi/_time.d.ts +107 -0
- package/dist/midi/_time.js +130 -0
- package/dist/midi/index.d.ts +9 -0
- package/dist/midi/index.js +9 -0
- package/dist/midi/musical-time-input.d.ts +48 -0
- package/dist/midi/musical-time-input.js +154 -0
- package/dist/midi/piano-keyboard.d.ts +50 -0
- package/dist/midi/piano-keyboard.js +193 -0
- package/dist/midi/spectrum-visualizer.d.ts +65 -0
- package/dist/midi/spectrum-visualizer.js +213 -0
- package/dist/midi/threshold-rail.d.ts +72 -0
- package/dist/midi/threshold-rail.js +214 -0
- package/dist/midi/timeline-ruler.d.ts +83 -0
- package/dist/midi/timeline-ruler.js +152 -0
- package/dist/overlays/_debounce.js +34 -0
- package/dist/overlays/_glyphs.js +59 -0
- package/dist/overlays/_surface.d.ts +70 -0
- package/dist/overlays/_surface.js +70 -0
- package/dist/overlays/_wheel.js +42 -0
- package/dist/overlays/alert-dialog.d.ts +67 -0
- package/dist/overlays/alert-dialog.js +117 -0
- package/dist/overlays/command-palette.d.ts +93 -0
- package/dist/overlays/command-palette.js +193 -0
- package/dist/overlays/context-menu.d.ts +64 -0
- package/dist/overlays/context-menu.js +78 -0
- package/dist/overlays/dialog.d.ts +72 -0
- package/dist/overlays/dialog.js +124 -0
- package/dist/overlays/drawer.d.ts +75 -0
- package/dist/overlays/drawer.js +151 -0
- package/dist/overlays/dropdown-menu.d.ts +79 -0
- package/dist/overlays/dropdown-menu.js +156 -0
- package/dist/overlays/hover-card.d.ts +58 -0
- package/dist/overlays/hover-card.js +91 -0
- package/dist/overlays/index.d.ts +15 -0
- package/dist/overlays/index.js +15 -0
- package/dist/overlays/info-tip.d.ts +41 -0
- package/dist/overlays/info-tip.js +45 -0
- package/dist/overlays/menubar.d.ts +72 -0
- package/dist/overlays/menubar.js +100 -0
- package/dist/overlays/popover.d.ts +66 -0
- package/dist/overlays/popover.js +114 -0
- package/dist/overlays/sheet.d.ts +78 -0
- package/dist/overlays/sheet.js +131 -0
- package/dist/overlays/toast.d.ts +106 -0
- package/dist/overlays/toast.js +146 -0
- package/dist/overlays/tooltip.d.ts +57 -0
- package/dist/overlays/tooltip.js +95 -0
- package/dist/primitives/avatar.d.ts +51 -0
- package/dist/primitives/avatar.js +93 -0
- package/dist/primitives/button.d.ts +46 -0
- package/dist/primitives/button.js +94 -0
- package/dist/primitives/chip.d.ts +47 -0
- package/dist/primitives/chip.js +152 -0
- package/dist/primitives/icon-button.d.ts +41 -0
- package/dist/primitives/icon-button.js +59 -0
- package/dist/primitives/index.d.ts +10 -0
- package/dist/primitives/index.js +10 -0
- package/dist/primitives/kbd.d.ts +17 -0
- package/dist/primitives/kbd.js +24 -0
- package/dist/primitives/separator.d.ts +25 -0
- package/dist/primitives/separator.js +30 -0
- package/dist/primitives/skeleton.d.ts +20 -0
- package/dist/primitives/skeleton.js +27 -0
- package/dist/primitives/spinner.d.ts +26 -0
- package/dist/primitives/spinner.js +31 -0
- package/dist/primitives/status-dot.d.ts +44 -0
- package/dist/primitives/status-dot.js +50 -0
- package/dist/styles.css +19 -0
- package/dist/system/density.d.ts +33 -0
- package/dist/system/density.js +49 -0
- package/dist/system/index.d.ts +8 -0
- package/dist/system/index.js +7 -0
- package/dist/system/portal.d.ts +51 -0
- package/dist/system/portal.js +122 -0
- package/dist/system/prepaint.d.ts +38 -0
- package/dist/system/prepaint.js +37 -0
- package/dist/system/theme-provider.d.ts +72 -0
- package/dist/system/theme-provider.js +252 -0
- package/dist/system/use-density.d.ts +33 -0
- package/dist/system/use-density.js +84 -0
- package/dist/system/use-isomorphic-layout-effect.js +15 -0
- package/dist/system/use-theme.d.ts +19 -0
- package/dist/system/use-theme.js +25 -0
- package/dist/theming/_presets.d.ts +11 -0
- package/dist/theming/_presets.js +487 -0
- package/dist/theming/contrast.d.ts +110 -0
- package/dist/theming/contrast.js +167 -0
- package/dist/theming/create-theme.d.ts +165 -0
- package/dist/theming/create-theme.js +442 -0
- package/dist/theming/index.d.ts +5 -0
- package/dist/theming/index.js +5 -0
- package/dist/theming/serialize.d.ts +145 -0
- package/dist/theming/serialize.js +165 -0
- package/package.json +216 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { formatColorString } from "../color/_convert.js";
|
|
2
|
+
import { BASE_FONT_STACKS, DEFAULT_DENSITY_LEVEL, DENSITY_RADIUS_SCALE, THEME_COLOR_TOKENS, THEME_PRESETS } from "./_presets.js";
|
|
3
|
+
import { WCAG_AA_TEXT, contrastRatio, contrastReport } from "./contrast.js";
|
|
4
|
+
import { assertCssValue, assertThemeName, derivedTokens, serializeThemeCss, themeSelector, tokenProperty } from "./serialize.js";
|
|
5
|
+
import { clampChroma, converter, formatHex, inGamut, parse } from "culori";
|
|
6
|
+
//#region src/theming/create-theme.ts
|
|
7
|
+
/**
|
|
8
|
+
* `createTheme()` — the typed generator behind the spec's second theming tier.
|
|
9
|
+
*
|
|
10
|
+
* Three things it is not, because each of them is a trap this deliberately
|
|
11
|
+
* avoids:
|
|
12
|
+
*
|
|
13
|
+
* - **Not a runtime engine.** It is a pure function of its arguments, called at
|
|
14
|
+
* build time (or once, in the configurator's export dialog), and what comes
|
|
15
|
+
* out is a plain `[data-theme]` stylesheet — tier 1, the guaranteed contract.
|
|
16
|
+
* Nothing about a shipped app depends on this module existing.
|
|
17
|
+
* - **Not a palette generator.** Handed a preset and nothing else it returns the
|
|
18
|
+
* preset, byte for byte. Derivation is what happens to the tokens hanging off
|
|
19
|
+
* an anchor you actually moved, and only those: swap the accent and the hover,
|
|
20
|
+
* the ink on it, the focus ring and the selection wash follow; the ground, the
|
|
21
|
+
* ink and the five status tones do not. That rule is the whole reason
|
|
22
|
+
* "start from `terminal`, make it orange" is a two-line call rather than a
|
|
23
|
+
* fork of a palette.
|
|
24
|
+
* - **Not a colour opinion.** Every derivation happens in oklch, where a
|
|
25
|
+
* lightness step is a step the eye agrees with, and every result is measured
|
|
26
|
+
* against WCAG before you are told it is fine. A failing pair comes back in
|
|
27
|
+
* the report rather than being silently repaired — with one exception, noted
|
|
28
|
+
* below, where the value was this module's invention in the first place.
|
|
29
|
+
*/
|
|
30
|
+
/** The preset a theme starts from when the caller names none. */
|
|
31
|
+
const DEFAULT_THEME_BASE = "cue";
|
|
32
|
+
/**
|
|
33
|
+
* How far the sunken ground sits below the page, in oklch lightness.
|
|
34
|
+
*
|
|
35
|
+
* Small on purpose: sunken is a well, not a second theme. Across the eight
|
|
36
|
+
* shipped presets the authored gap runs −0.011 to −0.031.
|
|
37
|
+
*/
|
|
38
|
+
const SUNKEN_LIGHTNESS_STEP = -.02;
|
|
39
|
+
/**
|
|
40
|
+
* The three raised surfaces, as lightness above the ground.
|
|
41
|
+
*
|
|
42
|
+
* Even spacing, because the ladder encodes *elevation* and a reader counts
|
|
43
|
+
* rungs. The shipped presets cluster around these three values.
|
|
44
|
+
*/
|
|
45
|
+
const SURFACE_LIGHTNESS_STEPS = [
|
|
46
|
+
.03,
|
|
47
|
+
.05,
|
|
48
|
+
.07
|
|
49
|
+
];
|
|
50
|
+
/**
|
|
51
|
+
* The hairline and its stronger sibling, as lightness above the ground.
|
|
52
|
+
*
|
|
53
|
+
* Above the top surface, so a border still reads as a line when it is drawn on
|
|
54
|
+
* `surface-3`. `border-overlay` reuses the strong value: an overlay floats over
|
|
55
|
+
* everything, so its rim has to hold against the brightest thing under it.
|
|
56
|
+
*/
|
|
57
|
+
const BORDER_LIGHTNESS_STEPS = [.08, .14];
|
|
58
|
+
/** How far secondary ink recedes from primary ink. */
|
|
59
|
+
const FG_MUTED_LIGHTNESS_STEP = -.25;
|
|
60
|
+
/** How far tertiary ink recedes from primary ink. */
|
|
61
|
+
const FG_SUBTLE_LIGHTNESS_STEP = -.45;
|
|
62
|
+
/**
|
|
63
|
+
* Chroma each rung of the ink ramp keeps from the rung above.
|
|
64
|
+
*
|
|
65
|
+
* Ink greys as it recedes rather than staying saturated: a muted label that held
|
|
66
|
+
* full chroma would read as a *coloured* label — a status, a link — instead of a
|
|
67
|
+
* quiet one. The hue is preserved throughout, so the ramp still belongs to the
|
|
68
|
+
* theme. `fg-subtle` takes the square, being two rungs down.
|
|
69
|
+
*/
|
|
70
|
+
const INK_RAMP_CHROMA_SCALE = .6;
|
|
71
|
+
/** How far the accent darkens on hover. */
|
|
72
|
+
const ACCENT_HOVER_LIGHTNESS_STEP = -.05;
|
|
73
|
+
/** How much accent a derived selection wash carries. */
|
|
74
|
+
const SELECTION_ACCENT_PERCENT = 22;
|
|
75
|
+
/** What a label on a solid accent has to reach before this module accepts it. */
|
|
76
|
+
const ACCENT_FG_MINIMUM = WCAG_AA_TEXT;
|
|
77
|
+
/**
|
|
78
|
+
* How much of the dark ground's lightness survives the flip to paper.
|
|
79
|
+
*
|
|
80
|
+
* A ground's job is to be the extreme, so inverting it means going to the *other*
|
|
81
|
+
* extreme rather than to the arithmetic mirror: `1 − L` of a 0.16 near-black is a
|
|
82
|
+
* 0.84 mid-grey, which is not a page. Compressing instead maps the ladder of
|
|
83
|
+
* dark grounds onto a narrow paper band — a deeper dark theme gets a cleaner
|
|
84
|
+
* white page — and lands within 0.01 of all eight authored light presets.
|
|
85
|
+
*/
|
|
86
|
+
const LIGHT_GROUND_COMPRESSION = .1;
|
|
87
|
+
/** Lightness a derived light colour steps down by while hunting for contrast. */
|
|
88
|
+
const LIGHT_DARKEN_STEP = .02;
|
|
89
|
+
/** How many steps that hunt may take before it gives up and returns its darkest. */
|
|
90
|
+
const LIGHT_DARKEN_LIMIT = 40;
|
|
91
|
+
const toOklch = converter("oklch");
|
|
92
|
+
const inSrgb = inGamut("rgb");
|
|
93
|
+
/** Read an anchor into oklch, or say which anchor could not be read. */
|
|
94
|
+
function readColor(value, label) {
|
|
95
|
+
const color = toOklch(parse(value));
|
|
96
|
+
if (color === void 0) throw new TypeError(`${label} is not a colour: ${JSON.stringify(value)}`);
|
|
97
|
+
return {
|
|
98
|
+
mode: "oklch",
|
|
99
|
+
l: color.l,
|
|
100
|
+
c: color.c,
|
|
101
|
+
h: color.h ?? 0
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Print a derived colour.
|
|
106
|
+
*
|
|
107
|
+
* Chroma is **clamped** into sRGB rather than clipped — clipping a too-saturated
|
|
108
|
+
* step turns it into a different hue, whereas clamping only gives up the
|
|
109
|
+
* saturation no display was going to show. Then it goes out through the same
|
|
110
|
+
* formatter the colour picker writes with, so a generated token and a
|
|
111
|
+
* hand-picked one are spelled the same way.
|
|
112
|
+
*/
|
|
113
|
+
function emit(color) {
|
|
114
|
+
const hex = formatHex(inSrgb(color) ? color : clampChroma(color, "oklch", "rgb"));
|
|
115
|
+
return formatColorString(hex, "oklch") ?? hex;
|
|
116
|
+
}
|
|
117
|
+
const clampLightness = (lightness) => Math.min(1, Math.max(0, lightness));
|
|
118
|
+
/** The same hue, moved along the lightness axis and optionally greyed. */
|
|
119
|
+
function shift(color, delta, chromaScale = 1) {
|
|
120
|
+
return emit({
|
|
121
|
+
mode: "oklch",
|
|
122
|
+
l: clampLightness(color.l + delta),
|
|
123
|
+
c: color.c * chromaScale,
|
|
124
|
+
h: color.h
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The candidate that can be read on `ground`, preferring the theme's own ink.
|
|
129
|
+
*
|
|
130
|
+
* Tried in order: the colours the theme already owns, then pure black and white
|
|
131
|
+
* as a last resort. A theme's own ground or ink keeps the palette coherent; the
|
|
132
|
+
* fallback exists because an accent at mid lightness contrasts with neither.
|
|
133
|
+
*/
|
|
134
|
+
function pickInk(ground, candidates) {
|
|
135
|
+
let best = candidates[0] ?? "#000000";
|
|
136
|
+
let bestRatio = -1;
|
|
137
|
+
for (const candidate of candidates) {
|
|
138
|
+
const ratio = contrastRatio(candidate, ground);
|
|
139
|
+
if (ratio > bestRatio) [best, bestRatio] = [candidate, ratio];
|
|
140
|
+
}
|
|
141
|
+
if (bestRatio >= ACCENT_FG_MINIMUM) return best;
|
|
142
|
+
for (const candidate of ["#000000", "#ffffff"]) {
|
|
143
|
+
const ratio = contrastRatio(candidate, ground);
|
|
144
|
+
if (ratio > bestRatio) [best, bestRatio] = [candidate, ratio];
|
|
145
|
+
}
|
|
146
|
+
return best;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Darken a colour until it can be read on `ground`.
|
|
150
|
+
*
|
|
151
|
+
* Only ever applied to values this module invented — the light variant's accent
|
|
152
|
+
* and status tones, which have to survive being moved from a near-black ground
|
|
153
|
+
* to paper. An authored colour is never quietly adjusted; it is reported.
|
|
154
|
+
*/
|
|
155
|
+
function darkenToContrast(value, ground, minimum, label) {
|
|
156
|
+
const start = readColor(value, label);
|
|
157
|
+
for (let step = 0; step <= LIGHT_DARKEN_LIMIT; step += 1) {
|
|
158
|
+
const candidate = emit({
|
|
159
|
+
...start,
|
|
160
|
+
l: clampLightness(start.l - step * LIGHT_DARKEN_STEP)
|
|
161
|
+
});
|
|
162
|
+
if (contrastRatio(candidate, ground) >= minimum) return candidate;
|
|
163
|
+
}
|
|
164
|
+
return emit({
|
|
165
|
+
...start,
|
|
166
|
+
l: clampLightness(start.l - LIGHT_DARKEN_LIMIT * LIGHT_DARKEN_STEP)
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/** Own-property lookup that admits a token may be absent. */
|
|
170
|
+
function lookup(map, token) {
|
|
171
|
+
return map != null && Object.hasOwn(map, token) ? map[token] : void 0;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* A derived selection wash, as a live expression rather than a resolved colour.
|
|
175
|
+
*
|
|
176
|
+
* `var(--cue-accent)` is a self-reference — it resolves inside whichever block
|
|
177
|
+
* declares it — so one string is correct in the dark block and the light one,
|
|
178
|
+
* and stays correct if a consumer overrides the accent in tier 1 later.
|
|
179
|
+
*/
|
|
180
|
+
const SELECTION_WASH = `color-mix(in oklch, var(--cue-accent) 22%, transparent)`;
|
|
181
|
+
/** The five status tones, in the order every theme block declares them. */
|
|
182
|
+
const STATUSES = [
|
|
183
|
+
"ok",
|
|
184
|
+
"busy",
|
|
185
|
+
"warn",
|
|
186
|
+
"danger",
|
|
187
|
+
"info"
|
|
188
|
+
];
|
|
189
|
+
/** The three raised surfaces, in ladder order. */
|
|
190
|
+
const SURFACES = [
|
|
191
|
+
"surface-1",
|
|
192
|
+
"surface-2",
|
|
193
|
+
"surface-3"
|
|
194
|
+
];
|
|
195
|
+
/**
|
|
196
|
+
* Resolve the dark block.
|
|
197
|
+
*
|
|
198
|
+
* The shape of every branch is the same: an explicit override wins; otherwise
|
|
199
|
+
* the preset's value stands *unless the anchor this token hangs off has moved*;
|
|
200
|
+
* only then is anything derived.
|
|
201
|
+
*/
|
|
202
|
+
function resolveDark(anchors, preset) {
|
|
203
|
+
const colors = {};
|
|
204
|
+
const authored = /* @__PURE__ */ new Set();
|
|
205
|
+
const surfaces = anchors.surfaces ?? {};
|
|
206
|
+
const status = anchors.status ?? {};
|
|
207
|
+
/**
|
|
208
|
+
* One token. `derive` is omitted for the anchors themselves — a preset that
|
|
209
|
+
* declares no ground is not something to paper over with an invented one.
|
|
210
|
+
*/
|
|
211
|
+
const define = (token, explicit, moved, derive) => {
|
|
212
|
+
if (explicit !== void 0) {
|
|
213
|
+
readColor(explicit, token);
|
|
214
|
+
colors[token] = explicit;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const inherited = lookup(preset, token);
|
|
218
|
+
if (!moved && inherited !== void 0) {
|
|
219
|
+
colors[token] = inherited;
|
|
220
|
+
authored.add(token);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (derive === void 0) throw new TypeError(`the base preset declares no ${token}`);
|
|
224
|
+
colors[token] = derive();
|
|
225
|
+
};
|
|
226
|
+
const groundMoved = anchors.bg !== void 0;
|
|
227
|
+
const inkMoved = anchors.fg !== void 0;
|
|
228
|
+
const accentMoved = anchors.accent !== void 0;
|
|
229
|
+
define("bg", anchors.bg, false);
|
|
230
|
+
const ground = readColor(colors["bg"], "bg");
|
|
231
|
+
define("sunken", surfaces.sunken, groundMoved, () => shift(ground, SUNKEN_LIGHTNESS_STEP));
|
|
232
|
+
SURFACES.forEach((token, index) => {
|
|
233
|
+
define(token, surfaces[token], groundMoved, () => shift(ground, SURFACE_LIGHTNESS_STEPS[index]));
|
|
234
|
+
});
|
|
235
|
+
define("fg", anchors.fg, false);
|
|
236
|
+
const ink = readColor(colors["fg"], "fg");
|
|
237
|
+
define("fg-muted", void 0, inkMoved, () => shift(ink, FG_MUTED_LIGHTNESS_STEP, INK_RAMP_CHROMA_SCALE));
|
|
238
|
+
define("fg-subtle", void 0, inkMoved, () => shift(ink, FG_SUBTLE_LIGHTNESS_STEP, INK_RAMP_CHROMA_SCALE ** 2));
|
|
239
|
+
define("border", void 0, groundMoved, () => shift(ground, BORDER_LIGHTNESS_STEPS[0]));
|
|
240
|
+
define("border-strong", void 0, groundMoved, () => shift(ground, BORDER_LIGHTNESS_STEPS[1]));
|
|
241
|
+
define("border-overlay", void 0, groundMoved, () => colors["border-strong"]);
|
|
242
|
+
define("accent", anchors.accent, false);
|
|
243
|
+
const accent = colors["accent"];
|
|
244
|
+
define("accent-hover", void 0, accentMoved, () => shift(readColor(accent, "accent"), ACCENT_HOVER_LIGHTNESS_STEP));
|
|
245
|
+
define("accent-fg", void 0, accentMoved, () => pickInk(accent, [colors["bg"], colors["fg"]]));
|
|
246
|
+
for (const tone of STATUSES) define(tone, status[tone], false);
|
|
247
|
+
define("selection", void 0, accentMoved, () => SELECTION_WASH);
|
|
248
|
+
define("focus", void 0, accentMoved, () => accent);
|
|
249
|
+
define("data-ground", void 0, false);
|
|
250
|
+
define("scrim", void 0, false);
|
|
251
|
+
return {
|
|
252
|
+
colors,
|
|
253
|
+
authored
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Resolve the light block.
|
|
258
|
+
*
|
|
259
|
+
* Per token: if the dark value came from the preset untouched and the preset
|
|
260
|
+
* ships a light block, the authored light value stands. Everything else is
|
|
261
|
+
* inverted — which is guesswork, and is why any derivation here marks the report
|
|
262
|
+
* provisional.
|
|
263
|
+
*
|
|
264
|
+
* The inversion is not `1 − L` across the board. The ground goes to paper (see
|
|
265
|
+
* {@link LIGHT_GROUND_COMPRESSION}) and the ladder then steps *down* from it,
|
|
266
|
+
* because paper leaves no headroom above: a "raised" surface on a white page is
|
|
267
|
+
* the slightly grey one, and the sunken well — the one rung that cannot keep its
|
|
268
|
+
* sign — folds to the far end, which is still the deepest thing on the page.
|
|
269
|
+
*/
|
|
270
|
+
function resolveLight(dark, preset) {
|
|
271
|
+
const colors = {};
|
|
272
|
+
let provisional = false;
|
|
273
|
+
const define = (token, derive) => {
|
|
274
|
+
const inherited = dark.authored.has(token) ? lookup(preset, token) : void 0;
|
|
275
|
+
if (inherited !== void 0) {
|
|
276
|
+
colors[token] = inherited;
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
colors[token] = derive();
|
|
280
|
+
provisional = true;
|
|
281
|
+
};
|
|
282
|
+
const darkGround = readColor(dark.colors["bg"], "bg");
|
|
283
|
+
define("bg", () => emit({
|
|
284
|
+
mode: "oklch",
|
|
285
|
+
l: clampLightness(1 - LIGHT_GROUND_COMPRESSION * darkGround.l),
|
|
286
|
+
c: darkGround.c,
|
|
287
|
+
h: darkGround.h
|
|
288
|
+
}));
|
|
289
|
+
const paper = readColor(colors["bg"], "bg");
|
|
290
|
+
/** Deepest rung: past the top surface by the sunken step. */
|
|
291
|
+
const sunkenDepth = (SURFACE_LIGHTNESS_STEPS.at(-1) ?? 0) + Math.abs(SUNKEN_LIGHTNESS_STEP);
|
|
292
|
+
define("sunken", () => shift(paper, -sunkenDepth));
|
|
293
|
+
SURFACES.forEach((token, index) => {
|
|
294
|
+
define(token, () => shift(paper, -SURFACE_LIGHTNESS_STEPS[index]));
|
|
295
|
+
});
|
|
296
|
+
const darkInk = readColor(dark.colors["fg"], "fg");
|
|
297
|
+
define("fg", () => emit({
|
|
298
|
+
mode: "oklch",
|
|
299
|
+
l: clampLightness(1 - darkInk.l),
|
|
300
|
+
c: darkInk.c,
|
|
301
|
+
h: darkInk.h
|
|
302
|
+
}));
|
|
303
|
+
const ink = readColor(colors["fg"], "fg");
|
|
304
|
+
define("fg-muted", () => shift(ink, .25, INK_RAMP_CHROMA_SCALE));
|
|
305
|
+
define("fg-subtle", () => shift(ink, .45, INK_RAMP_CHROMA_SCALE ** 2));
|
|
306
|
+
define("border", () => shift(paper, -BORDER_LIGHTNESS_STEPS[0]));
|
|
307
|
+
define("border-strong", () => shift(paper, -BORDER_LIGHTNESS_STEPS[1]));
|
|
308
|
+
define("border-overlay", () => colors["border-strong"]);
|
|
309
|
+
const paperGround = colors["bg"];
|
|
310
|
+
define("accent", () => darkenToContrast(dark.colors["accent"], paperGround, WCAG_AA_TEXT, "accent"));
|
|
311
|
+
const accent = colors["accent"];
|
|
312
|
+
define("accent-hover", () => shift(readColor(accent, "accent"), ACCENT_HOVER_LIGHTNESS_STEP));
|
|
313
|
+
define("accent-fg", () => pickInk(accent, [colors["bg"], colors["fg"]]));
|
|
314
|
+
for (const tone of STATUSES) define(tone, () => darkenToContrast(dark.colors[tone], paperGround, 3, tone));
|
|
315
|
+
define("selection", () => SELECTION_WASH);
|
|
316
|
+
define("focus", () => accent);
|
|
317
|
+
define("data-ground", () => dark.colors["data-ground"]);
|
|
318
|
+
define("scrim", () => dark.colors["scrim"]);
|
|
319
|
+
return {
|
|
320
|
+
colors,
|
|
321
|
+
provisional
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/** The font declarations a theme block carries. */
|
|
325
|
+
function resolveFonts(anchors, preset) {
|
|
326
|
+
const fonts = anchors.fonts ?? {};
|
|
327
|
+
const declared = [];
|
|
328
|
+
if (fonts.sans !== void 0) declared.push(["font-sans", fonts.sans]);
|
|
329
|
+
declared.push(["font-mono", fonts.mono ?? lookup(preset, "font-mono") ?? BASE_FONT_STACKS["font-mono"]]);
|
|
330
|
+
if (fonts.display !== void 0) declared.push(["font-display", fonts.display]);
|
|
331
|
+
return declared.map(([name, value]) => [tokenProperty(name), assertCssValue(name, value)]);
|
|
332
|
+
}
|
|
333
|
+
/** Round a product of two scales to something a stylesheet can print. */
|
|
334
|
+
const scaled = (a, b) => String(Number((a * b).toFixed(4)));
|
|
335
|
+
/**
|
|
336
|
+
* The radius rules for a theme that asked for one.
|
|
337
|
+
*
|
|
338
|
+
* Geometry belongs to the density axis, so the multiplier is **crossed** with it
|
|
339
|
+
* rather than declared flat: an uncrossed `--cue-radius-scale` in a theme block
|
|
340
|
+
* outranks the density block that set it and would freeze `ultra-compact`'s 0.75
|
|
341
|
+
* at 1. The base block carries the product for the default density — that is the
|
|
342
|
+
* value an unstamped document falls back to — and every density whose own scale
|
|
343
|
+
* differs gets one crossed rule.
|
|
344
|
+
*/
|
|
345
|
+
function radiusBlocks(name, radiusScale) {
|
|
346
|
+
const property = tokenProperty("radius-scale");
|
|
347
|
+
const fallback = DENSITY_RADIUS_SCALE[DEFAULT_DENSITY_LEVEL];
|
|
348
|
+
const blocks = [];
|
|
349
|
+
for (const [level, scale] of Object.entries(DENSITY_RADIUS_SCALE)) {
|
|
350
|
+
if (scale === fallback) continue;
|
|
351
|
+
blocks.push({
|
|
352
|
+
selector: themeSelector(name, { density: level }),
|
|
353
|
+
sections: [{ declarations: [[property, scaled(radiusScale, scale)]] }]
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
base: [[property, scaled(radiusScale, fallback)]],
|
|
358
|
+
blocks
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/** `[--cue-token, value]` pairs, in the order the contract lists them. */
|
|
362
|
+
function colorDeclarations(colors) {
|
|
363
|
+
return THEME_COLOR_TOKENS.map((token) => [tokenProperty(token), colors[token]]);
|
|
364
|
+
}
|
|
365
|
+
const DERIVED_COMMENT = "derived — kept inside the block so color-mix() reads this theme's values";
|
|
366
|
+
const FONT_COMMENT = "font — the theme's override, or the base stack restated";
|
|
367
|
+
const SHAPE_COMMENT = "shape — crossed with the density ladder below, never flat";
|
|
368
|
+
/**
|
|
369
|
+
* Generate a theme: a stylesheet, its token map, and a contrast report.
|
|
370
|
+
*
|
|
371
|
+
* Pure and synchronous. Call it in a build script, in a test, or in the
|
|
372
|
+
* configurator's export dialog — never in a render path, because there is
|
|
373
|
+
* nothing here a stylesheet is not already doing faster.
|
|
374
|
+
*
|
|
375
|
+
* @param anchors - The name, the preset to start from, and whatever you moved.
|
|
376
|
+
* @returns The stylesheet, the resolved tokens for both modes, and the report.
|
|
377
|
+
* @throws TypeError if the name is not a CSS identifier, an anchor is not a
|
|
378
|
+
* colour, a font stack carries CSS punctuation, or the base preset is unknown.
|
|
379
|
+
* @example
|
|
380
|
+
* const { css, report } = createTheme({ name: "acme", base: "terminal", accent: "#ff8800" });
|
|
381
|
+
* if (!report.passes) console.warn(report.failures);
|
|
382
|
+
*/
|
|
383
|
+
function createTheme(anchors) {
|
|
384
|
+
const name = assertThemeName(anchors.name);
|
|
385
|
+
const baseName = anchors.base ?? "cue";
|
|
386
|
+
const preset = THEME_PRESETS[baseName];
|
|
387
|
+
if (preset === void 0) throw new TypeError(`unknown base theme: ${JSON.stringify(baseName)}`);
|
|
388
|
+
if (anchors.radiusScale !== void 0 && !(anchors.radiusScale > 0)) throw new TypeError(`radiusScale must be a positive number, got ${anchors.radiusScale}`);
|
|
389
|
+
const dark = resolveDark(anchors, preset.dark);
|
|
390
|
+
const light = anchors.supportsLight ?? preset.light !== null ? resolveLight(dark, preset.light) : null;
|
|
391
|
+
const derived = Object.entries(derivedTokens());
|
|
392
|
+
const fonts = resolveFonts(anchors, preset.fonts);
|
|
393
|
+
const radius = anchors.radiusScale === void 0 ? null : radiusBlocks(name, anchors.radiusScale);
|
|
394
|
+
const darkSections = [
|
|
395
|
+
{ declarations: colorDeclarations(dark.colors) },
|
|
396
|
+
{
|
|
397
|
+
comment: DERIVED_COMMENT,
|
|
398
|
+
declarations: derived
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
comment: FONT_COMMENT,
|
|
402
|
+
declarations: fonts
|
|
403
|
+
},
|
|
404
|
+
...radius === null ? [] : [{
|
|
405
|
+
comment: SHAPE_COMMENT,
|
|
406
|
+
declarations: radius.base
|
|
407
|
+
}]
|
|
408
|
+
];
|
|
409
|
+
const blocks = [
|
|
410
|
+
{
|
|
411
|
+
selector: themeSelector(name),
|
|
412
|
+
colorScheme: "dark",
|
|
413
|
+
sections: darkSections
|
|
414
|
+
},
|
|
415
|
+
...light === null ? [] : [{
|
|
416
|
+
selector: themeSelector(name, { mode: "light" }),
|
|
417
|
+
colorScheme: "light",
|
|
418
|
+
sections: [{ declarations: colorDeclarations(light.colors) }, {
|
|
419
|
+
comment: DERIVED_COMMENT,
|
|
420
|
+
declarations: derived
|
|
421
|
+
}]
|
|
422
|
+
}],
|
|
423
|
+
...radius?.blocks ?? []
|
|
424
|
+
];
|
|
425
|
+
const inputs = [{
|
|
426
|
+
mode: "dark",
|
|
427
|
+
tokens: dark.colors
|
|
428
|
+
}, ...light === null ? [] : [{
|
|
429
|
+
mode: "light",
|
|
430
|
+
tokens: light.colors,
|
|
431
|
+
provisional: light.provisional
|
|
432
|
+
}]];
|
|
433
|
+
return {
|
|
434
|
+
name,
|
|
435
|
+
css: serializeThemeCss(blocks, { banner: `@cueplusplus/ui — theme "${name}"` }),
|
|
436
|
+
tokens: Object.fromEntries(darkSections.flatMap((section) => section.declarations)),
|
|
437
|
+
light: light === null ? null : Object.fromEntries([...colorDeclarations(light.colors), ...derived]),
|
|
438
|
+
report: contrastReport(inputs)
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
//#endregion
|
|
442
|
+
export { ACCENT_FG_MINIMUM, ACCENT_HOVER_LIGHTNESS_STEP, BORDER_LIGHTNESS_STEPS, DEFAULT_THEME_BASE, FG_MUTED_LIGHTNESS_STEP, FG_SUBTLE_LIGHTNESS_STEP, INK_RAMP_CHROMA_SCALE, LIGHT_GROUND_COMPRESSION, SELECTION_ACCENT_PERCENT, SUNKEN_LIGHTNESS_STEP, SURFACE_LIGHTNESS_STEPS, THEME_COLOR_TOKENS, createTheme };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { CONTRAST_REQUIREMENTS, ContrastCheck, ContrastInput, ContrastReport, ContrastRequirement, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, contrastRatio, contrastReport, relativeLuminance } from "./contrast.js";
|
|
2
|
+
import { THEME_COLOR_TOKENS, ThemeColorToken } from "./_presets.js";
|
|
3
|
+
import { ACCENT_FG_MINIMUM, ACCENT_HOVER_LIGHTNESS_STEP, BORDER_LIGHTNESS_STEPS, CreatedTheme, DEFAULT_THEME_BASE, FG_MUTED_LIGHTNESS_STEP, FG_SUBTLE_LIGHTNESS_STEP, INK_RAMP_CHROMA_SCALE, LIGHT_GROUND_COMPRESSION, SELECTION_ACCENT_PERCENT, SUNKEN_LIGHTNESS_STEP, SURFACE_LIGHTNESS_STEPS, ThemeAnchors, createTheme } from "./create-theme.js";
|
|
4
|
+
import { CUE_TOKEN_PREFIX, DERIVED_TOKEN_TEMPLATES, SerializeThemeCssOptions, THEME_NAME_PATTERN, ThemeBlock, ThemeBlockSection, ThemeSelectorOptions, assertCssValue, assertThemeName, derivedTokens, serializeThemeBlock, serializeThemeCss, themeSelector, tokenProperty } from "./serialize.js";
|
|
5
|
+
export { ACCENT_FG_MINIMUM, ACCENT_HOVER_LIGHTNESS_STEP, BORDER_LIGHTNESS_STEPS, CONTRAST_REQUIREMENTS, CUE_TOKEN_PREFIX, type ContrastCheck, type ContrastInput, type ContrastReport, type ContrastRequirement, type CreatedTheme, DEFAULT_THEME_BASE, DERIVED_TOKEN_TEMPLATES, FG_MUTED_LIGHTNESS_STEP, FG_SUBTLE_LIGHTNESS_STEP, INK_RAMP_CHROMA_SCALE, LIGHT_GROUND_COMPRESSION, SELECTION_ACCENT_PERCENT, SUNKEN_LIGHTNESS_STEP, SURFACE_LIGHTNESS_STEPS, type SerializeThemeCssOptions, THEME_COLOR_TOKENS, THEME_NAME_PATTERN, type ThemeAnchors, type ThemeBlock, type ThemeBlockSection, type ThemeColorToken, type ThemeSelectorOptions, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, assertCssValue, assertThemeName, contrastRatio, contrastReport, createTheme, derivedTokens, relativeLuminance, serializeThemeBlock, serializeThemeCss, themeSelector, tokenProperty };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { THEME_COLOR_TOKENS } from "./_presets.js";
|
|
2
|
+
import { CONTRAST_REQUIREMENTS, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, contrastRatio, contrastReport, relativeLuminance } from "./contrast.js";
|
|
3
|
+
import { CUE_TOKEN_PREFIX, DERIVED_TOKEN_TEMPLATES, THEME_NAME_PATTERN, assertCssValue, assertThemeName, derivedTokens, serializeThemeBlock, serializeThemeCss, themeSelector, tokenProperty } from "./serialize.js";
|
|
4
|
+
import { ACCENT_FG_MINIMUM, ACCENT_HOVER_LIGHTNESS_STEP, BORDER_LIGHTNESS_STEPS, DEFAULT_THEME_BASE, FG_MUTED_LIGHTNESS_STEP, FG_SUBTLE_LIGHTNESS_STEP, INK_RAMP_CHROMA_SCALE, LIGHT_GROUND_COMPRESSION, SELECTION_ACCENT_PERCENT, SUNKEN_LIGHTNESS_STEP, SURFACE_LIGHTNESS_STEPS, createTheme } from "./create-theme.js";
|
|
5
|
+
export { ACCENT_FG_MINIMUM, ACCENT_HOVER_LIGHTNESS_STEP, BORDER_LIGHTNESS_STEPS, CONTRAST_REQUIREMENTS, CUE_TOKEN_PREFIX, DEFAULT_THEME_BASE, DERIVED_TOKEN_TEMPLATES, FG_MUTED_LIGHTNESS_STEP, FG_SUBTLE_LIGHTNESS_STEP, INK_RAMP_CHROMA_SCALE, LIGHT_GROUND_COMPRESSION, SELECTION_ACCENT_PERCENT, SUNKEN_LIGHTNESS_STEP, SURFACE_LIGHTNESS_STEPS, THEME_COLOR_TOKENS, THEME_NAME_PATTERN, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, assertCssValue, assertThemeName, contrastRatio, contrastReport, createTheme, derivedTokens, relativeLuminance, serializeThemeBlock, serializeThemeCss, themeSelector, tokenProperty };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Density } from "@cueplusplus/tokens";
|
|
2
|
+
//#region src/theming/serialize.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Turning a resolved token map back into the stylesheet the tokens build would
|
|
5
|
+
* have written.
|
|
6
|
+
*
|
|
7
|
+
* This module exists so there is exactly **one** spelling of the output format.
|
|
8
|
+
* The spec's whole third-party theming story is "tier 2 always compiles down to
|
|
9
|
+
* tier 1" — a generated theme has to be indistinguishable from a hand-authored
|
|
10
|
+
* `[data-theme]` block, or a consumer ends up with two vocabularies and the
|
|
11
|
+
* generator becomes a toy. So the shapes below mirror `build.mjs` deliberately,
|
|
12
|
+
* comments included: same selector form, same declaration order, same section
|
|
13
|
+
* breaks. `test/theming/presets.test.ts` diffs the two.
|
|
14
|
+
*
|
|
15
|
+
* It is also the only place that touches raw strings, which makes it the place
|
|
16
|
+
* to stop injection. A theme name lands inside an attribute selector and a font
|
|
17
|
+
* stack lands inside a declaration; both are checked here rather than trusted.
|
|
18
|
+
*/
|
|
19
|
+
/** The one custom-property prefix this design system emits. */
|
|
20
|
+
declare const CUE_TOKEN_PREFIX = "--cue-";
|
|
21
|
+
/**
|
|
22
|
+
* What a theme may be called.
|
|
23
|
+
*
|
|
24
|
+
* The name is interpolated into `[data-theme="…"]`, so the pattern is a
|
|
25
|
+
* whitelist rather than an escape: a CSS identifier, which is also what a
|
|
26
|
+
* `data-theme` attribute is expected to hold and what a file called
|
|
27
|
+
* `<name>.css` can be named.
|
|
28
|
+
*/
|
|
29
|
+
declare const THEME_NAME_PATTERN: RegExp;
|
|
30
|
+
/**
|
|
31
|
+
* Check a theme name and hand it back.
|
|
32
|
+
*
|
|
33
|
+
* @param name - The proposed theme name.
|
|
34
|
+
* @returns The same name, once it is known to be safe to interpolate.
|
|
35
|
+
* @throws TypeError if the name is not a CSS identifier.
|
|
36
|
+
* @example
|
|
37
|
+
* assertThemeName("acme-dark"); // → "acme-dark"
|
|
38
|
+
*/
|
|
39
|
+
declare function assertThemeName(name: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Check a free-text declaration value and hand it back.
|
|
42
|
+
*
|
|
43
|
+
* @param name - The token the value belongs to, for the error message.
|
|
44
|
+
* @param value - The proposed value.
|
|
45
|
+
* @returns The same value, once it is known to stay inside its declaration.
|
|
46
|
+
* @throws TypeError if the value could end the declaration or the block.
|
|
47
|
+
* @example
|
|
48
|
+
* assertCssValue("font-mono", '"Berkeley Mono", monospace');
|
|
49
|
+
*/
|
|
50
|
+
declare function assertCssValue(name: string, value: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* The custom property a token is emitted as.
|
|
53
|
+
*
|
|
54
|
+
* Idempotent, so code that already holds `--cue-bg` can pass it straight back
|
|
55
|
+
* through instead of stripping the prefix first.
|
|
56
|
+
*
|
|
57
|
+
* @param name - A bare token name, or an already-prefixed property.
|
|
58
|
+
* @returns The `--cue-*` custom property name.
|
|
59
|
+
* @example
|
|
60
|
+
* tokenProperty("surface-1"); // → "--cue-surface-1"
|
|
61
|
+
*/
|
|
62
|
+
declare function tokenProperty(name: string): string;
|
|
63
|
+
interface ThemeSelectorOptions {
|
|
64
|
+
/** Cross with the mode axis. Only `"light"` needs a selector; dark is the base block. */
|
|
65
|
+
mode?: "dark" | "light";
|
|
66
|
+
/** Cross with the density axis. Used only for the radius multiplier. */
|
|
67
|
+
density?: Density;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The selector a theme's declarations live under.
|
|
71
|
+
*
|
|
72
|
+
* Attribute selectors, not classes, because the two axes have to compose: a
|
|
73
|
+
* `[data-theme]` on the root and another on a portal, a `[data-mode]` crossed
|
|
74
|
+
* over either, all at the same specificity so later blocks win predictably.
|
|
75
|
+
*
|
|
76
|
+
* @param name - The theme name. Not re-checked — {@link assertThemeName} owns that.
|
|
77
|
+
* @param options - Which axes to cross with.
|
|
78
|
+
* @returns The CSS selector.
|
|
79
|
+
* @example
|
|
80
|
+
* themeSelector("acme", { mode: "light" }); // → '[data-theme="acme"][data-mode="light"]'
|
|
81
|
+
*/
|
|
82
|
+
declare function themeSelector(name: string, options?: ThemeSelectorOptions): string;
|
|
83
|
+
/**
|
|
84
|
+
* The derived colours every theme block carries, as `[token, value]` pairs.
|
|
85
|
+
*
|
|
86
|
+
* They are `color-mix()` expressions over `var(--cue-*)` rather than resolved
|
|
87
|
+
* literals, and they are emitted **inside** the theme block rather than once on
|
|
88
|
+
* `:root`, for the same reason the build does it: a custom property's `var()`
|
|
89
|
+
* references are substituted at the element that declares it, so a `:root`-level
|
|
90
|
+
* mix would freeze the default theme's accent into every nested theme.
|
|
91
|
+
*/
|
|
92
|
+
declare const DERIVED_TOKEN_TEMPLATES: ReadonlyArray<readonly [string, string]>;
|
|
93
|
+
/**
|
|
94
|
+
* {@link DERIVED_TOKEN_TEMPLATES} as a property → value map.
|
|
95
|
+
*
|
|
96
|
+
* @returns The eleven derived declarations, keyed by custom property.
|
|
97
|
+
* @example
|
|
98
|
+
* derivedTokens()["--cue-accent-soft"];
|
|
99
|
+
*/
|
|
100
|
+
declare function derivedTokens(): Record<string, string>;
|
|
101
|
+
/** One run of declarations, optionally introduced by a comment. */
|
|
102
|
+
interface ThemeBlockSection {
|
|
103
|
+
/** Comment printed above the run. Omit on the first section. */
|
|
104
|
+
comment?: string;
|
|
105
|
+
/** `[property, value]` pairs, in emission order. */
|
|
106
|
+
declarations: ReadonlyArray<readonly [string, string]>;
|
|
107
|
+
}
|
|
108
|
+
/** One CSS rule of a generated theme. */
|
|
109
|
+
interface ThemeBlock {
|
|
110
|
+
/** The selector the rule declares under. */
|
|
111
|
+
selector: string;
|
|
112
|
+
/** Emitted as `color-scheme`, ahead of the custom properties. Omit to skip it. */
|
|
113
|
+
colorScheme?: "dark" | "light";
|
|
114
|
+
/** The declarations, grouped into commented sections. */
|
|
115
|
+
sections: readonly ThemeBlockSection[];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* One rule, printed.
|
|
119
|
+
*
|
|
120
|
+
* Empty sections vanish rather than leaving a comment with nothing under it —
|
|
121
|
+
* which is what makes "emit the fonts section only if there are fonts" a
|
|
122
|
+
* data decision instead of a branch at every call site.
|
|
123
|
+
*
|
|
124
|
+
* @param block - Selector, colour scheme and sections.
|
|
125
|
+
* @returns The rule, without a trailing newline.
|
|
126
|
+
* @example
|
|
127
|
+
* serializeThemeBlock({ selector: ":root", sections: [{ declarations: [["--cue-bg", "#000"]] }] });
|
|
128
|
+
*/
|
|
129
|
+
declare function serializeThemeBlock(block: ThemeBlock): string;
|
|
130
|
+
interface SerializeThemeCssOptions {
|
|
131
|
+
/** Text of the leading `/*! … *\/` banner. Omit for a bare stylesheet. */
|
|
132
|
+
banner?: string;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* A whole generated stylesheet: banner, then one rule per block.
|
|
136
|
+
*
|
|
137
|
+
* @param blocks - The rules, in cascade order — later blocks win ties.
|
|
138
|
+
* @param options - Banner text.
|
|
139
|
+
* @returns The stylesheet, newline-terminated.
|
|
140
|
+
* @example
|
|
141
|
+
* serializeThemeCss(blocks, { banner: "@cueplusplus/ui — createTheme()" });
|
|
142
|
+
*/
|
|
143
|
+
declare function serializeThemeCss(blocks: readonly ThemeBlock[], options?: SerializeThemeCssOptions): string;
|
|
144
|
+
//#endregion
|
|
145
|
+
export { CUE_TOKEN_PREFIX, DERIVED_TOKEN_TEMPLATES, SerializeThemeCssOptions, THEME_NAME_PATTERN, ThemeBlock, ThemeBlockSection, ThemeSelectorOptions, assertCssValue, assertThemeName, derivedTokens, serializeThemeBlock, serializeThemeCss, themeSelector, tokenProperty };
|