@halftone-ds/tokens 0.0.0-stage → 1.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.
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Tailwind 4 / shadcn web (v4 shape): oklch variables in `:root` and `.dark`, the `@theme inline`
3
+ * mapping that turns them into utilities, and shadcn's base layer. A partial, not a full stylesheet:
4
+ * app/globals.css → @import "tailwindcss"; @import "./tokens.css";
5
+ * Same tokens, same contrast gate as the native CSS; colours are converted with a round-trip check.
6
+ * options: { out: 'tokens/generated/web.css' }
7
+ * density: false leave the spacing scale alone (the docs site)
8
+ * typeset: false leave the text sizes alone (the docs site keeps its reading sizes)
9
+ */
10
+ import { hexToOklch } from '../lib/color.mjs';
11
+ import { group, radiusCalc } from '../lib/resolve.mjs';
12
+ import { TEXT_LINE_HEIGHT } from '../lib/theme/recipe.mjs';
13
+ import { typeVars } from '../lib/theme/vars.mjs';
14
+
15
+ const rem = (px) => (px >= 9999 ? '9999px' : `${px / 16}rem`);
16
+ const SYSTEM_SANS = "ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif";
17
+ const SYSTEM_MONO = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace';
18
+
19
+ /** Tailwind's text-xs…4xl from the primitive sizes, scaled by the typeset (sizes and line heights). */
20
+ function textScale(ctx) {
21
+ const sizes = group(ctx.prim, ['font', 'size']).filter(({ key }) => key in TEXT_LINE_HEIGHT);
22
+ return sizes
23
+ .map(({ key, value }) => {
24
+ // Exact ratios, as Tailwind writes them (calc(1 / 0.75)), so the default typeset changes nothing.
25
+ return ` --text-${key}: calc(${value / 16}rem * var(--type-scale));\n --text-${key}--line-height: calc(${TEXT_LINE_HEIGHT[key]} / ${value} * var(--leading-factor));`;
26
+ })
27
+ .join('\n');
28
+ }
29
+
30
+ export default function shadcnWeb(ctx, options) {
31
+ const vars = (mode) => ctx.colors.map((c) => ` --${c.name}: ${hexToOklch(c[mode])};`).join('\n');
32
+ const shadows = (mode) => Object.entries(ctx.shape.shadows[mode]).map(([level, css]) => ` --shadow-${level}: ${css};`).join('\n');
33
+ const contents = `/* ${ctx.header} */
34
+ @custom-variant dark (&:is(.dark *));
35
+
36
+ :root {
37
+ --radius: ${rem(ctx.radius.base)};
38
+ --radius-control: ${rem(ctx.shape.control)};
39
+ --border-width: ${ctx.shape.borderWidth}px;
40
+ --icon-stroke: ${ctx.shape.stroke};${options.density === false ? '' : `\n --density: ${ctx.shape.density.scale};`}${
41
+ options.typeset === false ? '' : '\n' + Object.entries(typeVars(ctx.type)).map(([k, v]) => ` ${k}: ${v};`).join('\n')
42
+ }
43
+ ${shadows('light')}
44
+ ${vars('light')}
45
+ }
46
+
47
+ .dark {
48
+ ${shadows('dark')}
49
+ ${vars('dark')}
50
+ }
51
+
52
+ @theme inline {
53
+ ${ctx.radius.steps.map((s) => ` --radius-${s.name}: ${radiusCalc(s.scale)};`).join('\n')}
54
+ --radius-control: var(--radius-control);
55
+ --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
+ --shadow-2xs: var(--shadow-1);
57
+ --shadow-xs: var(--shadow-1);
58
+ --shadow-sm: var(--shadow-1);
59
+ --shadow-md: var(--shadow-2);
60
+ --shadow-lg: var(--shadow-3);
61
+ --shadow-xl: var(--shadow-3);
62
+ --shadow-2xl: var(--shadow-3);
63
+ --font-sans: var(--kit-font-body, ${SYSTEM_SANS});
64
+ --font-heading: var(--kit-font-heading, var(--kit-font-body, ${SYSTEM_SANS}));
65
+ --font-mono: var(--kit-font-mono, ${SYSTEM_MONO});${
66
+ options.typeset === false ? '' : `\n /* The theme's typeset scales every text size and its line height (text-sm, text-base…). */\n${textScale(ctx)}`
67
+ }
68
+ ${ctx.colors.map((c) => ` --color-${c.name}: var(--${c.name});`).join('\n')}
69
+ }
70
+
71
+ @layer base {
72
+ * {
73
+ @apply border-border outline-ring/50;
74
+ }
75
+ body {
76
+ @apply bg-background text-foreground;
77
+ }
78
+ /* The theme's heading font (--kit-font-heading, set by app/fonts.ts or a live theme). */
79
+ h1, h2, h3, h4, [data-slot='card-title'] {
80
+ font-family: var(--font-heading);
81
+ }
82
+ /* Lucide icons take the theme's stroke. */
83
+ svg.lucide {
84
+ stroke-width: var(--icon-stroke);
85
+ }
86
+ }
87
+ `;
88
+ return [{ path: options.out, contents }];
89
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * lib/theme.ts — semantic colours as hex per scheme, non-colour primitives, and (for Expo Router apps)
3
+ * the React Navigation theme.
4
+ * options: { out: 'lib/theme.ts', navTheme?: 'expo-router/react-navigation' | false }
5
+ */
6
+ import { camel, group } from '../lib/resolve.mjs';
7
+ import { presetById, sameRecipe, themeStatus } from '../lib/theme/index.mjs';
8
+
9
+ /** The applied theme, for a read-only summary in the app (Foundations). */
10
+ function themeInfo(source) {
11
+ const status = themeStatus(source);
12
+ if (!status.applied) return 'null';
13
+ const preset = presetById(status.recipe.preset ?? '');
14
+ const name = preset && sameRecipe(preset.recipe, status.recipe) ? preset.name : 'Custom';
15
+ return JSON.stringify({ code: status.code, name, edited: status.drift.length > 0, recipe: status.recipe }, null, 2);
16
+ }
17
+
18
+ function colorObject(colors, mode) {
19
+ return colors.map((c) => ` ${camel(c.name)}: '${c[mode]}',`).join('\n');
20
+ }
21
+
22
+ function primitiveGroup(prim, prefix) {
23
+ return group(prim, prefix)
24
+ .map(({ key, value }) => ` ${/^[a-z_$][\w$]*$/i.test(key) ? key : `'${key}'`}: ${JSON.stringify(value)},`)
25
+ .join('\n');
26
+ }
27
+
28
+ const navTheme = (scheme, base) => ` ${scheme}: {
29
+ ...${base},
30
+ colors: {
31
+ background: THEME.${scheme}.background,
32
+ border: THEME.${scheme}.border,
33
+ card: THEME.${scheme}.card,
34
+ notification: THEME.${scheme}.destructive,
35
+ primary: THEME.${scheme}.primary,
36
+ text: THEME.${scheme}.foreground,
37
+ },
38
+ },`;
39
+
40
+ export default function tsTheme(ctx, options) {
41
+ const nav = options.navTheme;
42
+ const contents = `// ${ctx.header}
43
+ ${nav ? `import { DarkTheme, DefaultTheme, type Theme } from '${nav}';\n` : ''}
44
+ /**
45
+ * Semantic colours as hex, per scheme. Use ONLY where a className cannot reach:
46
+ * SVG fills, chart strokes, canvas, platform props (a StatusBar, gradient stops). Everything
47
+ * else styles with Tailwind classes (bg-primary, text-muted-foreground, …).
48
+ */
49
+ export const THEME = {
50
+ light: {
51
+ ${colorObject(ctx.colors, 'light')}
52
+ },
53
+ dark: {
54
+ ${colorObject(ctx.colors, 'dark')}
55
+ },
56
+ } as const;
57
+
58
+ export type ThemeColors = typeof THEME.light;
59
+ export type ThemeColorName = keyof ThemeColors;
60
+
61
+ /** Non-colour primitives (px / ms) for the rare inline-style or Reanimated use. */
62
+ export const TOKENS = {
63
+ space: {
64
+ ${primitiveGroup(ctx.prim, ['space'])}
65
+ },
66
+ radius: {
67
+ ${primitiveGroup(ctx.prim, ['radius'])}
68
+ base: ${ctx.radius.base},
69
+ control: ${ctx.shape.control},
70
+ },
71
+ /** Lucide stroke width for icons (the Icon component's default; an explicit strokeWidth is relative to 2). */
72
+ iconStroke: ${ctx.shape.stroke},
73
+ /** Width of \`border\` in px. */
74
+ borderWidth: ${ctx.shape.borderWidth},
75
+ /** Control heights and padding (px) for the theme's density: h-control-sm, h-control, h-control-lg, px-control-x. */
76
+ density: ${JSON.stringify({ sm: ctx.shape.density.sm, md: ctx.shape.density.md, lg: ctx.shape.density.lg, x: ctx.shape.density.x })},
77
+ /** The type scale at 16px; the theme's typeset multiplies it (text-* classes are already scaled). */
78
+ fontSize: {
79
+ ${primitiveGroup(ctx.prim, ['font', 'size'])}
80
+ },
81
+ /** The typeset: body size (px) and its scale, running-text leading and its factor on the text sizes' line
82
+ * heights, and Prose's flow (em between blocks) and measure (characters per line). */
83
+ typeset: ${JSON.stringify(ctx.type)},
84
+ fontFamily: {
85
+ ${primitiveGroup(ctx.prim, ['font', 'family'])}
86
+ },
87
+ duration: {
88
+ ${primitiveGroup(ctx.prim, ['duration'])}
89
+ },
90
+ easing: {
91
+ ${primitiveGroup(ctx.prim, ['easing'])}
92
+ },
93
+ } as const;
94
+
95
+ /** The theme tokens.json was generated from (\`halftone-tokens theme apply\`), or null. \`edited\`: theme tokens
96
+ * were changed by hand since. */
97
+ export const THEME_INFO: ThemeInfo | null = ${themeInfo(ctx.source)};
98
+
99
+ export type ThemeInfo = {
100
+ code: string;
101
+ name: string;
102
+ edited: boolean;
103
+ recipe: {
104
+ v: number;
105
+ brand: string;
106
+ neutral: string;
107
+ radius: string;
108
+ controls: string;
109
+ font: { heading: string; body: string; mono: string };
110
+ stroke: string;
111
+ depth: string;
112
+ density: string;
113
+ border: string;
114
+ size: string;
115
+ leading: string;
116
+ flow: string;
117
+ measure: string;
118
+ preset?: string;
119
+ };
120
+ };
121
+ ${
122
+ nav
123
+ ? `
124
+ export const NAV_THEME: Record<'light' | 'dark', Theme> = {
125
+ ${navTheme('light', 'DefaultTheme')}
126
+ ${navTheme('dark', 'DarkTheme')}
127
+ };
128
+ `
129
+ : ''
130
+ }`;
131
+ return [{ path: options.out, contents }];
132
+ }