@halftone-ds/tokens 1.1.0 → 1.3.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/README.md +84 -4
- package/cli.mjs +33 -14
- package/lib/apca.mjs +55 -0
- package/lib/color.mjs +27 -7
- package/lib/contrast.mjs +56 -1
- package/lib/cvd.mjs +97 -0
- package/lib/resolve.mjs +100 -12
- package/lib/system/generate.mjs +212 -0
- package/lib/system/index.d.mts +168 -0
- package/lib/system/index.mjs +33 -0
- package/lib/system/migrate.mjs +38 -0
- package/lib/system/schema.mjs +511 -0
- package/lib/theme/apply.mjs +191 -16
- package/lib/theme/codec.mjs +38 -23
- package/lib/theme/components.mjs +471 -0
- package/lib/theme/extras.mjs +105 -0
- package/lib/theme/fix.mjs +63 -10
- package/lib/theme/fonts.mjs +16 -1
- package/lib/theme/generate.mjs +114 -13
- package/lib/theme/index.d.mts +166 -2
- package/lib/theme/index.mjs +33 -3
- package/lib/theme/presets.mjs +19 -7
- package/lib/theme/ramp.mjs +55 -15
- package/lib/theme/recipe.mjs +107 -0
- package/lib/theme/semantic.mjs +43 -6
- package/lib/theme/shadcn.mjs +265 -0
- package/lib/theme/vars.mjs +81 -3
- package/lib/theme-cli.mjs +179 -17
- package/lib/usage.mjs +136 -0
- package/package.json +5 -1
- package/targets/design-md.mjs +64 -17
- package/targets/figma.mjs +1 -1
- package/targets/fonts.mjs +5 -5
- package/targets/llms.mjs +9 -4
- package/targets/nativewind3.mjs +77 -12
- package/targets/registry-theme.mjs +3 -3
- package/targets/shadcn-web.mjs +106 -6
- package/targets/ts-theme.mjs +42 -0
package/lib/theme/fix.mjs
CHANGED
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
*
|
|
11
11
|
* It checks the STRICT set (tones on muted/accent too). A moved token becomes a literal hex for that
|
|
12
12
|
* mode, and every move is reported as { token, mode, from, to } so the picker can show it.
|
|
13
|
+
*
|
|
14
|
+
* A system's hand-made choices are never moved; each is a { light: Set, dark: Set } of token names:
|
|
15
|
+
* locked a role mapped by hand: the fixer changes nothing about it (no label flip, no nudge)
|
|
16
|
+
* pinned a role on an edited step: as a label it may still flip between the neutrals, but its colour is
|
|
17
|
+
* never nudged off the step
|
|
18
|
+
* The fixer still flips a generated label on a fixed fill, and moves generated ink off a fixed surface. It never
|
|
19
|
+
* moves a fill to suit a hand-picked label (that would move every surface the fill is). A pairing it leaves
|
|
20
|
+
* failing is reported in `warnings` as { mode, label, fg, bg, ratio, threshold, locked }.
|
|
13
21
|
*/
|
|
14
22
|
import { contrast, hexToOklchParts } from '../color.mjs';
|
|
15
23
|
import { contrastPairs } from '../contrast.mjs';
|
|
@@ -37,9 +45,10 @@ function nudge(hex, dir, ok) {
|
|
|
37
45
|
* semantic: { name: { light, dark } } of references or hex; lookup: (value) → hex.
|
|
38
46
|
* Returns { semantic, adjustments } — a new map, the input is not touched.
|
|
39
47
|
*/
|
|
40
|
-
export function fixContrast(semantic, lookup) {
|
|
48
|
+
export function fixContrast(semantic, lookup, { locked, pinned } = {}) {
|
|
41
49
|
const out = Object.fromEntries(Object.entries(semantic).map(([k, v]) => [k, { ...v }]));
|
|
42
50
|
const adjustments = [];
|
|
51
|
+
const warnings = [];
|
|
43
52
|
|
|
44
53
|
for (const mode of ['light', 'dark']) {
|
|
45
54
|
const original = Object.fromEntries(Object.entries(out).map(([k, v]) => [k, lookup(v[mode])]));
|
|
@@ -48,39 +57,83 @@ export function fixContrast(semantic, lookup) {
|
|
|
48
57
|
const set = (name, value) => {
|
|
49
58
|
out[name][mode] = value;
|
|
50
59
|
};
|
|
60
|
+
const lockedHere = locked?.[mode] ?? new Set();
|
|
61
|
+
const pinnedHere = pinned?.[mode] ?? new Set();
|
|
62
|
+
const isLocked = (name) => lockedHere.has(name);
|
|
63
|
+
// Its colour stays: locked, or on an edited step.
|
|
64
|
+
const isFixed = (name) => lockedHere.has(name) || pinnedHere.has(name);
|
|
65
|
+
// Pairings left as they are: only a locked colour could fix them.
|
|
66
|
+
const accepted = new Set();
|
|
67
|
+
const accept = (f) => accepted.add(f.label);
|
|
68
|
+
// Without locks, a pairing nothing fixes is a bug (it throws, as it always has); with them, a warning.
|
|
69
|
+
const cannot = (f) => {
|
|
70
|
+
if (!lockedHere.size && !pinnedHere.size) throw new Error(`fixContrast: cannot fix ${f.label} (${mode})`);
|
|
71
|
+
accept(f);
|
|
72
|
+
};
|
|
51
73
|
|
|
52
|
-
for (let pass = 0; pass < MAX_PASSES;
|
|
74
|
+
for (let pass = 0; pass < MAX_PASSES; ) {
|
|
53
75
|
const hex = hexNow();
|
|
54
|
-
const [f] = failing(contrastPairs(hex, mode, { strict: true }));
|
|
76
|
+
const [f] = failing(contrastPairs(hex, mode, { strict: true })).filter((p) => !accepted.has(p.label));
|
|
55
77
|
if (!f) break;
|
|
56
78
|
|
|
57
79
|
if (f.fgName === `${f.bgName}-foreground`) {
|
|
58
80
|
// 1. A label on its own fill.
|
|
59
81
|
const [best] = FOREGROUND_CHOICES.map((ref) => ({ ref, ratio: contrast(lookup(ref), hex[f.bgName]) })).sort((a, b) => b.ratio - a.ratio);
|
|
60
|
-
if (best.ratio >= f.threshold && out[f.fgName][mode] !== best.ref) {
|
|
82
|
+
if (!isLocked(f.fgName) && best.ratio >= f.threshold && out[f.fgName][mode] !== best.ref) {
|
|
61
83
|
set(f.fgName, best.ref);
|
|
84
|
+
pass++;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (isFixed(f.bgName) || isLocked(f.fgName)) {
|
|
88
|
+
accept(f);
|
|
62
89
|
continue;
|
|
63
90
|
}
|
|
64
91
|
const label = lookup(best.ref);
|
|
65
92
|
// A light label (it reads on black) needs a darker fill; a dark label a lighter one.
|
|
66
93
|
const dir = contrast(label, '#000000') > contrast(label, '#ffffff') ? -1 : 1;
|
|
67
94
|
const moved = nudge(hex[f.bgName], dir, (c) => contrast(label, c) >= f.threshold);
|
|
68
|
-
if (!moved)
|
|
95
|
+
if (!moved) {
|
|
96
|
+
cannot(f);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
69
99
|
set(f.fgName, best.ref);
|
|
70
100
|
moveFill(f.bgName, moved);
|
|
101
|
+
pass++;
|
|
71
102
|
continue;
|
|
72
103
|
}
|
|
73
104
|
|
|
74
105
|
// 2 and 3: move the ink until every pairing it takes part in as ink passes.
|
|
75
106
|
const ink = f.fgName;
|
|
76
|
-
|
|
107
|
+
if (isFixed(ink)) {
|
|
108
|
+
accept(f);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const asInk = (candidate) =>
|
|
112
|
+
failing(contrastPairs({ ...hex, [ink]: candidate }, mode, { strict: true, fg: ink })).filter((p) => !accepted.has(p.label)).length === 0;
|
|
77
113
|
const moved = nudge(hex[ink], away, asInk);
|
|
78
|
-
if (!moved)
|
|
114
|
+
if (!moved) {
|
|
115
|
+
cannot(f);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
79
118
|
moveFill(ink, moved);
|
|
119
|
+
pass++;
|
|
80
120
|
}
|
|
81
121
|
|
|
82
|
-
const
|
|
122
|
+
const all = failing(contrastPairs(hexNow(), mode, { strict: true }));
|
|
123
|
+
const left = all.filter((p) => !accepted.has(p.label));
|
|
83
124
|
if (left.length) throw new Error(`fixContrast: ${left[0].label} still fails in ${mode} after ${MAX_PASSES} passes`);
|
|
125
|
+
// The strict set can name a pairing twice (muted-foreground on muted): warn once.
|
|
126
|
+
for (const p of all.filter((p, i) => all.findIndex((q) => q.label === p.label) === i)) {
|
|
127
|
+
warnings.push({
|
|
128
|
+
mode,
|
|
129
|
+
label: p.label,
|
|
130
|
+
fg: p.fgName,
|
|
131
|
+
bg: p.bgName,
|
|
132
|
+
ratio: Number(p.ratio.toFixed(2)),
|
|
133
|
+
threshold: p.threshold,
|
|
134
|
+
locked: [p.fgName, p.bgName].filter(isFixed),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
84
137
|
|
|
85
138
|
for (const [name, before] of Object.entries(original)) {
|
|
86
139
|
const after = lookup(out[name][mode]);
|
|
@@ -91,9 +144,9 @@ export function fixContrast(semantic, lookup) {
|
|
|
91
144
|
const was = lookup(out[name][mode]);
|
|
92
145
|
set(name, hex);
|
|
93
146
|
if (name === 'primary') {
|
|
94
|
-
for (const linked of BRAND_LINKED) if (lookup(out[linked][mode]) === was) set(linked, hex);
|
|
147
|
+
for (const linked of BRAND_LINKED) if (!isFixed(linked) && lookup(out[linked][mode]) === was) set(linked, hex);
|
|
95
148
|
}
|
|
96
149
|
}
|
|
97
150
|
}
|
|
98
|
-
return { semantic: out, adjustments };
|
|
151
|
+
return { semantic: out, adjustments, warnings };
|
|
99
152
|
}
|
package/lib/theme/fonts.mjs
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* system the platform's own font for its role: nothing to load
|
|
14
14
|
* weights the static weights we load (400–700); a class asking for another snaps to the nearest
|
|
15
15
|
* expoPrefix the export prefix of the @expo-google-fonts package (Inter_600SemiBold → 'Inter')
|
|
16
|
+
* fontsource 'static' when only @fontsource/<id> exists; every other family has @fontsource-variable/<id>
|
|
17
|
+
* (for a shadcn project: lib/theme/shadcn.mjs shadcnFont)
|
|
16
18
|
*/
|
|
17
19
|
export const FONTS = [
|
|
18
20
|
{ id: 'system', family: 'System', category: 'sans', weights: [400, 500, 600, 700, 800], system: true },
|
|
@@ -35,7 +37,7 @@ export const FONTS = [
|
|
|
35
37
|
{ id: 'system-mono', family: 'Menlo', category: 'mono', weights: [400, 500, 600, 700], system: true },
|
|
36
38
|
{ id: 'jetbrains-mono', family: 'JetBrains Mono', category: 'mono', weights: [400, 500, 600, 700], expoPrefix: 'JetBrainsMono' },
|
|
37
39
|
{ id: 'geist-mono', family: 'Geist Mono', category: 'mono', weights: [400, 500, 600, 700], expoPrefix: 'GeistMono' },
|
|
38
|
-
{ id: 'ibm-plex-mono', family: 'IBM Plex Mono', category: 'mono', weights: [400, 500, 600, 700], expoPrefix: 'IBMPlexMono' },
|
|
40
|
+
{ id: 'ibm-plex-mono', family: 'IBM Plex Mono', category: 'mono', weights: [400, 500, 600, 700], expoPrefix: 'IBMPlexMono', fontsource: 'static' },
|
|
39
41
|
{ id: 'roboto-mono', family: 'Roboto Mono', category: 'mono', weights: [400, 500, 600, 700], expoPrefix: 'RobotoMono' },
|
|
40
42
|
{ id: 'fira-code', family: 'Fira Code', category: 'mono', weights: [400, 500, 600, 700], expoPrefix: 'FiraCode' },
|
|
41
43
|
{ id: 'source-code-pro', family: 'Source Code Pro', category: 'mono', weights: [400, 500, 600, 700], expoPrefix: 'SourceCodePro' },
|
|
@@ -43,6 +45,19 @@ export const FONTS = [
|
|
|
43
45
|
|
|
44
46
|
export const fontById = (id) => FONTS.find((f) => f.id === id);
|
|
45
47
|
|
|
48
|
+
/**
|
|
49
|
+
* A font for any Google Fonts family (a system's fonts can be any): its curated entry, or one made from the
|
|
50
|
+
* name the way @expo-google-fonts and next/font name it ('Inter Tight' → inter-tight, InterTight_600SemiBold,
|
|
51
|
+
* Inter_Tight), loading 400–700. A family without one of those weights fails to load; pick weights in the Studio.
|
|
52
|
+
*/
|
|
53
|
+
export function fontByFamily(family) {
|
|
54
|
+
const name = String(family).trim();
|
|
55
|
+
const curated = FONTS.find((f) => f.family.toLowerCase() === name.toLowerCase());
|
|
56
|
+
if (curated) return curated;
|
|
57
|
+
const id = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
58
|
+
return { id, family: name, category: 'sans', weights: [400, 500, 600, 700], expoPrefix: name.replace(/[^A-Za-z0-9]/g, '') };
|
|
59
|
+
}
|
|
60
|
+
|
|
46
61
|
/** The nearest loaded weight of a font to the one a class asks for. */
|
|
47
62
|
export function nearestWeight(font, weight) {
|
|
48
63
|
return font.weights.reduce((best, w) => (Math.abs(w - weight) < Math.abs(best - weight) ? w : best), font.weights[0]);
|
package/lib/theme/generate.mjs
CHANGED
|
@@ -3,29 +3,72 @@
|
|
|
3
3
|
* the browser for the live picker, the CLI runs it to commit a theme into tokens.json.
|
|
4
4
|
*
|
|
5
5
|
* generateTheme(recipe, base) base = the app's tokens.json (status ramps, radius steps come from it)
|
|
6
|
-
* → { recipe, code, primitives, semantic, resolved, radius, type, adjustments }
|
|
6
|
+
* → { recipe, code, primitives, semantic, resolved, radius, type, adjustments, warnings }
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* A recipe doesn't theme the status colours (success, warning, danger, info): they come from `base`. A system
|
|
9
|
+
* document does (lib/system/generate.mjs), through `colors`, the hook below that only generateSystem uses:
|
|
10
|
+
* colors.ramps { brand?, neutral?, danger?, … }: finished ramps in place of the recipe's (or base's)
|
|
11
|
+
* colors.anchor the brand seed's step; colors.anchors the status seeds' steps
|
|
12
|
+
* colors.roles { name: { light?, dark? } } references or hex chosen by hand: never moved by the fixer
|
|
13
|
+
* colors.edited { ramp: [steps] } edited by hand: a role on one keeps its colour (a warning if it fails)
|
|
14
|
+
* Without it, the theme is exactly the recipe's.
|
|
9
15
|
*/
|
|
10
16
|
import { fixContrast } from './fix.mjs';
|
|
11
17
|
import { encodeRecipe } from './codec.mjs';
|
|
12
18
|
import { fontById } from './fonts.mjs';
|
|
13
|
-
import {
|
|
19
|
+
import { componentLayer } from './components.mjs';
|
|
20
|
+
import { BORDER, BUTTON, DENSITY, DEPTH, FIELD, fieldWidth, FLOW, LEADING, normalizeRecipe, RADIUS, resolveTextStyles, STROKE, textEdits, typeScale } from './recipe.mjs';
|
|
14
21
|
import { anchorStep, brandRamp, neutralRamp } from './ramp.mjs';
|
|
15
22
|
import { resolvedMap } from './refs.mjs';
|
|
16
|
-
import { semanticColors } from './semantic.mjs';
|
|
23
|
+
import { BRAND_LINKED, semanticColors, styleColors } from './semantic.mjs';
|
|
17
24
|
|
|
18
25
|
|
|
19
26
|
const ramp = (values) => Object.fromEntries(Object.entries(values).map(([step, $value]) => [step, { $type: 'color', $value }]));
|
|
27
|
+
const STATUS = ['danger', 'success', 'warning', 'info'];
|
|
28
|
+
const modes = () => ({ light: new Set(), dark: new Set() });
|
|
29
|
+
|
|
30
|
+
/** Role overrides on top of a semantic map; the tokens that share primary's step follow a primary override. */
|
|
31
|
+
function withRoles(colors, roles, linked = false) {
|
|
32
|
+
if (!roles) return colors;
|
|
33
|
+
const out = Object.fromEntries(Object.entries(colors).map(([k, v]) => [k, { ...v }]));
|
|
34
|
+
for (const mode of ['light', 'dark']) {
|
|
35
|
+
const primary = roles.primary?.[mode];
|
|
36
|
+
if (linked && primary !== undefined) for (const name of BRAND_LINKED) if (!roles[name]?.[mode] && out[name]?.[mode] === colors.primary[mode]) out[name][mode] = primary;
|
|
37
|
+
for (const [name, value] of Object.entries(roles)) if (name in out && value[mode] !== undefined) out[name][mode] = value[mode];
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Which colours the fixer leaves alone, per mode: `locked` = mapped by hand (and what follows a hand-mapped
|
|
44
|
+
* primary), `pinned` = on an edited step. `from` locks a style colour that is a locked base colour (a solid
|
|
45
|
+
* button is the primary).
|
|
46
|
+
*/
|
|
47
|
+
function locks(colors, { roles = {}, edited = {} }, from) {
|
|
48
|
+
const locked = modes();
|
|
49
|
+
const pinned = modes();
|
|
50
|
+
const editedRef = new Set(Object.entries(edited).flatMap(([name, steps]) => steps.map((step) => `{primitive.color.${name}.${step}}`)));
|
|
51
|
+
for (const mode of ['light', 'dark']) {
|
|
52
|
+
for (const [name, value] of Object.entries(colors)) {
|
|
53
|
+
if (roles[name]?.[mode] !== undefined) locked[mode].add(name);
|
|
54
|
+
else if (roles.primary?.[mode] !== undefined && BRAND_LINKED.includes(name) && value[mode] === roles.primary[mode]) locked[mode].add(name);
|
|
55
|
+
else if (from && !(name in from.colors) && Object.entries(from.colors).some(([base, v]) => from.locked[mode].has(base) && v[mode] === value[mode])) locked[mode].add(name);
|
|
56
|
+
else if (editedRef.has(value[mode])) pinned[mode].add(name);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { locked, pinned };
|
|
60
|
+
}
|
|
20
61
|
|
|
21
62
|
/** The theme for a recipe, on top of `base` (a tokens.json object). `fix: false` skips the contrast fixer. */
|
|
22
|
-
export function generateTheme(input, base, { fix = true } = {}) {
|
|
63
|
+
export function generateTheme(input, base, { fix = true, colors: system } = {}) {
|
|
23
64
|
const recipe = normalizeRecipe(input);
|
|
24
|
-
const brand = brandRamp(recipe.brand);
|
|
25
|
-
const neutral = neutralRamp(recipe.neutral, recipe.brand);
|
|
65
|
+
const brand = system?.ramps?.brand ?? brandRamp(recipe.brand);
|
|
66
|
+
const neutral = system?.ramps?.neutral ?? neutralRamp(recipe.neutral, recipe.brand);
|
|
67
|
+
const status = Object.fromEntries(STATUS.filter((name) => system?.ramps?.[name]).map((name) => [name, system.ramps[name]]));
|
|
68
|
+
const statusTokens = Object.fromEntries(Object.entries(status).map(([name, values]) => [name, ramp(values)]));
|
|
26
69
|
|
|
27
70
|
// Primitive lookup: the base primitives with this theme's ramps swapped in.
|
|
28
|
-
const primitive = { ...base.primitive, color: { ...base.primitive.color, brand: ramp(brand), neutral: ramp(neutral) } };
|
|
71
|
+
const primitive = { ...base.primitive, color: { ...base.primitive.color, brand: ramp(brand), neutral: ramp(neutral), ...statusTokens } };
|
|
29
72
|
const prim = resolvedMap({ primitive }, 'light');
|
|
30
73
|
const lookup = (value) => {
|
|
31
74
|
if (typeof value !== 'string' || !value.startsWith('{')) return value;
|
|
@@ -34,9 +77,22 @@ export function generateTheme(input, base, { fix = true } = {}) {
|
|
|
34
77
|
return hex;
|
|
35
78
|
};
|
|
36
79
|
|
|
37
|
-
|
|
80
|
+
const anchor = system?.anchor ?? anchorStep(recipe.brand);
|
|
81
|
+
let colors = withRoles(semanticColors(anchor, system?.anchors), system?.roles, true);
|
|
82
|
+
const baseLocks = locks(colors, system ?? {});
|
|
38
83
|
let adjustments = [];
|
|
39
|
-
|
|
84
|
+
let warnings = [];
|
|
85
|
+
if (fix) ({ semantic: colors, adjustments, warnings } = fixContrast(colors, lookup, baseLocks));
|
|
86
|
+
// The styles' colours build on the fixed ones, then get their own check (a soft button's ink, a field's line).
|
|
87
|
+
const fixed = colors;
|
|
88
|
+
colors = { ...colors, ...withRoles(styleColors(recipe, anchor, colors), system?.roles) };
|
|
89
|
+
if (fix) {
|
|
90
|
+
const styled = fixContrast(colors, lookup, locks(colors, system ?? {}, { colors: fixed, locked: baseLocks.locked }));
|
|
91
|
+
colors = styled.semantic;
|
|
92
|
+
adjustments = [...adjustments, ...styled.adjustments];
|
|
93
|
+
// The second pass sees every pairing again: keep each warning once.
|
|
94
|
+
warnings = styled.warnings;
|
|
95
|
+
}
|
|
40
96
|
|
|
41
97
|
const radiusRef = RADIUS[recipe.radius];
|
|
42
98
|
const radiusValue = typeof radiusRef === 'string' ? `{primitive.radius.${radiusRef}}` : radiusRef;
|
|
@@ -57,10 +113,18 @@ export function generateTheme(input, base, { fix = true } = {}) {
|
|
|
57
113
|
measure: Number(recipe.measure),
|
|
58
114
|
};
|
|
59
115
|
|
|
60
|
-
|
|
116
|
+
const borderWidth = BORDER[recipe.border];
|
|
117
|
+
const btn = BUTTON[recipe.button];
|
|
118
|
+
const fld = FIELD[recipe.field];
|
|
119
|
+
const button = { alpha: btn.alpha, border: btn.border, edge: btn.edge };
|
|
120
|
+
const field = { alpha: fld.alpha, border: fieldWidth(fld.border, borderWidth), edge: fieldWidth(fld.edge, borderWidth), inset: fld.inset };
|
|
121
|
+
// rounded-md is the radius × md / lg (resolve.mjs); a field's radius is that or square.
|
|
122
|
+
const md = lookup('{primitive.radius.md}') / lookup('{primitive.radius.lg}');
|
|
123
|
+
|
|
124
|
+
const theme = {
|
|
61
125
|
recipe,
|
|
62
126
|
code: encodeRecipe(recipe),
|
|
63
|
-
primitives: { brand, neutral },
|
|
127
|
+
primitives: { brand, neutral, ...status },
|
|
64
128
|
fonts: { heading: font(recipe.font.heading), body: font(recipe.font.body), mono: font(recipe.font.mono) },
|
|
65
129
|
semantic: {
|
|
66
130
|
color: colors,
|
|
@@ -76,8 +140,45 @@ export function generateTheme(input, base, { fix = true } = {}) {
|
|
|
76
140
|
resolved,
|
|
77
141
|
radius,
|
|
78
142
|
radiusControl: lookup(controlRadius),
|
|
79
|
-
borderWidth
|
|
143
|
+
borderWidth,
|
|
144
|
+
button,
|
|
145
|
+
field: { ...field, radius: lookup(radiusValue) * md * fld.round, radiusBottom: lookup(radiusValue) * md * fld.roundBottom },
|
|
80
146
|
stroke: STROKE[recipe.stroke],
|
|
81
147
|
adjustments,
|
|
148
|
+
warnings,
|
|
82
149
|
};
|
|
150
|
+
return withComponents(theme, base);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The theme with its component layer (lib/theme/components.mjs): semantic.component (the tokens) and `components`
|
|
155
|
+
* (resolved). Built from the theme's own corners, density, border, styles, text styles and colours, so
|
|
156
|
+
* generateSystem calls it again once a system's edits are in, with the system's `components` as `overrides`.
|
|
157
|
+
*/
|
|
158
|
+
export function withComponents(theme, base, overrides = {}) {
|
|
159
|
+
const primitive = { ...base.primitive, color: { ...base.primitive.color, ...Object.fromEntries(Object.entries(theme.primitives).map(([name, values]) => [name, ramp(values)])) } };
|
|
160
|
+
const prim = resolvedMap({ primitive }, 'light');
|
|
161
|
+
const lg = prim['primitive.radius.lg'];
|
|
162
|
+
const edited = theme.semantic.radius;
|
|
163
|
+
const fld = FIELD[theme.recipe.field];
|
|
164
|
+
const { tokens, shape } = componentLayer({
|
|
165
|
+
styles: { button: theme.recipe.button, field: theme.recipe.field },
|
|
166
|
+
controls: theme.recipe.controls,
|
|
167
|
+
corner: (name) => (typeof edited[name] === 'number' ? edited[name] : (theme.radius * prim[`primitive.radius.${name}`]) / lg),
|
|
168
|
+
density: theme.density,
|
|
169
|
+
borderWidth: theme.borderWidth,
|
|
170
|
+
button: theme.button,
|
|
171
|
+
field: { ...theme.field, round: fld.round, roundBottom: fld.roundBottom },
|
|
172
|
+
textStyles: resolveTextStyles(theme.type, textEdits(theme.semantic.type)),
|
|
173
|
+
type: theme.type,
|
|
174
|
+
hex: (ref, mode) => {
|
|
175
|
+
const name = /^\{semantic\.color\.(.+)\}$/.exec(ref)?.[1];
|
|
176
|
+
if (name) return theme.resolved[mode][name];
|
|
177
|
+
const value = prim[ref.slice(1, -1)];
|
|
178
|
+
if (value === undefined) throw new Error(`generateTheme: ${ref} is not in the base tokens`);
|
|
179
|
+
return value;
|
|
180
|
+
},
|
|
181
|
+
overrides,
|
|
182
|
+
});
|
|
183
|
+
return { ...theme, semantic: { ...theme.semantic, component: tokens }, components: shape };
|
|
83
184
|
}
|
package/lib/theme/index.d.mts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
/** Types for @halftone-ds/tokens/theme (index.mjs). */
|
|
2
|
+
import type { HalftoneSystem } from '../system/index.mjs';
|
|
3
|
+
// Systems (lib/system): also @halftone-ds/tokens/system.
|
|
4
|
+
export * from '../system/index.mjs';
|
|
2
5
|
|
|
3
6
|
export type Neutral = 'zinc' | 'neutral' | 'slate' | 'stone' | 'gray' | 'brand';
|
|
4
7
|
export type Radius = 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
|
@@ -11,7 +14,13 @@ export type Size = '16' | '14' | '15' | '18';
|
|
|
11
14
|
export type Leading = 'normal' | 'tight' | 'relaxed';
|
|
12
15
|
export type Flow = 'normal' | 'tight' | 'loose';
|
|
13
16
|
export type Measure = '70' | '60' | '80' | '90';
|
|
17
|
+
export type ButtonStyle = 'solid' | 'soft' | 'outline' | 'raised';
|
|
18
|
+
export type FieldStyle = 'outline' | 'filled' | 'underline';
|
|
14
19
|
export type Mode = 'light' | 'dark';
|
|
20
|
+
export type CvdType = 'protanopia' | 'deuteranopia' | 'tritanopia';
|
|
21
|
+
/** A pair of tones as one vision sees them: `fg` and `bg` are the simulated hex of `a` and `b`. */
|
|
22
|
+
export type PairDistance = { a: string; b: string; vision: CvdType | 'none'; fg: string; bg: string; deltaE: number };
|
|
23
|
+
export type CvdWarning = PairDistance & { mode: Mode };
|
|
15
24
|
|
|
16
25
|
export type Recipe = {
|
|
17
26
|
v: number;
|
|
@@ -28,22 +37,31 @@ export type Recipe = {
|
|
|
28
37
|
leading: Leading;
|
|
29
38
|
flow: Flow;
|
|
30
39
|
measure: Measure;
|
|
40
|
+
/** The primary button's style. */
|
|
41
|
+
button: ButtonStyle;
|
|
42
|
+
/** Text fields' style (inputs, text areas, select triggers). */
|
|
43
|
+
field: FieldStyle;
|
|
31
44
|
preset?: string;
|
|
32
45
|
};
|
|
33
46
|
export type RecipeInput = Partial<Omit<Recipe, 'font'>> & { font?: Partial<Recipe['font']> };
|
|
34
47
|
|
|
35
48
|
export type Adjustment = { token: string; mode: Mode; from: string; to: string };
|
|
49
|
+
/** A pairing the contrast fixer left failing because a colour in it was chosen by hand (a system's role or step). */
|
|
50
|
+
export type ThemeWarning = { mode: Mode; label: string; fg: string; bg: string; ratio: number; threshold: number; locked: string[] };
|
|
36
51
|
|
|
37
52
|
export type Theme = {
|
|
38
53
|
recipe: Recipe;
|
|
39
54
|
code: string;
|
|
40
|
-
|
|
55
|
+
/** brand and neutral, plus a status ramp a system seeded or edited. */
|
|
56
|
+
primitives: { brand: Record<string, string>; neutral: Record<string, string> } & Partial<Record<'danger' | 'success' | 'warning' | 'info', Record<string, string>>>;
|
|
41
57
|
semantic: Record<string, unknown>;
|
|
42
58
|
/** Semantic colours as hex per mode, kebab-case names (`primary-foreground`). */
|
|
43
59
|
resolved: Record<Mode, Record<string, string>>;
|
|
44
60
|
/** Base radius in px. */
|
|
45
61
|
radius: number;
|
|
46
62
|
adjustments: Adjustment[];
|
|
63
|
+
/** Always empty for a recipe; a system's hand-picked colours that fail contrast. */
|
|
64
|
+
warnings: ThemeWarning[];
|
|
47
65
|
/** Font families by role ('' = system; mono 'Menlo' = the platform monospace). */
|
|
48
66
|
fonts: { heading: string; body: string; mono: string };
|
|
49
67
|
/** The typeset: size (px), scale (size / 16), leading, leadingFactor (leading / 1.75), flow (em), measure (ch). */
|
|
@@ -52,13 +70,84 @@ export type Theme = {
|
|
|
52
70
|
borderWidth: number;
|
|
53
71
|
stroke: number;
|
|
54
72
|
density: { sm: number; md: number; lg: number; x: number; scale: number };
|
|
73
|
+
/** The button style's numbers: fill opacity, border and bottom edge (px). */
|
|
74
|
+
button: { alpha: number; border: number; edge: number };
|
|
75
|
+
/** The field style's numbers: fill opacity per mode, border and bottom edge (px), radii (px), inset (0 or 1). */
|
|
76
|
+
field: { alpha: Record<Mode, number>; border: number; edge: number; inset: number; radius: number; radiusBottom: number };
|
|
77
|
+
/** The component layer (Button, Chip, Badge, Input), resolved: what both kits draw. */
|
|
78
|
+
components: ComponentShapes;
|
|
55
79
|
[key: string]: unknown;
|
|
56
80
|
};
|
|
57
81
|
|
|
82
|
+
export type ComponentName = 'button' | 'chip' | 'badge' | 'input';
|
|
83
|
+
export type Corner = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
|
|
84
|
+
export type ComponentSlot = 'fill' | 'label' | 'border' | 'pressed';
|
|
85
|
+
/** A component colour as drawn: its role (null for a style's ramp step), hex and opacity per mode. */
|
|
86
|
+
export type ComponentColor = {
|
|
87
|
+
role: string | null;
|
|
88
|
+
light: string;
|
|
89
|
+
dark: string;
|
|
90
|
+
alpha: Record<Mode, number>;
|
|
91
|
+
ref?: Record<Mode, string>;
|
|
92
|
+
/** Set in the system (not the default). */
|
|
93
|
+
edited?: boolean;
|
|
94
|
+
/** Pressed derived from the fill, a ring from the border. */
|
|
95
|
+
derived?: boolean;
|
|
96
|
+
};
|
|
97
|
+
export type ComponentVariant = Partial<Record<ComponentSlot | 'ring', ComponentColor>> & { borderWidth?: number; edgeWidth?: number };
|
|
98
|
+
export type ComponentShape = {
|
|
99
|
+
style?: string;
|
|
100
|
+
/** The Foundations corner it follows, or null when set in px. */
|
|
101
|
+
corner?: Corner | null;
|
|
102
|
+
/** px (9999 = a pill); Input: the top corners. */
|
|
103
|
+
radius: number;
|
|
104
|
+
radiusBottom?: number;
|
|
105
|
+
/** Heights (px) by size. */
|
|
106
|
+
h: Partial<Record<'sm' | 'md' | 'lg', number>>;
|
|
107
|
+
/** Horizontal padding (px) by size. */
|
|
108
|
+
x: Partial<Record<'sm' | 'md' | 'lg', number>>;
|
|
109
|
+
/** Font role, size and line height (px), tracking (em); `style` the text style it follows. */
|
|
110
|
+
text: { style?: string; font: 'heading' | 'body' | 'mono'; size: number; leading: number; tracking: number };
|
|
111
|
+
variants: Record<string, ComponentVariant>;
|
|
112
|
+
};
|
|
113
|
+
export type ComponentShapes = Record<ComponentName, ComponentShape>;
|
|
114
|
+
export type ComponentDefinition = {
|
|
115
|
+
prefix: string;
|
|
116
|
+
label: string;
|
|
117
|
+
styles: string[];
|
|
118
|
+
sizes: Array<'sm' | 'md' | 'lg' | 'x'>;
|
|
119
|
+
variants: Record<string, ComponentSlot[]>;
|
|
120
|
+
text: string;
|
|
121
|
+
};
|
|
122
|
+
export const COMPONENT_NAMES: ComponentName[];
|
|
123
|
+
export const COMPONENTS: Record<ComponentName, ComponentDefinition>;
|
|
124
|
+
export const CORNERS: Corner[];
|
|
125
|
+
export const SLOTS: ComponentSlot[];
|
|
126
|
+
export const SLOT_VAR: Record<ComponentSlot | 'ring', string>;
|
|
127
|
+
/** The touch-target floor (px) the mobile kit applies on phones. */
|
|
128
|
+
export const TOUCH_TARGET: number;
|
|
129
|
+
export function defaultCorner(name: ComponentName, options?: { controls?: string; style?: string }): Corner;
|
|
130
|
+
export function defaultStyle(name: ComponentName, recipe?: Partial<Recipe>): string;
|
|
131
|
+
export function fittedLeading(style: string): number;
|
|
132
|
+
export function componentVars(shape: ComponentShapes, format: 'hsl' | 'oklch', convert: (hex: string) => string, font?: (role: string) => string): Record<Mode, Record<string, string>>;
|
|
133
|
+
export function componentVarNames(): string[];
|
|
134
|
+
export function componentColors(shape: ComponentShapes, css: (key: string, alpha: boolean) => string): Record<string, string>;
|
|
135
|
+
export function componentPairs(
|
|
136
|
+
shape: ComponentShapes,
|
|
137
|
+
mode: Mode,
|
|
138
|
+
background: string,
|
|
139
|
+
blend: (hex: string, base: string, alpha: number) => string
|
|
140
|
+
): Array<{ component: ComponentName; variant: string; label: string; fg: string; bg: string; fgRole: string | null; bgRole: string | null }>;
|
|
141
|
+
/** Every variant's label on its fill, both modes: rows like contrastReport's, with the component and variant. */
|
|
142
|
+
export function componentContrast(shape: ComponentShapes, background: Record<Mode, string>): Array<ContrastPair & { component: ComponentName; variant: string }>;
|
|
143
|
+
|
|
58
144
|
export type TypeScale = { size: number; scale: number; leading: number; leadingFactor: number; flow: number; measure: number };
|
|
59
145
|
export type Font = { id: string; family: string; category: 'sans' | 'serif' | 'display' | 'mono'; weights: number[]; expoPrefix?: string; hidden?: boolean; system?: boolean };
|
|
60
146
|
export type Preset = { id: string; name: string; description: string; recipe: RecipeInput };
|
|
61
|
-
export type ThemeStatus =
|
|
147
|
+
export type ThemeStatus =
|
|
148
|
+
| { applied: false }
|
|
149
|
+
/** code is null for a system with overrides; system is set when a system document was applied. */
|
|
150
|
+
| { applied: true; code: string | null; recipe: Recipe; system?: HalftoneSystem; drift: string[] };
|
|
62
151
|
export type TokensJson = Record<string, unknown> & { primitive: Record<string, unknown>; semantic: Record<string, unknown> };
|
|
63
152
|
|
|
64
153
|
export const RECIPE_VERSION: number;
|
|
@@ -75,14 +164,20 @@ export const OPTIONS: {
|
|
|
75
164
|
leading: Leading[];
|
|
76
165
|
flow: Flow[];
|
|
77
166
|
measure: Measure[];
|
|
167
|
+
button: ButtonStyle[];
|
|
168
|
+
field: FieldStyle[];
|
|
78
169
|
};
|
|
79
170
|
export const TYPESET_KEYS: ['size', 'leading', 'flow', 'measure'];
|
|
171
|
+
export const STYLE_KEYS: ['button', 'field'];
|
|
80
172
|
export const LEADING: Record<Leading, number>;
|
|
81
173
|
export const FLOW: Record<Flow, number>;
|
|
82
174
|
export const FONTS: Font[];
|
|
83
175
|
export const PRESETS: Preset[];
|
|
84
176
|
export const BRAND_RAMPS: Record<string, Record<string, string>>;
|
|
85
177
|
export const THEME_EXTENSION: string;
|
|
178
|
+
export const CVD_TYPES: CvdType[];
|
|
179
|
+
export const DISTINCT_PAIRS: [string, string][];
|
|
180
|
+
export const MIN_DELTA_E: number;
|
|
86
181
|
|
|
87
182
|
export function normalizeRecipe(input?: RecipeInput): Recipe;
|
|
88
183
|
export function sameRecipe(a: RecipeInput, b: RecipeInput): boolean;
|
|
@@ -91,12 +186,81 @@ export function encodeRecipe(recipe: RecipeInput): string;
|
|
|
91
186
|
export function decodeRecipe(code: string): Recipe;
|
|
92
187
|
export function isThemeCode(value: unknown): boolean;
|
|
93
188
|
export function fontById(id: string): Font | undefined;
|
|
189
|
+
/** The curated font for a family, or one made from any Google Fonts family name (400–700). */
|
|
190
|
+
export function fontByFamily(family: string): Font;
|
|
94
191
|
export function googleFontsHref(ids: string[]): string | null;
|
|
95
192
|
export function presetById(id: string): Preset | undefined;
|
|
96
193
|
export function defaultTypeset(recipe: Recipe): boolean;
|
|
194
|
+
export function defaultStyles(recipe: Recipe): boolean;
|
|
97
195
|
export function typeVars(type: TypeScale): Record<string, string>;
|
|
196
|
+
export type TextStyleName = 'h1' | 'h2' | 'h3' | 'h4' | 'lead' | 'p' | 'large' | 'small' | 'muted' | 'code' | 'blockquote';
|
|
197
|
+
export type TextFontRole = 'heading' | 'body' | 'mono';
|
|
198
|
+
/** A text style as drawn: size and lineHeight px, leading × size, tracking em; edited: a system changed it. */
|
|
199
|
+
export type DrawnTextStyle = { font: TextFontRole; weight: number; size: number; leading: number; tracking: number; lineHeight: number; edited: boolean };
|
|
200
|
+
export const TEXT_STYLES: TextStyleName[];
|
|
201
|
+
export const TEXT_DEFAULTS: Record<TextStyleName, { font: TextFontRole; weight: number; size: number; leading: number | 'prose'; tracking: number }>;
|
|
202
|
+
/** A system's edited styles from a semantic.type group (a theme's, or a token file's DTCG one). */
|
|
203
|
+
export function textEdits(group?: Record<string, unknown>): Partial<Record<TextStyleName, Partial<Omit<DrawnTextStyle, 'lineHeight' | 'edited'>>>>;
|
|
204
|
+
export function resolveTextStyles(type: TypeScale, edits?: ReturnType<typeof textEdits>): Record<TextStyleName, DrawnTextStyle>;
|
|
205
|
+
export function themeTextStyles(theme: Theme): Record<TextStyleName, DrawnTextStyle>;
|
|
206
|
+
/** --type-<style>-{size,leading,weight,tracking} (+ -font on the web) in a kit's units. */
|
|
207
|
+
export function textStyleVars(styles: Record<string, DrawnTextStyle>, format: 'hsl' | 'oklch', font?: (role: TextFontRole) => string): Record<string, string>;
|
|
208
|
+
export function styleVars(styles: Pick<Theme, 'button' | 'field'>, format: 'hsl' | 'oklch'): Record<Mode, Record<string, string>>;
|
|
98
209
|
export function themeVars(theme: Theme, format: 'hsl' | 'oklch'): Record<Mode, Record<string, string>>;
|
|
99
210
|
export function themeHex(theme: Theme): Record<Mode, Record<string, string>>;
|
|
100
211
|
export function themeCss(theme: Theme, options?: { format?: 'hsl' | 'oklch'; light?: string; dark?: string }): string;
|
|
101
212
|
export function applyRecipe(tokens: TokensJson, recipe: RecipeInput): { tokens: TokensJson; theme: Theme };
|
|
102
213
|
export function themeStatus(tokens: TokensJson): ThemeStatus;
|
|
214
|
+
|
|
215
|
+
/** One pairing the contrast gate checks: `fg` on `bg` in one mode, its WCAG ratio and the threshold it must clear. */
|
|
216
|
+
export type ContrastPair = { mode: Mode; label: string; fgName: string; bgName: string; fg: string; bg: string; threshold: number; ratio: number };
|
|
217
|
+
/** A ContrastPair for a table: `pass` against its WCAG threshold, and `apca`, the advisory APCA Lc (signed). */
|
|
218
|
+
export type ContrastRow = ContrastPair & { pass: boolean; apca: number };
|
|
219
|
+
|
|
220
|
+
/** WCAG 2.1 AA for text (4.5) and for icons and other non-text (3). */
|
|
221
|
+
export const AA: number;
|
|
222
|
+
export const AA_NON_TEXT: number;
|
|
223
|
+
export function contrastPairs(hex: Record<string, string>, mode: Mode, options?: { strict?: boolean; fg?: string }): ContrastPair[];
|
|
224
|
+
/** Every pairing the build checks (strict by default), light then dark. `hex` is a theme's `resolved`. */
|
|
225
|
+
export function contrastReport(hex: Partial<Record<Mode, Record<string, string>>>, options?: { strict?: boolean }): ContrastRow[];
|
|
226
|
+
/** APCA Lc of `text` on `background` (APCA-W3 0.0.98G-4g). Advisory: it never fails the build. */
|
|
227
|
+
export function apcaContrast(text: string, background: string): number;
|
|
228
|
+
export function simulateCvd(hex: string, type: CvdType | 'none' | null | undefined): string;
|
|
229
|
+
export function pairDistances(hex: Record<string, string>, type: CvdType | 'none', pairs?: [string, string][]): PairDistance[];
|
|
230
|
+
export function cvdWarnings(resolved: Partial<Record<Mode, Record<string, string>>>, options?: { pairs?: [string, string][]; threshold?: number }): CvdWarning[];
|
|
231
|
+
export function describeCvdWarnings(warnings: CvdWarning[]): string[];
|
|
232
|
+
|
|
233
|
+
/** A decoded shadcn/ui preset code (ui.shadcn.com/create). */
|
|
234
|
+
export type ShadcnPreset = {
|
|
235
|
+
version: 'a' | 'b';
|
|
236
|
+
style: string;
|
|
237
|
+
baseColor: string;
|
|
238
|
+
theme: string;
|
|
239
|
+
chartColor: string;
|
|
240
|
+
iconLibrary: string;
|
|
241
|
+
font: string;
|
|
242
|
+
fontHeading: string;
|
|
243
|
+
radius: string;
|
|
244
|
+
menuAccent: string;
|
|
245
|
+
menuColor: string;
|
|
246
|
+
};
|
|
247
|
+
/** A shadcn registry item (registry:theme or registry:font), as JSON. */
|
|
248
|
+
export type ShadcnRegistryItem = {
|
|
249
|
+
$schema: string;
|
|
250
|
+
name: string;
|
|
251
|
+
title?: string;
|
|
252
|
+
description?: string;
|
|
253
|
+
type: 'registry:theme' | 'registry:font';
|
|
254
|
+
registryDependencies?: string[];
|
|
255
|
+
cssVars?: { theme?: Record<string, string>; light?: Record<string, string>; dark?: Record<string, string> };
|
|
256
|
+
font?: { family: string; provider: 'google'; import: string; variable: string; subsets?: string[]; weight?: string[]; dependency?: string };
|
|
257
|
+
};
|
|
258
|
+
export function isShadcnPresetCode(s: unknown): s is string;
|
|
259
|
+
export function decodeShadcnPreset(code: string): ShadcnPreset | null;
|
|
260
|
+
export function encodeShadcnPreset(preset: Partial<ShadcnPreset>): string;
|
|
261
|
+
/** The nearest Halftone recipe for a shadcn preset, and a note for each value it approximated or dropped. */
|
|
262
|
+
export function recipeFromShadcn(preset: ShadcnPreset): { recipe: Recipe; notes: string[] };
|
|
263
|
+
export const SHADCN_FONTS_URL: string;
|
|
264
|
+
export function shadcnTheme(theme: Theme, options?: { name?: string; title?: string; description?: string; fontsUrl?: string }): ShadcnRegistryItem;
|
|
265
|
+
export function shadcnFont(id: string, role?: 'body' | 'heading' | 'mono'): ShadcnRegistryItem | null;
|
|
266
|
+
export function shadcnFontPath(id: string, role: 'body' | 'heading' | 'mono'): string;
|
package/lib/theme/index.mjs
CHANGED
|
@@ -5,10 +5,40 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { generateTheme } from './generate.mjs';
|
|
7
7
|
export { encodeRecipe, decodeRecipe, isThemeCode } from './codec.mjs';
|
|
8
|
-
export { DEFAULT_RECIPE, defaultTypeset, FLOW, LEADING, OPTIONS, normalizeRecipe, sameRecipe, RECIPE_VERSION, TYPESET_KEYS } from './recipe.mjs';
|
|
9
|
-
export { FONTS, fontById, googleFontsHref } from './fonts.mjs';
|
|
8
|
+
export { DEFAULT_RECIPE, defaultStyles, defaultTypeset, FLOW, LEADING, OPTIONS, normalizeRecipe, sameRecipe, RECIPE_VERSION, resolveTextStyles, STYLE_KEYS, TEXT_DEFAULTS, TEXT_STYLES, textEdits, TYPESET_KEYS } from './recipe.mjs';
|
|
9
|
+
export { FONTS, fontByFamily, fontById, googleFontsHref } from './fonts.mjs';
|
|
10
10
|
export { PRESETS, presetById } from './presets.mjs';
|
|
11
|
-
export { themeVars, themeCss, themeHex, typeVars } from './vars.mjs';
|
|
11
|
+
export { styleVars, textStyleVars, themeTextStyles, themeVars, themeCss, themeHex, typeVars } from './vars.mjs';
|
|
12
12
|
export { applyRecipe, themeStatus, THEME_EXTENSION } from './apply.mjs';
|
|
13
|
+
// Systems (lib/system): also @halftone-ds/tokens/system.
|
|
14
|
+
export * from '../system/index.mjs';
|
|
13
15
|
export { BRAND_RAMPS } from './palettes.mjs';
|
|
16
|
+
export {
|
|
17
|
+
decodeShadcnPreset,
|
|
18
|
+
encodeShadcnPreset,
|
|
19
|
+
isShadcnPresetCode,
|
|
20
|
+
recipeFromShadcn,
|
|
21
|
+
SHADCN_FONTS_URL,
|
|
22
|
+
shadcnFont,
|
|
23
|
+
shadcnFontPath,
|
|
24
|
+
shadcnTheme,
|
|
25
|
+
} from './shadcn.mjs';
|
|
26
|
+
export { CVD_TYPES, cvdWarnings, describeCvdWarnings, DISTINCT_PAIRS, MIN_DELTA_E, pairDistances, simulateCvd } from '../cvd.mjs';
|
|
14
27
|
|
|
28
|
+
export { AA, AA_NON_TEXT, componentContrast, contrastPairs, contrastReport } from '../contrast.mjs';
|
|
29
|
+
export {
|
|
30
|
+
COMPONENT_NAMES,
|
|
31
|
+
COMPONENTS,
|
|
32
|
+
componentColors,
|
|
33
|
+
componentPairs,
|
|
34
|
+
componentVarNames,
|
|
35
|
+
componentVars,
|
|
36
|
+
CORNERS,
|
|
37
|
+
defaultCorner,
|
|
38
|
+
defaultStyle,
|
|
39
|
+
fittedLeading,
|
|
40
|
+
SLOT_VAR,
|
|
41
|
+
SLOTS,
|
|
42
|
+
TOUCH_TARGET,
|
|
43
|
+
} from './components.mjs';
|
|
44
|
+
export { apcaContrast } from '../apca.mjs';
|