zabi-components 8.1.0 → 9.0.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/CHANGELOG.md +1011 -0
- package/README.md +1 -1
- package/THEME.md +18 -5
- package/THEMING.md +308 -74
- package/dist/atoms/ActionPanel.svelte +1 -1
- package/dist/atoms/Avatar.svelte +19 -3
- package/dist/atoms/Avatar.svelte.d.ts +12 -0
- package/dist/atoms/Badge.svelte +37 -8
- package/dist/atoms/Badge.svelte.d.ts +6 -0
- package/dist/atoms/Block.svelte +74 -0
- package/dist/atoms/Block.svelte.d.ts +29 -0
- package/dist/atoms/Button.svelte +52 -15
- package/dist/atoms/Button.svelte.d.ts +3 -2
- package/dist/atoms/Card.svelte +181 -25
- package/dist/atoms/Card.svelte.d.ts +55 -3
- package/dist/atoms/Checkbox.svelte +33 -0
- package/dist/atoms/Checkbox.svelte.d.ts +27 -0
- package/dist/atoms/Chip.svelte +351 -0
- package/dist/atoms/Chip.svelte.d.ts +57 -0
- package/dist/atoms/CodeField.svelte +385 -0
- package/dist/atoms/CodeField.svelte.d.ts +61 -0
- package/dist/atoms/ColorPicker.svelte +1 -0
- package/dist/atoms/DateField.svelte +41 -3
- package/dist/atoms/DateField.svelte.d.ts +31 -3
- package/dist/atoms/FloatingActionButton.svelte +5 -3
- package/dist/atoms/FloatingActionButton.svelte.d.ts +1 -1
- package/dist/atoms/Heading.svelte +27 -1
- package/dist/atoms/Heading.svelte.d.ts +7 -0
- package/dist/atoms/IconButton.svelte +81 -9
- package/dist/atoms/IconButton.svelte.d.ts +13 -0
- package/dist/atoms/Input.svelte +10 -5
- package/dist/atoms/Input.svelte.d.ts +3 -2
- package/dist/atoms/List.svelte +3 -2
- package/dist/atoms/ListItem.svelte +31 -7
- package/dist/atoms/ListItem.svelte.d.ts +6 -0
- package/dist/atoms/PickerField.svelte +239 -0
- package/dist/atoms/PickerField.svelte.d.ts +67 -0
- package/dist/atoms/Progress.svelte +78 -17
- package/dist/atoms/Progress.svelte.d.ts +11 -1
- package/dist/atoms/Radio.svelte +32 -0
- package/dist/atoms/Radio.svelte.d.ts +26 -0
- package/dist/atoms/Rating.svelte +132 -6
- package/dist/atoms/Rating.svelte.d.ts +24 -3
- package/dist/atoms/Select.svelte +24 -26
- package/dist/atoms/Select.svelte.d.ts +12 -0
- package/dist/atoms/SelectionControl.svelte +99 -6
- package/dist/atoms/SelectionControl.svelte.d.ts +25 -0
- package/dist/atoms/Skeleton.svelte +114 -18
- package/dist/atoms/Skeleton.svelte.d.ts +30 -3
- package/dist/atoms/SkeletonGroup.svelte +43 -0
- package/dist/atoms/SkeletonGroup.svelte.d.ts +20 -0
- package/dist/atoms/Slider.svelte +35 -0
- package/dist/atoms/Sparkline.svelte +109 -0
- package/dist/atoms/Sparkline.svelte.d.ts +27 -0
- package/dist/atoms/SpeechBubble.svelte +187 -0
- package/dist/atoms/SpeechBubble.svelte.d.ts +33 -0
- package/dist/atoms/Stat.svelte +72 -0
- package/dist/atoms/Stat.svelte.d.ts +19 -0
- package/dist/atoms/TemporalField.svelte +342 -10
- package/dist/atoms/TemporalField.svelte.d.ts +20 -4
- package/dist/atoms/Textarea.svelte +100 -5
- package/dist/atoms/Textarea.svelte.d.ts +21 -0
- package/dist/atoms/TimeColumn.svelte +150 -0
- package/dist/atoms/TimeColumn.svelte.d.ts +25 -0
- package/dist/atoms/TimeField.svelte +46 -3
- package/dist/atoms/TimeField.svelte.d.ts +34 -3
- package/dist/atoms/Toast.svelte +6 -5
- package/dist/atoms/Toggle.svelte +61 -10
- package/dist/atoms/Toggle.svelte.d.ts +15 -0
- package/dist/atoms/Tooltip.svelte +20 -1
- package/dist/atoms/Tooltip.svelte.d.ts +7 -0
- package/dist/atoms/index.d.ts +21 -0
- package/dist/atoms/index.js +17 -0
- package/dist/atoms/selection-control.styles.d.ts +15 -4
- package/dist/atoms/selection-control.styles.js +16 -5
- package/dist/components/atoms/index.d.ts +21 -0
- package/dist/components/atoms/index.d.ts.map +1 -1
- package/dist/components/atoms/selection-control.styles.d.ts +15 -4
- package/dist/components/atoms/selection-control.styles.d.ts.map +1 -1
- package/dist/components/index.d.ts +27 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/map.d.ts +11 -0
- package/dist/components/map.d.ts.map +1 -0
- package/dist/components/molecules/index.d.ts +7 -2
- package/dist/components/molecules/index.d.ts.map +1 -1
- package/dist/components/molecules/toast-store.d.ts +30 -0
- package/dist/components/molecules/toast-store.d.ts.map +1 -1
- package/dist/components/organisms/index.d.ts +6 -1
- package/dist/components/organisms/index.d.ts.map +1 -1
- package/dist/components/types/variants.d.ts +12 -3
- package/dist/components/types/variants.d.ts.map +1 -1
- package/dist/components/util/app-shell.d.ts +55 -4
- package/dist/components/util/app-shell.d.ts.map +1 -1
- package/dist/components/util/avatar.d.ts +3 -0
- package/dist/components/util/avatar.d.ts.map +1 -1
- package/dist/components/util/bottom-tab-bar.d.ts +6 -0
- package/dist/components/util/bottom-tab-bar.d.ts.map +1 -1
- package/dist/components/util/chart.d.ts +140 -0
- package/dist/components/util/chart.d.ts.map +1 -0
- package/dist/components/util/chip.d.ts +66 -0
- package/dist/components/util/chip.d.ts.map +1 -0
- package/dist/components/util/cn.d.ts.map +1 -1
- package/dist/components/util/code-field.d.ts +63 -0
- package/dist/components/util/code-field.d.ts.map +1 -0
- package/dist/components/util/collapsible.d.ts +6 -0
- package/dist/components/util/collapsible.d.ts.map +1 -1
- package/dist/components/util/display-voice.d.ts +10 -0
- package/dist/components/util/display-voice.d.ts.map +1 -0
- package/dist/components/util/field-control.d.ts +30 -0
- package/dist/components/util/field-control.d.ts.map +1 -0
- package/dist/components/util/field.d.ts +6 -0
- package/dist/components/util/field.d.ts.map +1 -1
- package/dist/components/util/focus-utils.d.ts +2 -0
- package/dist/components/util/focus-utils.d.ts.map +1 -1
- package/dist/components/util/map-view.d.ts +144 -0
- package/dist/components/util/map-view.d.ts.map +1 -0
- package/dist/components/util/photo.d.ts +19 -0
- package/dist/components/util/photo.d.ts.map +1 -1
- package/dist/components/util/progress.d.ts +19 -0
- package/dist/components/util/progress.d.ts.map +1 -0
- package/dist/components/util/scroll-edge.d.ts +33 -0
- package/dist/components/util/scroll-edge.d.ts.map +1 -0
- package/dist/components/util/sheet-drag.d.ts +25 -1
- package/dist/components/util/sheet-drag.d.ts.map +1 -1
- package/dist/components/util/skeleton.d.ts +15 -0
- package/dist/components/util/skeleton.d.ts.map +1 -0
- package/dist/components/util/sliding-indicator.d.ts +25 -0
- package/dist/components/util/sliding-indicator.d.ts.map +1 -0
- package/dist/components/util/temporal-field.d.ts +89 -0
- package/dist/components/util/temporal-field.d.ts.map +1 -0
- package/dist/components/util/toaster.d.ts +40 -0
- package/dist/components/util/toaster.d.ts.map +1 -1
- package/dist/components/util/tone-surface.d.ts +34 -0
- package/dist/components/util/tone-surface.d.ts.map +1 -0
- package/dist/components/util/top-navbar.d.ts +26 -2
- package/dist/components/util/top-navbar.d.ts.map +1 -1
- package/dist/components/util/zabi-strings.d.ts +9 -0
- package/dist/components/util/zabi-strings.d.ts.map +1 -1
- package/dist/create-theme/cli.js +6 -2
- package/dist/create-theme/index.d.ts +17 -3
- package/dist/create-theme/index.js +252 -35
- package/dist/create-theme/lib/resolve.js +97 -0
- package/dist/create-theme/theme-data.js +1618 -90
- package/dist/index.d.ts +27 -3
- package/dist/index.js +18 -0
- package/dist/map.d.ts +10 -0
- package/dist/map.js +9 -0
- package/dist/molecules/Alert.svelte +31 -12
- package/dist/molecules/Alert.svelte.d.ts +12 -1
- package/dist/molecules/AppBar.svelte +287 -28
- package/dist/molecules/AppBar.svelte.d.ts +62 -0
- package/dist/molecules/AvatarGroup.svelte +1 -1
- package/dist/molecules/BarChart.svelte +381 -0
- package/dist/molecules/BarChart.svelte.d.ts +45 -0
- package/dist/molecules/BottomSheet.svelte +88 -20
- package/dist/molecules/BottomSheet.svelte.d.ts +17 -1
- package/dist/molecules/BottomTabBar.svelte +134 -28
- package/dist/molecules/BottomTabBar.svelte.d.ts +36 -2
- package/dist/molecules/Calendar.svelte.d.ts +1 -1
- package/dist/molecules/ChipGroup.svelte +325 -0
- package/dist/molecules/ChipGroup.svelte.d.ts +41 -0
- package/dist/molecules/Collapsible.svelte +158 -54
- package/dist/molecules/Collapsible.svelte.d.ts +23 -6
- package/dist/molecules/CollapsibleGroup.svelte +16 -6
- package/dist/molecules/ConfirmDialog.svelte +5 -1
- package/dist/molecules/ConfirmDialog.svelte.d.ts +3 -0
- package/dist/molecules/Drawer.svelte +33 -12
- package/dist/molecules/Dropdown.svelte +9 -1
- package/dist/molecules/Dropdown.svelte.d.ts +2 -0
- package/dist/molecules/DropdownItem.svelte +1 -1
- package/dist/molecules/ImageUpload.svelte +230 -120
- package/dist/molecules/ImageUpload.svelte.d.ts +38 -3
- package/dist/molecules/LineChart.svelte +459 -0
- package/dist/molecules/LineChart.svelte.d.ts +53 -0
- package/dist/molecules/MapView.svelte +565 -0
- package/dist/molecules/MapView.svelte.d.ts +88 -0
- package/dist/molecules/Modal.svelte +11 -6
- package/dist/molecules/PhotoGrid.svelte +100 -10
- package/dist/molecules/PhotoGrid.svelte.d.ts +11 -0
- package/dist/molecules/PhotoViewer.svelte +121 -24
- package/dist/molecules/PhotoViewer.svelte.d.ts +18 -0
- package/dist/molecules/PieChart.svelte +421 -0
- package/dist/molecules/PieChart.svelte.d.ts +51 -0
- package/dist/molecules/RadioGroup.svelte +20 -4
- package/dist/molecules/RadioGroup.svelte.d.ts +5 -0
- package/dist/molecules/SegmentedControl.svelte +136 -3
- package/dist/molecules/SegmentedControl.svelte.d.ts +6 -0
- package/dist/molecules/SidebarFooter.svelte +1 -1
- package/dist/molecules/SidebarNavSection.svelte +1 -1
- package/dist/molecules/SortableList.svelte +1 -0
- package/dist/molecules/Stepper.svelte +21 -7
- package/dist/molecules/StickyActionBar.svelte +67 -8
- package/dist/molecules/StickyActionBar.svelte.d.ts +14 -1
- package/dist/molecules/Tabs.svelte +79 -2
- package/dist/molecules/Toaster.svelte +171 -7
- package/dist/molecules/Toaster.svelte.d.ts +60 -2
- package/dist/molecules/ToasterToast.svelte +292 -60
- package/dist/molecules/ToasterToast.svelte.d.ts +6 -0
- package/dist/molecules/UnsavedChangesBar.svelte +13 -2
- package/dist/molecules/index.d.ts +7 -2
- package/dist/molecules/index.js +4 -0
- package/dist/molecules/toast-store.d.ts +30 -0
- package/dist/molecules/toast-store.js +4 -1
- package/dist/organisms/AppNavigation.svelte +440 -0
- package/dist/organisms/AppNavigation.svelte.d.ts +72 -0
- package/dist/organisms/AppShell.svelte +539 -39
- package/dist/organisms/AppShell.svelte.d.ts +102 -6
- package/dist/organisms/AppShellHeaderPart.svelte +163 -0
- package/dist/organisms/AppShellHeaderPart.svelte.d.ts +57 -0
- package/dist/organisms/SidebarNavigation.svelte +23 -36
- package/dist/organisms/SidebarPanel.svelte +10 -14
- package/dist/organisms/SidebarPanel.svelte.d.ts +1 -0
- package/dist/organisms/SidebarShell.svelte +2 -3
- package/dist/organisms/TopNavbar.svelte +117 -28
- package/dist/organisms/TopNavbar.svelte.d.ts +22 -1
- package/dist/organisms/TopNavbarSheetContent.svelte +145 -0
- package/dist/organisms/TopNavbarSheetContent.svelte.d.ts +24 -0
- package/dist/organisms/index.d.ts +6 -1
- package/dist/organisms/index.js +4 -0
- package/dist/types/components.d.ts +17 -1
- package/dist/types/components.d.ts.map +1 -1
- package/dist/types/index.d.ts +497 -10
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/variants.ts +14 -3
- package/dist/util/app-shell.d.ts +55 -4
- package/dist/util/app-shell.js +32 -7
- package/dist/util/avatar.d.ts +3 -0
- package/dist/util/bottom-tab-bar.d.ts +6 -0
- package/dist/util/chart.d.ts +139 -0
- package/dist/util/chart.js +305 -0
- package/dist/util/chip.d.ts +65 -0
- package/dist/util/chip.js +58 -0
- package/dist/util/cn.js +28 -2
- package/dist/util/code-field.d.ts +62 -0
- package/dist/util/code-field.js +112 -0
- package/dist/util/collapsible.d.ts +6 -0
- package/dist/util/display-voice.d.ts +9 -0
- package/dist/util/display-voice.js +9 -0
- package/dist/util/field-control.d.ts +29 -0
- package/dist/util/field-control.js +32 -0
- package/dist/util/field.d.ts +6 -0
- package/dist/util/focus-utils.d.ts +2 -0
- package/dist/util/focus-utils.js +4 -0
- package/dist/util/map-view.d.ts +143 -0
- package/dist/util/map-view.js +121 -0
- package/dist/util/photo.d.ts +19 -0
- package/dist/util/photo.js +3 -0
- package/dist/util/progress.d.ts +18 -0
- package/dist/util/progress.js +17 -0
- package/dist/util/scroll-edge.d.ts +32 -0
- package/dist/util/scroll-edge.js +87 -0
- package/dist/util/sheet-drag.d.ts +25 -1
- package/dist/util/sheet-drag.js +71 -1
- package/dist/util/skeleton.d.ts +14 -0
- package/dist/util/skeleton.js +22 -0
- package/dist/util/sliding-indicator.d.ts +24 -0
- package/dist/util/sliding-indicator.js +44 -0
- package/dist/util/temporal-field.d.ts +88 -0
- package/dist/util/temporal-field.js +136 -0
- package/dist/util/toaster.d.ts +40 -0
- package/dist/util/toaster.js +55 -0
- package/dist/util/tone-surface.d.ts +33 -0
- package/dist/util/tone-surface.js +35 -0
- package/dist/util/top-navbar.d.ts +26 -2
- package/dist/util/top-navbar.js +23 -0
- package/dist/util/zabi-strings.d.ts +9 -0
- package/dist/zabi-components-colors.css +9 -3
- package/dist/zabi-components-theme-dark-only.css +474 -164
- package/dist/zabi-components-theme-dark.css +474 -164
- package/dist/zabi-components-theme-only.css +933 -99
- package/dist/zabi-components-theme.css +933 -99
- package/dist/zabi-components.css +2275 -321
- package/package.json +11 -1
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
toOklch,
|
|
47
47
|
toLab,
|
|
48
48
|
} from './lib/ramp-math.js';
|
|
49
|
-
import { resolveTokenColor } from './lib/resolve.js';
|
|
49
|
+
import { resolveTokenColor, resolveTokenPaint, compositeOver, dimBackdrop, backdropBrightness } from './lib/resolve.js';
|
|
50
50
|
|
|
51
51
|
/** The neutral ramp's 21 steps. */
|
|
52
52
|
export const BASE_STEPS = [
|
|
@@ -252,28 +252,28 @@ function triplet(hex) {
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
/**
|
|
255
|
-
* Light and dark declarations for the ink roles over a neutral ramp.
|
|
256
|
-
* author overrides
|
|
257
|
-
*
|
|
255
|
+
* Light and dark declarations for the ink roles over a neutral ramp. A role
|
|
256
|
+
* the author overrides in a mode (or in `both`) is left out for that mode, so
|
|
257
|
+
* their override is what it shows. The other mode keeps the ramp's ink: a role
|
|
258
|
+
* set in `light` alone is still the tinted mix in dark, not the library's grey.
|
|
258
259
|
*/
|
|
259
|
-
function inkTokens(ramp,
|
|
260
|
+
function inkTokens(ramp, modes) {
|
|
260
261
|
const light = {};
|
|
261
262
|
const dark = {};
|
|
263
|
+
const setIn = (mode, role) => role in modes[mode] || role in modes.both;
|
|
262
264
|
const mix = (step, percent) => `color-mix(in srgb, var(--zabi-base-${step}) ${percent}%, transparent)`;
|
|
263
265
|
for (const role of INK_ROLES) {
|
|
264
|
-
if (role in overrides) continue;
|
|
265
266
|
const lightPercent = alphaPercent(data.light[role]);
|
|
266
|
-
if (lightPercent !== null) light[role] = mix(900, lightPercent);
|
|
267
|
+
if (lightPercent !== null && !setIn('light', role)) light[role] = mix(900, lightPercent);
|
|
268
|
+
if (setIn('dark', role)) continue;
|
|
267
269
|
const darkPercent = alphaPercent(data.darkOnly[role]);
|
|
268
270
|
// A role the dark theme already points at a ramp step (the overlay edge)
|
|
269
271
|
// keeps that: the light value on `:root` would otherwise win there too.
|
|
270
272
|
if (darkPercent !== null) dark[role] = mix(50, darkPercent);
|
|
271
273
|
else if (data.darkOnly[role] !== undefined) dark[role] = data.darkOnly[role];
|
|
272
274
|
}
|
|
273
|
-
if (!('--shadow-color'
|
|
274
|
-
|
|
275
|
-
dark['--shadow-color'] = '0 0 0';
|
|
276
|
-
}
|
|
275
|
+
if (!setIn('light', '--shadow-color')) light['--shadow-color'] = triplet(ramp[900]);
|
|
276
|
+
if (!setIn('dark', '--shadow-color')) dark['--shadow-color'] = '0 0 0';
|
|
277
277
|
return { light, dark };
|
|
278
278
|
}
|
|
279
279
|
|
|
@@ -324,14 +324,37 @@ function themeMaps(tokens, darkTokens = {}) {
|
|
|
324
324
|
};
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
-
function checkPairs(tokens, darkTokens = {}, extraPairs = []) {
|
|
327
|
+
function checkPairs(tokens, darkTokens = {}, extraPairs = [], { gradients = true } = {}) {
|
|
328
328
|
const maps = themeMaps(tokens, darkTokens);
|
|
329
329
|
const warnings = [];
|
|
330
330
|
let checked = 0;
|
|
331
331
|
for (const mode of ['light', 'dark']) {
|
|
332
332
|
for (const pair of [...data.pairs, ...extraPairs]) {
|
|
333
|
-
|
|
334
|
-
|
|
333
|
+
// `only`: a pair about one mode (the dark field is a translucent step; light's is opaque).
|
|
334
|
+
if (pair.only && pair.only !== mode) continue;
|
|
335
|
+
// The control veils are settled last (settleControlGradients), so a
|
|
336
|
+
// choice made before then must not be steered by them.
|
|
337
|
+
if (!gradients && pair.gradient) continue;
|
|
338
|
+
const behind = pair.behind ? (typeof pair.behind === 'string' ? pair.behind : pair.behind[mode]) : null;
|
|
339
|
+
let bg = resolveTokenColor(maps[mode], pair.bg);
|
|
340
|
+
if (behind) {
|
|
341
|
+
// A material: its fill (an alpha paint) laid over what is behind it.
|
|
342
|
+
const paint = resolveTokenPaint(maps[mode], pair.bg);
|
|
343
|
+
const flat = resolveTokenColor(maps[mode], behind);
|
|
344
|
+
// A material's filter dims what is behind it (D137); other `behind` pairs are not filtered.
|
|
345
|
+
const backdrop = flat && pair.material ? dimBackdrop(flat, backdropBrightness(maps[mode])) : flat;
|
|
346
|
+
bg = paint && backdrop ? backdrop : null;
|
|
347
|
+
// `layers`: the same paint stacked, each laid over the last (nested translucent cards).
|
|
348
|
+
for (let layer = 0; layer < (pair.layers ?? 1) && bg; layer += 1) bg = compositeOver(paint, bg);
|
|
349
|
+
}
|
|
350
|
+
let fg = resolveTokenColor(maps[mode], pair.fg);
|
|
351
|
+
if (behind && pair.fgPaint) {
|
|
352
|
+
// `fgPaint`: the foreground is a fill too (a pressed field against the resting one), laid over the same backdrop.
|
|
353
|
+
const fgPaint = resolveTokenPaint(maps[mode], pair.fg);
|
|
354
|
+
let under = resolveTokenColor(maps[mode], behind);
|
|
355
|
+
for (let layer = 0; layer < (pair.layers ?? 1) && under && fgPaint; layer += 1) under = compositeOver(fgPaint, under);
|
|
356
|
+
fg = fgPaint ? under : null;
|
|
357
|
+
}
|
|
335
358
|
if (!bg || !fg) continue; // an alpha tint: not one flat colour
|
|
336
359
|
checked += 1;
|
|
337
360
|
const ratio = contrast(bg, fg);
|
|
@@ -347,7 +370,7 @@ function checkPairs(tokens, darkTokens = {}, extraPairs = []) {
|
|
|
347
370
|
required: pair.min,
|
|
348
371
|
foreground: { token: pair.fg, value: fg },
|
|
349
372
|
background: { token: pair.bg, value: bg },
|
|
350
|
-
message: `${mode} · ${pair.name}: ${fg} on ${bg} is ${ratio}:1, needs ${pair.min}:1 (${pair.fg} on ${pair.bg})`,
|
|
373
|
+
message: `${mode} · ${pair.name}: ${fg} on ${bg} is ${ratio}:1, needs ${pair.min}:1 (${pair.fg} on ${pair.bg}${behind ? ` over ${behind}` : ''})`,
|
|
351
374
|
});
|
|
352
375
|
}
|
|
353
376
|
}
|
|
@@ -370,14 +393,18 @@ const ON_ROLES = {
|
|
|
370
393
|
/**
|
|
371
394
|
* The label for a solid fill: white or the ramp's dark end, whichever reaches
|
|
372
395
|
* 4.5:1 on the fill, its hover and its active step. The library's own choice
|
|
373
|
-
* for the mode is tried first; if neither passes, the better one is kept and
|
|
396
|
+
* for the role and mode is tried first (white, except the accent in dark); if neither passes, the better one is kept and
|
|
374
397
|
* the pair check reports it.
|
|
375
398
|
*/
|
|
376
399
|
function chooseOn(name, mode, tokens, darkTokens = {}) {
|
|
377
400
|
const { role, fills } = ON_ROLES[name];
|
|
378
401
|
const knob = mode === 'dark' ? `--zabi-on-${name}-dark` : `--zabi-on-${name}`;
|
|
379
402
|
const darkEnd = `var(--zabi-${name}-950)`;
|
|
380
|
-
|
|
403
|
+
// White first for the brand in both modes: the library's dark primary is the
|
|
404
|
+
// same mid step as light. A pale dark primary (an app's own override, or
|
|
405
|
+
// darkPrimary: 'mirror') fails white and keeps the ramp's dark end. The
|
|
406
|
+
// accent's dark fill is the light step, so its dark end still goes first.
|
|
407
|
+
const candidates = mode === 'dark' && name === 'accent' ? [darkEnd, WHITE] : [WHITE, darkEnd];
|
|
381
408
|
let best = null;
|
|
382
409
|
for (const value of candidates) {
|
|
383
410
|
const map = themeMaps({ ...tokens, [knob]: value }, darkTokens)[mode];
|
|
@@ -393,6 +420,36 @@ function chooseOn(name, mode, tokens, darkTokens = {}) {
|
|
|
393
420
|
return [knob, best.value];
|
|
394
421
|
}
|
|
395
422
|
|
|
423
|
+
/* ---------- dark primary ---------- */
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* What `darkPrimary: 'mirror'` writes under the dark selectors: the 8.1 dark
|
|
427
|
+
* fills, which are the mirrored semantic steps (pale in dark), the danger
|
|
428
|
+
* aliases with the label they had, and the lighten-heavy control veil made for
|
|
429
|
+
* a pale fill. The label knob is chosen by chooseOn like any other.
|
|
430
|
+
*/
|
|
431
|
+
const MIRRORED_DARK = {
|
|
432
|
+
'--color-action-primary': 'var(--color-brand-600)',
|
|
433
|
+
'--color-action-primary-hover': 'var(--color-brand-700)',
|
|
434
|
+
'--color-action-primary-active': 'var(--color-brand-800)',
|
|
435
|
+
'--color-action-danger': 'var(--color-error-600)',
|
|
436
|
+
'--color-action-danger-hover': 'var(--color-error-700)',
|
|
437
|
+
'--color-action-danger-active': 'var(--color-error-800)',
|
|
438
|
+
'--color-action-danger-text': 'var(--color-error-50)',
|
|
439
|
+
'--gradient-control-top-strength': '22%',
|
|
440
|
+
'--gradient-control-bottom-strength': '6%',
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
function readDarkPrimary(value) {
|
|
444
|
+
if (value === undefined || value === null) return 'brand';
|
|
445
|
+
if (value !== 'brand' && value !== 'mirror') {
|
|
446
|
+
throw new TypeError(
|
|
447
|
+
`${TOOL}: darkPrimary must be "brand" or "mirror" (got ${typeof value === 'string' ? `"${value}"` : JSON.stringify(value)})`,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
return value;
|
|
451
|
+
}
|
|
452
|
+
|
|
396
453
|
/* ---------- pinning ---------- */
|
|
397
454
|
|
|
398
455
|
/**
|
|
@@ -508,10 +565,15 @@ const failing = (warnings, mode) => new Set(warnings.filter((w) => w.mode === mo
|
|
|
508
565
|
* 3:1, and no label makes that a button. So dark takes the colour only when nothing guarded fails with it,
|
|
509
566
|
* and otherwise keeps what it has without `pin`: the mirrored ramp step.
|
|
510
567
|
*/
|
|
511
|
-
function pinColour(name, colour, tokens, earlierDark, userOverrides, extraPairs, skip) {
|
|
568
|
+
function pinColour(name, colour, tokens, earlierDark, userOverrides, extraPairs, skip, darkPrimary = 'brand') {
|
|
512
569
|
const roles = PIN_ROLES[name];
|
|
513
570
|
const keep = (group) => Object.fromEntries(Object.entries(group).filter(([role]) => !skip.has(role)));
|
|
514
|
-
|
|
571
|
+
// What dark has for a role without the pin: what the file already says for
|
|
572
|
+
// dark (the mirrored fills under darkPrimary: 'mirror'), then the library.
|
|
573
|
+
const libraryDark = (role) => earlierDark[role] ?? data.darkOnly[role] ?? data.light[role];
|
|
574
|
+
// The brand's dark fill is the light recipe (the same steps, darker states)
|
|
575
|
+
// unless it is mirrored; the accent's dark fill is still the pale step.
|
|
576
|
+
const recipe = (mode) => (mode === 'dark' && name === 'brand' && darkPrimary === 'brand' ? 'light' : mode);
|
|
515
577
|
const light = {};
|
|
516
578
|
const dark = {};
|
|
517
579
|
const knobs = {};
|
|
@@ -522,7 +584,7 @@ function pinColour(name, colour, tokens, earlierDark, userOverrides, extraPairs,
|
|
|
522
584
|
const root = { ...tokens, ...knobs, ...lightRoles, ...userOverrides };
|
|
523
585
|
const darkRule = { ...earlierDark, ...darkRoles };
|
|
524
586
|
const [knob, value] = chooseOn(name, mode, root, darkRule);
|
|
525
|
-
const result = checkPairs({ ...root, [knob]: value }, darkRule, extraPairs);
|
|
587
|
+
const result = checkPairs({ ...root, [knob]: value }, darkRule, extraPairs, { gradients: false });
|
|
526
588
|
return { knob, value, failures: failing(result.warnings, mode) };
|
|
527
589
|
};
|
|
528
590
|
const noWorse = (after, before) => [...after].every((pair) => before.has(pair));
|
|
@@ -535,8 +597,8 @@ function pinColour(name, colour, tokens, earlierDark, userOverrides, extraPairs,
|
|
|
535
597
|
*/
|
|
536
598
|
const bestFill = (mode, tryWith) => {
|
|
537
599
|
let best = null;
|
|
538
|
-
for (const direction of SHADE_DIRECTIONS[mode]) {
|
|
539
|
-
const states = shades(colour, mode, direction);
|
|
600
|
+
for (const direction of SHADE_DIRECTIONS[recipe(mode)]) {
|
|
601
|
+
const states = shades(colour, recipe(mode), direction);
|
|
540
602
|
if (!states) continue;
|
|
541
603
|
const result = tryWith(keep(roles.fill(colour.hex, states)));
|
|
542
604
|
if (!best || result.failures.size < best.result.failures.size) best = { direction, states, result };
|
|
@@ -600,13 +662,151 @@ function pinColour(name, colour, tokens, earlierDark, userOverrides, extraPairs,
|
|
|
600
662
|
return { light, dark: { ...restated(), ...dark }, knobs, report };
|
|
601
663
|
}
|
|
602
664
|
|
|
665
|
+
/* ---------- links that follow the primary ---------- */
|
|
666
|
+
|
|
667
|
+
/** What the app's overrides say a role is in `mode`, or undefined. */
|
|
668
|
+
const overrideIn = (modes, mode, role) => modes[mode][role] ?? modes.both[role];
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Sets roles for one mode, answering a light value with the library's own
|
|
672
|
+
* dark one so that dark does not move (as overrideMaps does for `light`).
|
|
673
|
+
*/
|
|
674
|
+
function setRoles(tokens, darkTokens, mode, roles) {
|
|
675
|
+
if (mode === 'light') {
|
|
676
|
+
const dark = { ...darkTokens };
|
|
677
|
+
for (const role of Object.keys(roles)) if (!(role in dark)) dark[role] = data.darkOnly[role] ?? data.light[role];
|
|
678
|
+
return { tokens: { ...tokens, ...roles }, darkTokens: dark };
|
|
679
|
+
}
|
|
680
|
+
return { tokens, darkTokens: { ...darkTokens, ...roles } };
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** The pairs failing in `mode` with these tokens. */
|
|
684
|
+
const failingIn = (tokens, darkTokens, mode) => failing(checkPairs(tokens, darkTokens).warnings, mode);
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* `--color-link` follows an overridden primary, as a pinned colour's link
|
|
688
|
+
* does (PIN_ROLES): the library's link is the brand's step 700 because step
|
|
689
|
+
* 600, its primary, does not hold 4.5:1 as text on tints. An app that sets
|
|
690
|
+
* its own primary (a deeper step, say) otherwise has links beside buttons in
|
|
691
|
+
* a different blue. In each mode where the app sets `--color-action-primary`
|
|
692
|
+
* and not `--color-link`, link is that primary and, if the app set a primary
|
|
693
|
+
* hover too, link-hover is that (otherwise the ramp's link-hover stays: the
|
|
694
|
+
* library's hover is a step the app's primary does not use), but only if no
|
|
695
|
+
* guarded pair fails that did not fail before; otherwise the ramp's link
|
|
696
|
+
* stays in that mode.
|
|
697
|
+
* An explicit `--color-link` always wins.
|
|
698
|
+
*/
|
|
699
|
+
function followPrimaryWithLink(tokens, darkTokens, modes) {
|
|
700
|
+
for (const mode of ['light', 'dark']) {
|
|
701
|
+
const primary = overrideIn(modes, mode, '--color-action-primary');
|
|
702
|
+
if (primary === undefined || overrideIn(modes, mode, '--color-link') !== undefined) continue;
|
|
703
|
+
const hover = overrideIn(modes, mode, '--color-action-primary-hover');
|
|
704
|
+
const before = failingIn(tokens, darkTokens, mode);
|
|
705
|
+
for (const roles of [
|
|
706
|
+
...(hover === undefined ? [] : [{ '--color-link': primary, '--color-link-hover': hover }]),
|
|
707
|
+
{ '--color-link': primary },
|
|
708
|
+
]) {
|
|
709
|
+
const next = setRoles(tokens, darkTokens, mode, roles);
|
|
710
|
+
const after = failingIn(next.tokens, next.darkTokens, mode);
|
|
711
|
+
if ([...after].every((pair) => before.has(pair))) {
|
|
712
|
+
({ tokens, darkTokens } = next);
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return { tokens, darkTokens };
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* The edge of the Progress track (`--color-progress-track-border`) is the field
|
|
722
|
+
* edge by default, because the default track is the field fill and does not
|
|
723
|
+
* reach 3:1 against the page or a card (WCAG 1.4.11). An app that overrides
|
|
724
|
+
* the track so that it does reach 3:1 against both, in a mode, does not need
|
|
725
|
+
* an edge there: it is written as `transparent` for that mode, as the link
|
|
726
|
+
* follows an overridden primary. An edge the app sets itself, in either
|
|
727
|
+
* mode or both, is its decision, and is never moved (a `transparent` of its
|
|
728
|
+
* own draws no warning: a pair with no flat colour is skipped).
|
|
729
|
+
*/
|
|
730
|
+
const PROGRESS_TRACK = '--color-progress-track';
|
|
731
|
+
const PROGRESS_TRACK_EDGE = '--color-progress-track-border';
|
|
732
|
+
const MIN_TRACK_AGAINST_SURFACE = 3;
|
|
733
|
+
|
|
734
|
+
function settleProgressTrackEdge(tokens, darkTokens, modes) {
|
|
735
|
+
for (const mode of ['light', 'dark']) {
|
|
736
|
+
if (overrideIn(modes, mode, PROGRESS_TRACK) === undefined) continue;
|
|
737
|
+
if (overrideIn(modes, mode, PROGRESS_TRACK_EDGE) !== undefined) continue;
|
|
738
|
+
const maps = themeMaps(tokens, darkTokens)[mode];
|
|
739
|
+
const track = resolveTokenColor(maps, PROGRESS_TRACK);
|
|
740
|
+
if (!track) continue; // an alpha tint: not one flat colour
|
|
741
|
+
const surfaces = ['--color-surface-base', '--color-surface-raised'].map((role) => resolveTokenColor(maps, role));
|
|
742
|
+
if (surfaces.some((surface) => !surface || contrast(track, surface) < MIN_TRACK_AGAINST_SURFACE)) continue;
|
|
743
|
+
({ tokens, darkTokens } = setRoles(tokens, darkTokens, mode, { [PROGRESS_TRACK_EDGE]: 'transparent' }));
|
|
744
|
+
}
|
|
745
|
+
return { tokens, darkTokens };
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/* ---------- control gradients ---------- */
|
|
749
|
+
|
|
750
|
+
/** The strongest veil tried: the library's own cap. */
|
|
751
|
+
const MAX_VEIL = 14;
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* The veil of a solid control is a light stop on top and a dark one at the
|
|
755
|
+
* bottom, over the control's own fill. The library's light default darkens
|
|
756
|
+
* only (top 0, bottom 14%): a darker fill can only help a light label. An app
|
|
757
|
+
* whose fill takes a DARK label (a light accent, an amber) loses contrast to
|
|
758
|
+
* that darkening, so for each veil and each mode: if every pair passes, nothing
|
|
759
|
+
* is written; if not, the veil is flipped to lighten-only (the top at the
|
|
760
|
+
* largest whole percent up to 14 that passes, the bottom 0), or the other way
|
|
761
|
+
* if the label is the light one; if neither direction passes, both are 0%,
|
|
762
|
+
* which is no veil. A strength the app set itself is never moved: the pair
|
|
763
|
+
* check reports it. Primary and danger share one veil, so they flip together.
|
|
764
|
+
*/
|
|
765
|
+
function settleControlGradients(tokens, darkTokens, modes) {
|
|
766
|
+
const veils = new Map();
|
|
767
|
+
for (const control of data.controlGradients) {
|
|
768
|
+
const key = control.strengths.join('|');
|
|
769
|
+
if (!veils.has(key)) veils.set(key, { strengths: control.strengths, controls: [] });
|
|
770
|
+
veils.get(key).controls.push(control);
|
|
771
|
+
}
|
|
772
|
+
for (const mode of ['light', 'dark']) {
|
|
773
|
+
for (const { strengths, controls } of veils.values()) {
|
|
774
|
+
if (strengths.some((role) => overrideIn(modes, mode, role) !== undefined)) continue;
|
|
775
|
+
const failed = (t, d) =>
|
|
776
|
+
[...failingIn(t, d, mode)].filter((pair) => controls.some((control) => pair.startsWith(`${control.name} gradient `)));
|
|
777
|
+
if (failed(tokens, darkTokens).length === 0) continue;
|
|
778
|
+
const [top, bottom] = strengths;
|
|
779
|
+
const maps = themeMaps(tokens, darkTokens)[mode];
|
|
780
|
+
const labelIsDark = controls.some((control) => {
|
|
781
|
+
const label = resolveTokenColor(maps, control.on);
|
|
782
|
+
const fill = resolveTokenColor(maps, control.fills[0][1]);
|
|
783
|
+
return label && fill && luminance(label) < luminance(fill);
|
|
784
|
+
});
|
|
785
|
+
const directions = labelIsDark ? ['lighten', 'darken'] : ['darken', 'lighten'];
|
|
786
|
+
let chosen = { [top]: '0%', [bottom]: '0%' };
|
|
787
|
+
search: for (const direction of directions) {
|
|
788
|
+
for (let percent = MAX_VEIL; percent >= 1; percent -= 1) {
|
|
789
|
+
const roles = direction === 'lighten' ? { [top]: `${percent}%`, [bottom]: '0%' } : { [top]: '0%', [bottom]: `${percent}%` };
|
|
790
|
+
const next = setRoles(tokens, darkTokens, mode, roles);
|
|
791
|
+
if (failed(next.tokens, next.darkTokens).length === 0) {
|
|
792
|
+
chosen = roles;
|
|
793
|
+
break search;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
({ tokens, darkTokens } = setRoles(tokens, darkTokens, mode, chosen));
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return { tokens, darkTokens };
|
|
801
|
+
}
|
|
802
|
+
|
|
603
803
|
/* ---------- output ---------- */
|
|
604
804
|
|
|
605
805
|
function rampTokens(name, ramp, steps) {
|
|
606
806
|
return Object.fromEntries(steps.map((step) => [`--zabi-${name}-${step}`, ramp[step]]));
|
|
607
807
|
}
|
|
608
808
|
|
|
609
|
-
function render({ inputs, closest, tokens, darkTokens, pinned, neutralChroma, contrastWarnings, checked }) {
|
|
809
|
+
function render({ inputs, closest, tokens, darkTokens, pinned, neutralChroma, darkPrimary, contrastWarnings, checked }) {
|
|
610
810
|
const describeInput = (name) =>
|
|
611
811
|
inputs[name]
|
|
612
812
|
? `${inputs[name]} closest step ${closest[name].step} (${closest[name].hex}, ` +
|
|
@@ -621,6 +821,12 @@ function render({ inputs, closest, tokens, darkTokens, pinned, neutralChroma, co
|
|
|
621
821
|
` * brand ${describeInput('brand')}`,
|
|
622
822
|
` * accent ${describeInput('accent')}`,
|
|
623
823
|
` * neutral ${describeInput('neutral')}`,
|
|
824
|
+
...(darkPrimary === 'mirror'
|
|
825
|
+
? [
|
|
826
|
+
' * dark primary: mirror. The primary and danger fills use the mirrored (pale) steps in',
|
|
827
|
+
' * dark, as in 8.1, with the label and control veil chosen for them.',
|
|
828
|
+
]
|
|
829
|
+
: []),
|
|
624
830
|
...(neutralChroma === undefined
|
|
625
831
|
? []
|
|
626
832
|
: [
|
|
@@ -679,8 +885,8 @@ function pinnedNotes(pinned) {
|
|
|
679
885
|
if (report.dark.followed.length) lines.push(` * dark, also this colour: ${list(report.dark.followed)}`);
|
|
680
886
|
if (report.dark.onRamp.length) lines.push(` * dark, left on the ramp: ${list(report.dark.onRamp)}`);
|
|
681
887
|
} else {
|
|
682
|
-
lines.push(' * dark: NOT pinned. The colour fails there, so dark keeps the
|
|
683
|
-
lines.push(' *
|
|
888
|
+
lines.push(' * dark: NOT pinned. The colour fails there, so dark keeps the ramp step it has');
|
|
889
|
+
lines.push(' * without pin. With the colour, these would fail:');
|
|
684
890
|
for (const pair of report.dark.failed) lines.push(` * - ${pair}`);
|
|
685
891
|
lines.push(' * To force a dark value all the same, write the role in your own dark rule after this file.');
|
|
686
892
|
}
|
|
@@ -689,7 +895,7 @@ function pinnedNotes(pinned) {
|
|
|
689
895
|
}
|
|
690
896
|
|
|
691
897
|
/**
|
|
692
|
-
* @param {{ brand: string, accent?: string, neutral?: string, neutralChroma?: number, overrides?: Record<string, string> | { light?: Record<string, string>, dark?: Record<string, string>, both?: Record<string, string> } & Record<string, unknown>, pin?: boolean | { brand?: boolean, accent?: boolean } }} options
|
|
898
|
+
* @param {{ brand: string, accent?: string, neutral?: string, neutralChroma?: number, overrides?: Record<string, string> | { light?: Record<string, string>, dark?: Record<string, string>, both?: Record<string, string> } & Record<string, unknown>, pin?: boolean | { brand?: boolean, accent?: boolean }, darkPrimary?: 'brand' | 'mirror' }} options
|
|
693
899
|
*/
|
|
694
900
|
export function createTheme(options) {
|
|
695
901
|
if (options === null || typeof options !== 'object') {
|
|
@@ -699,10 +905,6 @@ export function createTheme(options) {
|
|
|
699
905
|
throw new TypeError(`${TOOL}: brand is required, for example { brand: "#0026EA" }`);
|
|
700
906
|
}
|
|
701
907
|
const modes = readOverrides(options.overrides ?? {});
|
|
702
|
-
// Roles the app sets in any mode are left to it: no ink role or pin replaces them.
|
|
703
|
-
const overridden = {};
|
|
704
|
-
for (const map of [modes.both, modes.light, modes.dark]) Object.assign(overridden, map);
|
|
705
|
-
|
|
706
908
|
const inputs = { brand: parseHex(options.brand, 'brand') };
|
|
707
909
|
if (options.accent !== undefined && options.accent !== null) inputs.accent = parseHex(options.accent, 'accent');
|
|
708
910
|
if (options.neutral !== undefined && options.neutral !== null) inputs.neutral = parseHex(options.neutral, 'neutral');
|
|
@@ -721,6 +923,7 @@ export function createTheme(options) {
|
|
|
721
923
|
neutralChroma = value;
|
|
722
924
|
}
|
|
723
925
|
|
|
926
|
+
const darkPrimary = readDarkPrimary(options.darkPrimary);
|
|
724
927
|
const pin = readPin(options.pin);
|
|
725
928
|
if (pin.accent && !inputs.accent) throw new TypeError(`${TOOL}: pin.accent needs an accent colour`);
|
|
726
929
|
const pinning = pin.brand || pin.accent;
|
|
@@ -747,7 +950,7 @@ export function createTheme(options) {
|
|
|
747
950
|
message:
|
|
748
951
|
`${name} ${colour.hex} is not in the ramp built from it: the nearest step is ` +
|
|
749
952
|
`${closest[name].step} (${closest[name].hex}, ΔE ${closest[name].deltaE.toFixed(1)}). ` +
|
|
750
|
-
'The ramp keeps its hue on the library\'s lightness curve; fills use step 600 in light and 400 in dark.',
|
|
953
|
+
'The ramp keeps its hue on the library\'s lightness curve; fills use step 600 in light and in dark (400 in dark with darkPrimary: "mirror").',
|
|
751
954
|
});
|
|
752
955
|
}
|
|
753
956
|
tokens = { ...tokens, ...rampTokens(name, ramp, RAMP_STEPS) };
|
|
@@ -766,12 +969,21 @@ export function createTheme(options) {
|
|
|
766
969
|
tokens = { ...tokens, ...rampTokens('base', ramp, BASE_STEPS) };
|
|
767
970
|
if (neutralChroma !== undefined) {
|
|
768
971
|
// Opt-in: without it these roles are the library's grey tints, as before.
|
|
769
|
-
const ink = inkTokens(ramp,
|
|
972
|
+
const ink = inkTokens(ramp, modes);
|
|
770
973
|
tokens = { ...tokens, ...ink.light };
|
|
771
974
|
darkTokens = ink.dark;
|
|
772
975
|
}
|
|
773
976
|
}
|
|
774
977
|
|
|
978
|
+
// darkPrimary: 'mirror' writes the 8.1 dark fills under the dark selectors,
|
|
979
|
+
// except for a role the app sets itself in dark or in both modes.
|
|
980
|
+
if (darkPrimary === 'mirror') {
|
|
981
|
+
for (const [role, value] of Object.entries(MIRRORED_DARK)) {
|
|
982
|
+
if (role in modes.dark || role in modes.both) continue;
|
|
983
|
+
darkTokens = { ...darkTokens, [role]: value };
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
775
987
|
// Overrides take part in choosing the labels, and are written last. Each
|
|
776
988
|
// mode sees its own values.
|
|
777
989
|
for (const name of ['brand', 'accent']) {
|
|
@@ -790,7 +1002,7 @@ export function createTheme(options) {
|
|
|
790
1002
|
for (const name of ['brand', 'accent']) {
|
|
791
1003
|
if (!pin[name]) continue;
|
|
792
1004
|
const own = overrideMaps(modes, tokens, darkTokens);
|
|
793
|
-
const result = pinColour(name, described[name], tokens, { ...darkTokens, ...own.dark }, own.root, extraPairs, own.skip);
|
|
1005
|
+
const result = pinColour(name, described[name], tokens, { ...darkTokens, ...own.dark }, own.root, extraPairs, own.skip, darkPrimary);
|
|
794
1006
|
tokens = { ...tokens, ...result.knobs, ...result.light };
|
|
795
1007
|
darkTokens = { ...darkTokens, ...result.dark };
|
|
796
1008
|
pinned[name] = result.report;
|
|
@@ -799,11 +1011,16 @@ export function createTheme(options) {
|
|
|
799
1011
|
tokens = { ...tokens, ...own.root };
|
|
800
1012
|
darkTokens = { ...darkTokens, ...own.dark };
|
|
801
1013
|
|
|
1014
|
+
// What the generator settles once the app's own values are in place.
|
|
1015
|
+
if (!pin.brand) ({ tokens, darkTokens } = followPrimaryWithLink(tokens, darkTokens, modes));
|
|
1016
|
+
({ tokens, darkTokens } = settleProgressTrackEdge(tokens, darkTokens, modes));
|
|
1017
|
+
({ tokens, darkTokens } = settleControlGradients(tokens, darkTokens, modes));
|
|
1018
|
+
|
|
802
1019
|
const { warnings: contrastWarnings, checked } = checkPairs(tokens, darkTokens, extraPairs);
|
|
803
1020
|
warnings.push(...contrastWarnings);
|
|
804
1021
|
|
|
805
|
-
const css = render({ inputs, closest, tokens, darkTokens, pinned: pinning ? pinned : null, neutralChroma, contrastWarnings, checked });
|
|
806
|
-
const hasDark = pinning || neutralChroma !== undefined || Object.keys(
|
|
1022
|
+
const css = render({ inputs, closest, tokens, darkTokens, pinned: pinning ? pinned : null, neutralChroma, darkPrimary, contrastWarnings, checked });
|
|
1023
|
+
const hasDark = pinning || neutralChroma !== undefined || Object.keys(darkTokens).length > 0;
|
|
807
1024
|
return {
|
|
808
1025
|
css,
|
|
809
1026
|
tokens,
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* Supported, because it is all the stylesheet uses for a flat colour:
|
|
11
11
|
* - `var(--token)` and `var(--token, fallback)`, anywhere in a value
|
|
12
12
|
* - `color-mix(in srgb, <opaque> p%, <opaque>)` (either or both percentages)
|
|
13
|
+
* - `color-mix(in srgb, <colour> p%, transparent)` and `rgba()`, as a colour
|
|
14
|
+
* with an alpha (resolveTokenPaint), for the materials' fills
|
|
13
15
|
* Anything else (`rgba()`, a mix with `transparent`) is returned as written,
|
|
14
16
|
* for the caller to composite over a surface or to skip.
|
|
15
17
|
*/
|
|
@@ -139,3 +141,98 @@ export function resolveTokenColor(map, token) {
|
|
|
139
141
|
const value = resolveTokenValue(map, token);
|
|
140
142
|
return value && HEX.test(value) ? value.toLowerCase() : null;
|
|
141
143
|
}
|
|
144
|
+
|
|
145
|
+
/* ---------- colours with an alpha ---------- */
|
|
146
|
+
|
|
147
|
+
/** `#rrggbb` + alpha, or null. `transparent` is alpha 0. */
|
|
148
|
+
function parsePaint(text) {
|
|
149
|
+
const value = text.trim();
|
|
150
|
+
if (value === 'transparent') return { hex: '#000000', alpha: 0 };
|
|
151
|
+
if (HEX.test(value)) return { hex: value.toLowerCase().length === 4 ? toHex(parseHex(value)) : value.toLowerCase(), alpha: 1 };
|
|
152
|
+
const rgba = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:[\s,/]+([\d.]+%?))?\s*\)$/.exec(value);
|
|
153
|
+
if (rgba) {
|
|
154
|
+
const a = rgba[4] === undefined ? 1 : rgba[4].endsWith('%') ? Number.parseFloat(rgba[4]) / 100 : Number(rgba[4]);
|
|
155
|
+
return { hex: toHex([rgba[1], rgba[2], rgba[3]].map(Number)), alpha: a };
|
|
156
|
+
}
|
|
157
|
+
if (value.startsWith('color-mix(')) return mixPaint(value);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* `color-mix(in srgb, A p%, B q%)` where either side may be `transparent` or
|
|
163
|
+
* carry an alpha. Premultiplied, as the browser mixes; a sum under 100% leaves
|
|
164
|
+
* the result that much more transparent. Null when it cannot be reduced.
|
|
165
|
+
*/
|
|
166
|
+
function mixPaint(value) {
|
|
167
|
+
const match = /^color-mix\(([\s\S]*)\)$/.exec(value.trim());
|
|
168
|
+
if (!match) return null;
|
|
169
|
+
const [space, first, second] = splitTopLevel(match[1]);
|
|
170
|
+
if (!space || !first || !second || space.replace(/\s+/g, ' ') !== 'in srgb') return null;
|
|
171
|
+
const operand = (text) => {
|
|
172
|
+
const t = text.trim();
|
|
173
|
+
let percent = null;
|
|
174
|
+
let colourText = t;
|
|
175
|
+
const trailing = /^([\s\S]*\S)\s+(-?[\d.]+)%$/.exec(t);
|
|
176
|
+
const leading = /^(-?[\d.]+)%\s+([\s\S]*)$/.exec(t);
|
|
177
|
+
if (trailing) [, colourText, percent] = trailing;
|
|
178
|
+
else if (leading) [, percent, colourText] = leading;
|
|
179
|
+
const paint = parsePaint(colourText);
|
|
180
|
+
return paint ? { paint, percent: percent === null ? null : Number(percent) } : null;
|
|
181
|
+
};
|
|
182
|
+
const a = operand(first);
|
|
183
|
+
const b = operand(second);
|
|
184
|
+
if (!a || !b) return null;
|
|
185
|
+
let pa = a.percent;
|
|
186
|
+
let pb = b.percent;
|
|
187
|
+
if (pa === null && pb === null) { pa = 50; pb = 50; }
|
|
188
|
+
else if (pa === null) pa = 100 - pb;
|
|
189
|
+
else if (pb === null) pb = 100 - pa;
|
|
190
|
+
const total = pa + pb;
|
|
191
|
+
if (total <= 0) return null;
|
|
192
|
+
const wa = (pa / total) * a.paint.alpha;
|
|
193
|
+
const wb = (pb / total) * b.paint.alpha;
|
|
194
|
+
const alpha = wa + wb;
|
|
195
|
+
if (alpha === 0) return { hex: '#000000', alpha: 0 };
|
|
196
|
+
const ca = parseHex(a.paint.hex);
|
|
197
|
+
const cb = parseHex(b.paint.hex);
|
|
198
|
+
return {
|
|
199
|
+
hex: toHex(ca.map((channel, i) => (channel * wa + cb[i] * wb) / alpha)),
|
|
200
|
+
alpha: alpha * Math.min(1, total / 100),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* What a token paints, as `{ hex, alpha }`: a flat colour (alpha 1), a
|
|
206
|
+
* `color-mix()` with `transparent`, or an `rgba()` tint. Null when the token
|
|
207
|
+
* is not declared or is not one of those.
|
|
208
|
+
*/
|
|
209
|
+
export function resolveTokenPaint(map, token) {
|
|
210
|
+
if (map[token] === undefined) return null;
|
|
211
|
+
const substituted = substituteVars(map, map[token], new Set([token]));
|
|
212
|
+
return substituted === null ? null : parsePaint(substituted);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** The colour `paint` makes laid source-over on the opaque `backdropHex`, as hex. */
|
|
216
|
+
export function compositeOver(paint, backdropHex) {
|
|
217
|
+
const under = parseHex(backdropHex);
|
|
218
|
+
const over = parseHex(paint.hex);
|
|
219
|
+
return toHex(over.map((channel, i) => channel * paint.alpha + under[i] * (1 - paint.alpha)));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The backdrop a material's filter hands to its fill: `brightness()` multiplies
|
|
224
|
+
* the sRGB-encoded channels, so each channel of `backdropHex` is scaled by
|
|
225
|
+
* `factor` (a number; 1 is the identity). Material pairs only.
|
|
226
|
+
*/
|
|
227
|
+
export function dimBackdrop(backdropHex, factor) {
|
|
228
|
+
const f = Number(factor);
|
|
229
|
+
if (!Number.isFinite(f) || f === 1) return backdropHex;
|
|
230
|
+
return toHex(parseHex(backdropHex).map((channel) => channel * f));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The mode's `--material-backdrop-brightness` as a number; 1 when it is not declared or not a number. */
|
|
234
|
+
export function backdropBrightness(map) {
|
|
235
|
+
const value = resolveTokenValue(map, '--material-backdrop-brightness');
|
|
236
|
+
const n = value === null ? NaN : Number(value);
|
|
237
|
+
return Number.isFinite(n) ? n : 1;
|
|
238
|
+
}
|