@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/targets/shadcn-web.mjs
CHANGED
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
* typeset: false leave the text sizes alone (the docs site keeps its reading sizes)
|
|
9
9
|
*/
|
|
10
10
|
import { hexToOklch } from '../lib/color.mjs';
|
|
11
|
-
import { group,
|
|
11
|
+
import { group, radiusCss } from '../lib/resolve.mjs';
|
|
12
12
|
import { TEXT_LINE_HEIGHT } from '../lib/theme/recipe.mjs';
|
|
13
|
-
import {
|
|
13
|
+
import { COMPONENT_NAMES, COMPONENTS, componentColors, componentVars } from '../lib/theme/components.mjs';
|
|
14
|
+
import { hasExtras } from '../lib/theme/extras.mjs';
|
|
15
|
+
import { extraVars, styleVars, textStyleVars, typeVars } from '../lib/theme/vars.mjs';
|
|
14
16
|
|
|
15
17
|
const rem = (px) => (px >= 9999 ? '9999px' : `${px / 16}rem`);
|
|
16
18
|
const SYSTEM_SANS = "ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif";
|
|
@@ -27,8 +29,96 @@ function textScale(ctx) {
|
|
|
27
29
|
.join('\n');
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
/** A font role's family, as @theme's --font-* resolve it (app/fonts.ts or a live theme sets --kit-font-*). */
|
|
33
|
+
const FONT_ROLE = {
|
|
34
|
+
heading: `var(--kit-font-heading, var(--kit-font-body, ${SYSTEM_SANS}))`,
|
|
35
|
+
body: `var(--kit-font-body, ${SYSTEM_SANS})`,
|
|
36
|
+
mono: `var(--kit-font-mono, ${SYSTEM_MONO})`,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The text styles as utilities: type-h1 … type-blockquote set the style's font, size, line height, tracking and
|
|
41
|
+
* weight from its variables. leading-*, tracking-* and font-* on the same element still win, as on text-*.
|
|
42
|
+
*/
|
|
43
|
+
function textStyleUtilities(names) {
|
|
44
|
+
return names
|
|
45
|
+
.map(
|
|
46
|
+
(name) => `@utility type-${name} {
|
|
47
|
+
font-family: var(--type-${name}-font);
|
|
48
|
+
font-size: var(--type-${name}-size);
|
|
49
|
+
line-height: var(--tw-leading, var(--type-${name}-leading));
|
|
50
|
+
letter-spacing: var(--tw-tracking, var(--type-${name}-tracking));
|
|
51
|
+
font-weight: var(--tw-font-weight, var(--type-${name}-weight));
|
|
52
|
+
}`
|
|
53
|
+
)
|
|
54
|
+
.join('\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A system's extras (lib/theme/extras.mjs): their variables in :root and .dark, colours and corners in @theme (so
|
|
59
|
+
* bg-<name>, text-<name>, rounded-<name> exist), and shadow-, border- and type- utilities. No kit component uses them.
|
|
60
|
+
*/
|
|
61
|
+
function extrasCss(ctx) {
|
|
62
|
+
const list = ctx.extras;
|
|
63
|
+
if (!list || !hasExtras(list)) return { light: '', dark: '', theme: '', utilities: '' };
|
|
64
|
+
const vars = extraVars(list, 'oklch', (role) => FONT_ROLE[role]);
|
|
65
|
+
const lines = (map) => Object.entries(map).map(([k, v]) => ` ${k}: ${v};`).join('\n');
|
|
66
|
+
// Corners are literal in @theme (rounded-<name>), not variables of their own.
|
|
67
|
+
const light = Object.fromEntries(Object.entries(vars.light).filter(([k]) => !k.startsWith('--radius-')));
|
|
68
|
+
const theme = [
|
|
69
|
+
...list.color.map((c) => ` --color-${c.name}: var(--${c.name});`),
|
|
70
|
+
...list.radius.map((r) => ` --radius-${r.name}: ${rem(r.px)};`),
|
|
71
|
+
];
|
|
72
|
+
const utilities = [
|
|
73
|
+
...list.shadow.map((s) => `@utility shadow-${s.name} {\n box-shadow: var(--shadow-${s.name});\n}`),
|
|
74
|
+
...list.border.map((b) => `@utility border-${b.name} {\n border-width: var(--border-${b.name}-width);\n border-style: ${b.style ?? 'solid'};\n}`),
|
|
75
|
+
textStyleUtilities(list.text.map((t) => t.name)),
|
|
76
|
+
].filter(Boolean);
|
|
77
|
+
return {
|
|
78
|
+
light: `\n /* Extras: this system's own tokens. No kit component uses them; screens may. */\n${lines(light)}`,
|
|
79
|
+
dark: Object.keys(vars.dark).length ? `\n /* Extras */\n${lines(vars.dark)}` : '',
|
|
80
|
+
theme: theme.length ? `\n /* Extras */\n${theme.join('\n')}` : '',
|
|
81
|
+
utilities: utilities.length ? `\n/* Extras */\n${utilities.join('\n')}\n` : '',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The component layer (Button, Chip, Badge, Input): the variables per mode, each variant's colours as Tailwind colours
|
|
87
|
+
* (bg-button-primary-bg; a colour with an opacity mixes it in), rounded-button / -chip / -badge, and type-button,
|
|
88
|
+
* type-chip, type-badge and type-field for the text. Heights and padding are read as h-(--button-h-md), px-(--button-x).
|
|
89
|
+
*/
|
|
90
|
+
function componentsCss(ctx) {
|
|
91
|
+
const vars = componentVars(ctx.components, 'oklch', hexToOklch, (role) => FONT_ROLE[role]);
|
|
92
|
+
const lines = (map) => Object.entries(map).map(([k, v]) => ` ${k}: ${v};`).join('\n');
|
|
93
|
+
const colors = componentColors(ctx.components, (key, alpha) => (alpha ? `color-mix(in oklab, var(--${key}) calc(var(--${key}-a) * 100%), transparent)` : `var(--${key})`));
|
|
94
|
+
const theme = [
|
|
95
|
+
...COMPONENT_NAMES.filter((n) => n !== 'input').map((n) => ` --radius-${n}: var(--${n}-radius);`),
|
|
96
|
+
...Object.entries(colors).map(([key, css]) => ` --color-${key}: ${css};`),
|
|
97
|
+
];
|
|
98
|
+
const utilities = COMPONENT_NAMES.map((n) => COMPONENTS[n].prefix).map(
|
|
99
|
+
(p) => `@utility type-${p} {
|
|
100
|
+
font-family: var(--${p}-text-font);
|
|
101
|
+
font-size: var(--${p}-text-size);
|
|
102
|
+
line-height: var(--tw-leading, var(--${p}-text-leading));
|
|
103
|
+
letter-spacing: var(--tw-tracking, var(--${p}-text-tracking));
|
|
104
|
+
}`
|
|
105
|
+
);
|
|
106
|
+
return {
|
|
107
|
+
light: `\n /* Components: Button, Chip, Badge, Input (corners, sizes, text, each variant's colours). */\n${lines(vars.light)}`,
|
|
108
|
+
dark: `\n /* Components */\n${lines(vars.dark)}`,
|
|
109
|
+
theme: `\n /* Components: rounded-button, bg-button-primary-bg, border-field-focus-border… */\n${theme.join('\n')}`,
|
|
110
|
+
utilities: `${utilities.join('\n')}\n`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
30
114
|
export default function shadcnWeb(ctx, options) {
|
|
115
|
+
const extras = extrasCss(ctx);
|
|
116
|
+
const components = componentsCss(ctx);
|
|
31
117
|
const vars = (mode) => ctx.colors.map((c) => ` --${c.name}: ${hexToOklch(c[mode])};`).join('\n');
|
|
118
|
+
const styles = (mode) => Object.entries(styleVars(ctx.shape, 'oklch')[mode]).map(([k, v]) => ` ${k}: ${v};`).join('\n');
|
|
119
|
+
// The styles' fills carry their own opacity (an outline button, a transparent field).
|
|
120
|
+
const color = (name) =>
|
|
121
|
+
name === 'button' || name === 'field' ? `color-mix(in oklab, var(--${name}) calc(var(--${name}-alpha) * 100%), transparent)` : `var(--${name})`;
|
|
32
122
|
const shadows = (mode) => Object.entries(ctx.shape.shadows[mode]).map(([level, css]) => ` --shadow-${level}: ${css};`).join('\n');
|
|
33
123
|
const contents = `/* ${ctx.header} */
|
|
34
124
|
@custom-variant dark (&:is(.dark *));
|
|
@@ -40,18 +130,26 @@ export default function shadcnWeb(ctx, options) {
|
|
|
40
130
|
--icon-stroke: ${ctx.shape.stroke};${options.density === false ? '' : `\n --density: ${ctx.shape.density.scale};`}${
|
|
41
131
|
options.typeset === false ? '' : '\n' + Object.entries(typeVars(ctx.type)).map(([k, v]) => ` ${k}: ${v};`).join('\n')
|
|
42
132
|
}
|
|
133
|
+
${styles('light')}
|
|
43
134
|
${shadows('light')}
|
|
44
|
-
|
|
135
|
+
/* The text styles (type-h1…type-blockquote), literal at the typeset: an edited style is one variable. */
|
|
136
|
+
${Object.entries(textStyleVars(ctx.textStyles, 'oklch', (role) => FONT_ROLE[role])).map(([k, v]) => ` ${k}: ${v};`).join('\n')}
|
|
137
|
+
${vars('light')}${components.light}${extras.light}
|
|
45
138
|
}
|
|
46
139
|
|
|
47
140
|
.dark {
|
|
141
|
+
${styles('dark')}
|
|
48
142
|
${shadows('dark')}
|
|
49
|
-
${vars('dark')}
|
|
143
|
+
${vars('dark')}${components.dark}${extras.dark}
|
|
50
144
|
}
|
|
51
145
|
|
|
52
146
|
@theme inline {
|
|
53
|
-
${ctx.radius.steps.map((s) => ` --radius-${s.name}: ${
|
|
147
|
+
${ctx.radius.steps.map((s) => ` --radius-${s.name}: ${radiusCss(s)};`).join('\n')}
|
|
54
148
|
--radius-control: var(--radius-control);
|
|
149
|
+
/* The field style: rounded-field, rounded-b-field-b, px-field-x. Widths are border-(length:--field-border-w) etc. */
|
|
150
|
+
--radius-field: var(--field-radius);
|
|
151
|
+
--radius-field-b: var(--field-radius-b);
|
|
152
|
+
--spacing-field-x: var(--field-x);
|
|
55
153
|
--default-border-width: var(--border-width);${options.density === false ? '' : `\n /* The theme's density scales the whole spacing scale (p-4, gap-2, h-9…). */\n --spacing: calc(0.25rem * var(--density));`}
|
|
56
154
|
--shadow-2xs: var(--shadow-1);
|
|
57
155
|
--shadow-xs: var(--shadow-1);
|
|
@@ -65,9 +163,11 @@ ${ctx.radius.steps.map((s) => ` --radius-${s.name}: ${radiusCalc(s.scale)};`).j
|
|
|
65
163
|
--font-mono: var(--kit-font-mono, ${SYSTEM_MONO});${
|
|
66
164
|
options.typeset === false ? '' : `\n /* The theme's typeset scales every text size and its line height (text-sm, text-base…). */\n${textScale(ctx)}`
|
|
67
165
|
}
|
|
68
|
-
${ctx.colors.map((c) => ` --color-${c.name}:
|
|
166
|
+
${ctx.colors.map((c) => ` --color-${c.name}: ${color(c.name)};`).join('\n')}${components.theme}${extras.theme}
|
|
69
167
|
}
|
|
70
168
|
|
|
169
|
+
${textStyleUtilities(Object.keys(ctx.textStyles))}
|
|
170
|
+
${components.utilities}${extras.utilities}
|
|
71
171
|
@layer base {
|
|
72
172
|
* {
|
|
73
173
|
@apply border-border outline-ring/50;
|
package/targets/ts-theme.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* options: { out: 'lib/theme.ts', navTheme?: 'expo-router/react-navigation' | false }
|
|
5
5
|
*/
|
|
6
6
|
import { camel, group } from '../lib/resolve.mjs';
|
|
7
|
+
import { COMPONENT_NAMES, componentVarNames, TOUCH_TARGET } from '../lib/theme/components.mjs';
|
|
7
8
|
import { presetById, sameRecipe, themeStatus } from '../lib/theme/index.mjs';
|
|
8
9
|
|
|
9
10
|
/** The applied theme, for a read-only summary in the app (Foundations). */
|
|
@@ -15,6 +16,25 @@ function themeInfo(source) {
|
|
|
15
16
|
return JSON.stringify({ code: status.code, name, edited: status.drift.length > 0, recipe: status.recipe }, null, 2);
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
const round = (n) => Number(n.toFixed(4));
|
|
20
|
+
|
|
21
|
+
/** The component layer's numbers: sizes (px, for the phone touch-target floor) and each text's font role and tracking. */
|
|
22
|
+
function componentsObject(components) {
|
|
23
|
+
return COMPONENT_NAMES.map((name) => {
|
|
24
|
+
const c = components[name];
|
|
25
|
+
const text = { font: c.text.font, size: round(c.text.size), lineHeight: round(c.text.leading), tracking: c.text.tracking };
|
|
26
|
+
// The font role is typed as a role, so code can branch on it (the object is `as const`).
|
|
27
|
+
const json = JSON.stringify({ height: c.h, x: c.x, radius: c.radius, text }).replace(/"font":("\w+")/, `"font":$1 as 'heading' | 'body' | 'mono'`);
|
|
28
|
+
return ` ${name}: ${json},`;
|
|
29
|
+
}).join('\n');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function textStylesObject(styles) {
|
|
33
|
+
return Object.entries(styles)
|
|
34
|
+
.map(([name, s]) => ` ${name}: ${JSON.stringify({ font: s.font, weight: s.weight, size: round(s.size), lineHeight: round(s.lineHeight), tracking: s.tracking })},`)
|
|
35
|
+
.join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
function colorObject(colors, mode) {
|
|
19
39
|
return colors.map((c) => ` ${camel(c.name)}: '${c[mode]}',`).join('\n');
|
|
20
40
|
}
|
|
@@ -72,6 +92,17 @@ ${primitiveGroup(ctx.prim, ['radius'])}
|
|
|
72
92
|
iconStroke: ${ctx.shape.stroke},
|
|
73
93
|
/** Width of \`border\` in px. */
|
|
74
94
|
borderWidth: ${ctx.shape.borderWidth},
|
|
95
|
+
/** The theme's styles (button: solid · soft · outline · raised; field: outline · filled · underline) and their
|
|
96
|
+
* numbers (px): bg-button / bg-field, border-button-w, rounded-field… already follow them. */
|
|
97
|
+
styles: ${JSON.stringify(ctx.styles)},
|
|
98
|
+
button: ${JSON.stringify(ctx.shape.button)},
|
|
99
|
+
field: ${JSON.stringify(ctx.shape.field)},
|
|
100
|
+
/** The component layer (Button, Chip, Badge, Input) as numbers: heights and padding (px), corner, and the text's
|
|
101
|
+
* font role and tracking. The classes (h-button-md, rounded-button, text-type-button…) already follow them; this is
|
|
102
|
+
* for the touch-target floor (phones raise a height under ${TOUCH_TARGET} with hitSlop) and the font. */
|
|
103
|
+
components: {
|
|
104
|
+
${componentsObject(ctx.components)}
|
|
105
|
+
},
|
|
75
106
|
/** Control heights and padding (px) for the theme's density: h-control-sm, h-control, h-control-lg, px-control-x. */
|
|
76
107
|
density: ${JSON.stringify({ sm: ctx.shape.density.sm, md: ctx.shape.density.md, lg: ctx.shape.density.lg, x: ctx.shape.density.x })},
|
|
77
108
|
/** The type scale at 16px; the theme's typeset multiplies it (text-* classes are already scaled). */
|
|
@@ -81,6 +112,12 @@ ${primitiveGroup(ctx.prim, ['font', 'size'])}
|
|
|
81
112
|
/** The typeset: body size (px) and its scale, running-text leading and its factor on the text sizes' line
|
|
82
113
|
* heights, and Prose's flow (em between blocks) and measure (characters per line). */
|
|
83
114
|
typeset: ${JSON.stringify(ctx.type)},
|
|
115
|
+
/** The Text variants at the typeset, a system's edited styles included: font role, weight, size and line height
|
|
116
|
+
* (px), tracking (em). The classes (text-type-h1, leading-type-h1…) already follow them; this is for code
|
|
117
|
+
* that picks something per style, such as a font file per weight. */
|
|
118
|
+
textStyles: {
|
|
119
|
+
${textStylesObject(ctx.textStyles)}
|
|
120
|
+
},
|
|
84
121
|
fontFamily: {
|
|
85
122
|
${primitiveGroup(ctx.prim, ['font', 'family'])}
|
|
86
123
|
},
|
|
@@ -92,6 +129,9 @@ ${primitiveGroup(ctx.prim, ['easing'])}
|
|
|
92
129
|
},
|
|
93
130
|
} as const;
|
|
94
131
|
|
|
132
|
+
/** The component layer's CSS variables (what an embedded kit accepts from the docs site's live theme). */
|
|
133
|
+
export const COMPONENT_VARS = ${JSON.stringify(componentVarNames())} as const;
|
|
134
|
+
|
|
95
135
|
/** The theme tokens.json was generated from (\`halftone-tokens theme apply\`), or null. \`edited\`: theme tokens
|
|
96
136
|
* were changed by hand since. */
|
|
97
137
|
export const THEME_INFO: ThemeInfo | null = ${themeInfo(ctx.source)};
|
|
@@ -115,6 +155,8 @@ export type ThemeInfo = {
|
|
|
115
155
|
leading: string;
|
|
116
156
|
flow: string;
|
|
117
157
|
measure: string;
|
|
158
|
+
button?: string;
|
|
159
|
+
field?: string;
|
|
118
160
|
preset?: string;
|
|
119
161
|
};
|
|
120
162
|
};
|