@arun-dev/tokens 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@arun-dev/tokens",
3
+ "version": "0.1.0",
4
+ "description": "Pure-CSS design tokens with white-label brand generation — primitives, brand palettes, semantic layers, and the createBrand() generator",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/arun9483/arun-design-system.git",
10
+ "directory": "packages/tokens"
11
+ },
12
+ "keywords": [
13
+ "design-tokens",
14
+ "css",
15
+ "design-system",
16
+ "theming",
17
+ "white-label"
18
+ ],
19
+ "sideEffects": [
20
+ "**/*.css"
21
+ ],
22
+ "files": [
23
+ "dist",
24
+ "src",
25
+ "!src/**/*.unit.spec.ts"
26
+ ],
27
+ "exports": {
28
+ "./base": "./src/base.css",
29
+ "./brands/default": "./src/brands/default.css",
30
+ "./brands/default/palette": "./src/brands/default/palette.css",
31
+ "./brands/default/semantic": "./src/brands/default/semantic.css",
32
+ "./primitives/typography": "./src/primitives/typography.css",
33
+ "./primitives/spacing": "./src/primitives/spacing.css",
34
+ "./primitives/radius": "./src/primitives/radius.css",
35
+ "./primitives/shadow": "./src/primitives/shadow.css",
36
+ "./primitives/motion": "./src/primitives/motion.css",
37
+ "./primitives/elevation": "./src/primitives/elevation.css",
38
+ "./primitives/breakpoints": "./src/primitives/breakpoints.css",
39
+ "./primitives/fonts": "./src/primitives/fonts.css",
40
+ "./createBrand": {
41
+ "import": {
42
+ "types": "./dist/createBrand.d.ts",
43
+ "default": "./dist/createBrand.js"
44
+ },
45
+ "require": {
46
+ "types": "./dist/createBrand.d.cts",
47
+ "default": "./dist/createBrand.cjs"
48
+ }
49
+ }
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "devDependencies": {
55
+ "eslint": "9.39.4",
56
+ "tsup": "8.5.1",
57
+ "typescript": "5.9.3",
58
+ "vitest": "3.2.4",
59
+ "@arun-dev/config": "0.0.0"
60
+ },
61
+ "scripts": {
62
+ "build": "tsup",
63
+ "clean": "rm -rf dist .turbo",
64
+ "typecheck": "tsc --noEmit",
65
+ "lint": "eslint .",
66
+ "lint:fix": "eslint . --fix",
67
+ "test": "pnpm run test:unit",
68
+ "test:unit": "vitest run --passWithNoTests --config vitest.unit.config.ts"
69
+ }
70
+ }
package/src/base.css ADDED
@@ -0,0 +1,10 @@
1
+ /* Brand-agnostic primitives — import once as the structural token layer */
2
+ /* fonts.css omitted: @font-face loading is a consumer concern.
3
+ Next.js consumers use next/font/local in layout.tsx; others import fonts.css directly. */
4
+ @import './primitives/typography.css';
5
+ @import './primitives/spacing.css';
6
+ @import './primitives/radius.css';
7
+ @import './primitives/shadow.css';
8
+ @import './primitives/motion.css';
9
+ @import './primitives/elevation.css';
10
+ @import './primitives/breakpoints.css';
@@ -0,0 +1,28 @@
1
+ :root {
2
+ /* Brand palette — indigo */
3
+ --color-brand-50: #eef2ff;
4
+ --color-brand-100: #e0e7ff;
5
+ --color-brand-200: #c7d2fe;
6
+ --color-brand-300: #a5b4fc;
7
+ --color-brand-400: #818cf8;
8
+ --color-brand-500: #6366f1;
9
+ --color-brand-600: #4f46e5;
10
+ --color-brand-700: #4338ca;
11
+ --color-brand-800: #3730a3;
12
+ --color-brand-900: #312e81;
13
+ --color-brand-950: #1e1b4b;
14
+
15
+ /* Neutral palette — slate (cool blue-gray) */
16
+ --color-neutral-0: #ffffff;
17
+ --color-neutral-50: #f8fafc;
18
+ --color-neutral-100: #f1f5f9;
19
+ --color-neutral-200: #e2e8f0;
20
+ --color-neutral-300: #cbd5e1;
21
+ --color-neutral-400: #96a5ba; /* nudged +2 per channel → 7.13:1 on neutral-900 (AAA) */
22
+ --color-neutral-500: #64748b;
23
+ --color-neutral-600: #475569;
24
+ --color-neutral-700: #334155;
25
+ --color-neutral-800: #1e293b;
26
+ --color-neutral-900: #0f172a;
27
+ --color-neutral-950: #020617;
28
+ }
@@ -0,0 +1,162 @@
1
+ /* Semantic tokens — derived from default palette.
2
+ Consumer contract: all variables listed here must be defined for @arun-dev/ui to render correctly.
3
+ Consumers may import this file and override individual variables, or replace it entirely.
4
+
5
+ Source of truth for createBrand()'s deriveLight()/deriveDark() (src/createBrand.ts) — if you
6
+ change a value here, change it there too. Guarded by createBrand.unit.spec.ts. */
7
+
8
+ :root {
9
+ /* ── Backgrounds ── */
10
+ --color-bg-primary: var(--color-neutral-0);
11
+ --color-bg-secondary: var(--color-neutral-50);
12
+ --color-bg-surface: var(--color-neutral-100);
13
+ --color-bg-accent: var(--color-brand-50);
14
+ --color-bg-inverse: var(--color-neutral-900);
15
+
16
+ /* ── Text ── */
17
+ --color-text-primary: var(--color-neutral-900);
18
+ --color-text-secondary: var(--color-neutral-700); /* 10.34:1 on white (AAA) */
19
+ --color-text-muted: var(--color-neutral-600); /* 7.58:1 on white (AAA) */
20
+ --color-text-accent: var(--color-brand-700); /* 7.89:1 on white (AAA) */
21
+ --color-text-inverse: var(--color-neutral-50);
22
+ --color-text-on-accent: var(--color-neutral-0);
23
+
24
+ /* ── Borders ── */
25
+ --color-border-default: var(--color-neutral-200);
26
+ --color-border-subtle: var(--color-neutral-100);
27
+ --color-border-strong: var(--color-neutral-300);
28
+ --color-border-accent: var(--color-brand-300);
29
+
30
+ /* ── Status ── */
31
+ --color-status-success: #15803d;
32
+ --color-status-success-bg: #f0fdf4;
33
+ --color-status-error: #b91c1c;
34
+ --color-status-error-bg: #fef2f2;
35
+ --color-status-warning: #b45309;
36
+ --color-status-warning-bg: #fffbeb;
37
+ --color-status-info: var(--color-brand-500);
38
+ --color-status-info-bg: var(--color-brand-50);
39
+ }
40
+
41
+ /* ── Dark mode — system preference ── */
42
+ @media (prefers-color-scheme: dark) {
43
+ :root:not([data-theme='light']) {
44
+ --color-bg-primary: var(--color-neutral-900);
45
+ --color-bg-secondary: var(--color-neutral-800);
46
+ --color-bg-surface: var(--color-neutral-700);
47
+ --color-bg-accent: var(--color-brand-950);
48
+ --color-bg-inverse: var(--color-neutral-50);
49
+
50
+ --color-text-primary: var(--color-neutral-100);
51
+ --color-text-secondary: var(--color-neutral-300); /* 12.03:1 on neutral-900 (AAA) */
52
+ --color-text-muted: var(--color-neutral-400); /* 7.13:1 on neutral-900 (AAA, palette nudged) */
53
+ --color-text-accent: var(--color-brand-300); /* 8.97:1 on neutral-900 (AAA) */
54
+ --color-text-inverse: var(--color-neutral-900);
55
+ --color-text-on-accent: var(--color-neutral-0);
56
+
57
+ --color-border-default: var(--color-neutral-700);
58
+ --color-border-subtle: var(--color-neutral-800);
59
+ --color-border-strong: var(--color-neutral-600);
60
+ --color-border-accent: var(--color-brand-700);
61
+
62
+ --color-status-success: #4ade80;
63
+ --color-status-success-bg: #052e16;
64
+ --color-status-error: #f87171;
65
+ --color-status-error-bg: #450a0a;
66
+ --color-status-warning: #fbbf24;
67
+ --color-status-warning-bg: #451a03;
68
+ --color-status-info: var(--color-brand-300);
69
+ --color-status-info-bg: var(--color-brand-950);
70
+ }
71
+ }
72
+
73
+ /* ── Dark mode — explicit override ── */
74
+ [data-theme='dark'] {
75
+ --color-bg-primary: var(--color-neutral-900);
76
+ --color-bg-secondary: var(--color-neutral-800);
77
+ --color-bg-surface: var(--color-neutral-700);
78
+ --color-bg-accent: var(--color-brand-950);
79
+ --color-bg-inverse: var(--color-neutral-50);
80
+
81
+ --color-text-primary: var(--color-neutral-100);
82
+ --color-text-secondary: var(--color-neutral-300); /* 12.03:1 on neutral-900 (AAA) */
83
+ --color-text-muted: var(--color-neutral-400); /* 7.13:1 on neutral-900 (AAA, palette nudged) */
84
+ --color-text-accent: var(--color-brand-300); /* 8.97:1 on neutral-900 (AAA) */
85
+ --color-text-inverse: var(--color-neutral-900);
86
+ --color-text-on-accent: var(--color-neutral-0);
87
+
88
+ --color-border-default: var(--color-neutral-700);
89
+ --color-border-subtle: var(--color-neutral-800);
90
+ --color-border-strong: var(--color-neutral-600);
91
+ --color-border-accent: var(--color-brand-700);
92
+
93
+ --color-status-success: #4ade80;
94
+ --color-status-success-bg: #052e16;
95
+ --color-status-error: #f87171;
96
+ --color-status-error-bg: #450a0a;
97
+ --color-status-warning: #fbbf24;
98
+ --color-status-warning-bg: #451a03;
99
+ --color-status-info: var(--color-brand-300);
100
+ --color-status-info-bg: var(--color-brand-950);
101
+ }
102
+
103
+ /* ── Light mode — explicit override ── */
104
+ [data-theme='light'] {
105
+ --color-bg-primary: var(--color-neutral-0);
106
+ --color-bg-secondary: var(--color-neutral-50);
107
+ --color-bg-surface: var(--color-neutral-100);
108
+ --color-bg-accent: var(--color-brand-50);
109
+ --color-bg-inverse: var(--color-neutral-900);
110
+
111
+ --color-text-primary: var(--color-neutral-900);
112
+ --color-text-secondary: var(--color-neutral-700); /* 10.34:1 on white (AAA) */
113
+ --color-text-muted: var(--color-neutral-600); /* 7.58:1 on white (AAA) */
114
+ --color-text-accent: var(--color-brand-700); /* 7.89:1 on white (AAA) */
115
+ --color-text-inverse: var(--color-neutral-50);
116
+ --color-text-on-accent: var(--color-neutral-0);
117
+
118
+ --color-border-default: var(--color-neutral-200);
119
+ --color-border-subtle: var(--color-neutral-100);
120
+ --color-border-strong: var(--color-neutral-300);
121
+ --color-border-accent: var(--color-brand-300);
122
+
123
+ --color-status-success: #15803d;
124
+ --color-status-success-bg: #f0fdf4;
125
+ --color-status-error: #b91c1c;
126
+ --color-status-error-bg: #fef2f2;
127
+ --color-status-warning: #b45309;
128
+ --color-status-warning-bg: #fffbeb;
129
+ --color-status-info: var(--color-brand-500);
130
+ --color-status-info-bg: var(--color-brand-50);
131
+ }
132
+
133
+ /* ── Code block — always dark, theme-independent ── */
134
+ :root {
135
+ --code-bg: #0d1117;
136
+ --code-header-bg: #161b22;
137
+ --code-border: #21262d;
138
+ --code-text-dim: #8b949e;
139
+ --code-copy-bg: rgba(255, 255, 255, 0.05);
140
+ --code-copied-bg: rgba(74, 222, 128, 0.1);
141
+ --code-copied-color: #4ade80;
142
+ --code-copied-border: rgba(74, 222, 128, 0.3);
143
+ --code-border-dim: #30363d;
144
+
145
+ /* sugar-high syntax tokens (applied via inline style="color:var(--sh-*)") */
146
+ --sh-keyword: #c792ea;
147
+ --sh-string: #c3e88d;
148
+ --sh-comment: #546e7a;
149
+ --sh-jsxliterals: #89ddff;
150
+ --sh-identifier: #e2e8f0;
151
+ --sh-sign: #89ddff;
152
+ --sh-class: #ffcb6b;
153
+ --sh-property: #80cbc4;
154
+ --sh-entity: #82aaff;
155
+ --sh-space: inherit;
156
+
157
+ /* ── Overlay — dark translucent surfaces (Cover hero, image overlays) ── */
158
+ --color-overlay-gradient: rgba(0, 0, 0, 0.45);
159
+ --color-overlay-text: rgb(255 255 255 / 0.85);
160
+ --color-overlay-orb: rgb(255 255 255 / 0.05);
161
+ --color-overlay-orb-dim: rgb(255 255 255 / 0.03);
162
+ }
@@ -0,0 +1,5 @@
1
+ /* Default brand — independent fallback for consumers who provide no brand.
2
+ Provides palette + semantic tokens out of the box.
3
+ To create a custom brand: use createBrand() from @arun-dev/tokens/createBrand. */
4
+ @import './default/palette.css';
5
+ @import './default/semantic.css';
@@ -0,0 +1,293 @@
1
+ /**
2
+ * createBrand — build-time CSS generator for @arun-dev/tokens.
3
+ *
4
+ * Usage (full palette):
5
+ * import { createBrand } from '@arun-dev/tokens/createBrand';
6
+ * const css = createBrand({ name: 'acme', palette: { 50: '#...', ..., 950: '#...' } });
7
+ * writeFileSync('src/brands/acme.css', css);
8
+ *
9
+ * Usage (seed color — auto-generates palette):
10
+ * const css = createBrand({ name: 'acme', seed: '#7c3aed' });
11
+ *
12
+ * Consumer-level brand completion:
13
+ * A consumer may skip createBrand entirely and define semantic tokens directly in their globals.css.
14
+ * The only requirement: all semantic variables listed in BrandSemanticContract must be defined
15
+ * before @arun-dev/ui components are rendered.
16
+ */
17
+
18
+ // ─── Types ───────────────────────────────────────────────────────────────────
19
+
20
+ type Shade = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950;
21
+ type NeutralShade = 0 | Shade;
22
+
23
+ export type BrandPalette = Record<Shade, string>;
24
+ export type NeutralPalette = Record<NeutralShade, string>;
25
+
26
+ type WithPalette = { palette: BrandPalette };
27
+ type WithSeed = { seed: string };
28
+
29
+ export type CreateBrandInput = {
30
+ /** Brand identifier — used as a comment label in generated CSS */
31
+ name: string;
32
+ /** Optional neutral/gray palette. Defaults to cool-gray (slate). */
33
+ neutral?: NeutralPalette;
34
+ } & (WithPalette | WithSeed);
35
+
36
+ // ─── Default neutral (cool-gray / slate) ─────────────────────────────────────
37
+
38
+ export const DEFAULT_NEUTRAL: NeutralPalette = {
39
+ 0: '#ffffff',
40
+ 50: '#f8fafc',
41
+ 100: '#f1f5f9',
42
+ 200: '#e2e8f0',
43
+ 300: '#cbd5e1',
44
+ 400: '#96a5ba' /* nudged +2 per channel → 7.13:1 on neutral-900 (AAA) */,
45
+ 500: '#64748b',
46
+ 600: '#475569',
47
+ 700: '#334155',
48
+ 800: '#1e293b',
49
+ 900: '#0f172a',
50
+ 950: '#020617',
51
+ };
52
+
53
+ // ─── Color math (HSL, no external deps) ──────────────────────────────────────
54
+
55
+ function hexToRgb(hex: string): [number, number, number] {
56
+ const h = hex.replace('#', '');
57
+ return [
58
+ parseInt(h.slice(0, 2), 16) / 255,
59
+ parseInt(h.slice(2, 4), 16) / 255,
60
+ parseInt(h.slice(4, 6), 16) / 255,
61
+ ];
62
+ }
63
+
64
+ function rgbToHsl(r: number, g: number, b: number): [number, number, number] {
65
+ const max = Math.max(r, g, b);
66
+ const min = Math.min(r, g, b);
67
+ const l = (max + min) / 2;
68
+ if (max === min) return [0, 0, l * 100];
69
+ const d = max - min;
70
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
71
+ let h: number;
72
+ switch (max) {
73
+ case r:
74
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
75
+ break;
76
+ case g:
77
+ h = ((b - r) / d + 2) / 6;
78
+ break;
79
+ default:
80
+ h = ((r - g) / d + 4) / 6;
81
+ }
82
+ return [h * 360, s * 100, l * 100];
83
+ }
84
+
85
+ function hslToHex(h: number, s: number, l: number): string {
86
+ const sl = s / 100;
87
+ const ll = l / 100;
88
+ const a = sl * Math.min(ll, 1 - ll);
89
+ const f = (n: number): string => {
90
+ const k = (n + h / 30) % 12;
91
+ const color = ll - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
92
+ return Math.round(255 * color)
93
+ .toString(16)
94
+ .padStart(2, '0');
95
+ };
96
+ return `#${f(0)}${f(8)}${f(4)}`;
97
+ }
98
+
99
+ /** Lightness targets for each shade step */
100
+ const SHADE_LIGHTNESS: Record<Shade, number> = {
101
+ 50: 97,
102
+ 100: 93,
103
+ 200: 86,
104
+ 300: 75,
105
+ 400: 64,
106
+ 500: 53,
107
+ 600: 44,
108
+ 700: 36,
109
+ 800: 28,
110
+ 900: 20,
111
+ 950: 14,
112
+ };
113
+
114
+ /**
115
+ * Generate an 11-step palette from a seed hex color.
116
+ * Uses HSL color space — perceptually approximate but dependency-free.
117
+ */
118
+ export function generatePaletteFromSeed(seed: string): BrandPalette {
119
+ const [r, g, b] = hexToRgb(seed);
120
+ const [h, s] = rgbToHsl(r, g, b);
121
+
122
+ const entries = (Object.entries(SHADE_LIGHTNESS) as [string, number][]).map(
123
+ ([shade, targetL]) => {
124
+ const adjustedS = targetL > 80 ? s * 0.55 : targetL < 25 ? s * 0.75 : s;
125
+ return [Number(shade), hslToHex(h, Math.min(adjustedS, 100), targetL)];
126
+ },
127
+ );
128
+ return Object.fromEntries(entries) as BrandPalette;
129
+ }
130
+
131
+ // ─── Semantic derivation ──────────────────────────────────────────────────────
132
+
133
+ /**
134
+ * BrandSemanticContract — the full set of CSS variables @arun-dev/ui expects.
135
+ * Consumers who skip createBrand must define all of these in their own CSS.
136
+ */
137
+ export type BrandSemanticContract = {
138
+ '--color-bg-primary': string;
139
+ '--color-bg-secondary': string;
140
+ '--color-bg-surface': string;
141
+ '--color-bg-accent': string;
142
+ '--color-bg-inverse': string;
143
+ '--color-text-primary': string;
144
+ '--color-text-secondary': string;
145
+ '--color-text-muted': string;
146
+ '--color-text-accent': string;
147
+ '--color-text-inverse': string;
148
+ '--color-text-on-accent': string;
149
+ '--color-border-default': string;
150
+ '--color-border-subtle': string;
151
+ '--color-border-strong': string;
152
+ '--color-border-accent': string;
153
+ '--color-status-success': string;
154
+ '--color-status-success-bg': string;
155
+ '--color-status-error': string;
156
+ '--color-status-error-bg': string;
157
+ '--color-status-warning': string;
158
+ '--color-status-warning-bg': string;
159
+ '--color-status-info': string;
160
+ '--color-status-info-bg': string;
161
+ };
162
+
163
+ const STATUS_LIGHT: Pick<
164
+ BrandSemanticContract,
165
+ | '--color-status-success'
166
+ | '--color-status-success-bg'
167
+ | '--color-status-error'
168
+ | '--color-status-error-bg'
169
+ | '--color-status-warning'
170
+ | '--color-status-warning-bg'
171
+ > = {
172
+ '--color-status-success': '#15803d',
173
+ '--color-status-success-bg': '#f0fdf4',
174
+ '--color-status-error': '#b91c1c',
175
+ '--color-status-error-bg': '#fef2f2',
176
+ '--color-status-warning': '#b45309',
177
+ '--color-status-warning-bg': '#fffbeb',
178
+ };
179
+
180
+ const STATUS_DARK = {
181
+ '--color-status-success': '#4ade80',
182
+ '--color-status-success-bg': '#052e16',
183
+ '--color-status-error': '#f87171',
184
+ '--color-status-error-bg': '#450a0a',
185
+ '--color-status-warning': '#fbbf24',
186
+ '--color-status-warning-bg': '#451a03',
187
+ };
188
+
189
+ /* deriveLight/deriveDark must stay in sync with src/brands/default/semantic.css — the
190
+ hand-audited source of truth (AAA contrast). Guarded by createBrand.unit.spec.ts. */
191
+ function deriveLight(): Record<string, string> {
192
+ return {
193
+ '--color-bg-primary': 'var(--color-neutral-0)',
194
+ '--color-bg-secondary': 'var(--color-neutral-50)',
195
+ '--color-bg-surface': 'var(--color-neutral-100)',
196
+ '--color-bg-accent': 'var(--color-brand-50)',
197
+ '--color-bg-inverse': 'var(--color-neutral-900)',
198
+ '--color-text-primary': 'var(--color-neutral-900)',
199
+ '--color-text-secondary': 'var(--color-neutral-700)',
200
+ '--color-text-muted': 'var(--color-neutral-600)',
201
+ '--color-text-accent': 'var(--color-brand-700)',
202
+ '--color-text-inverse': 'var(--color-neutral-50)',
203
+ '--color-text-on-accent': 'var(--color-neutral-0)',
204
+ '--color-border-default': 'var(--color-neutral-200)',
205
+ '--color-border-subtle': 'var(--color-neutral-100)',
206
+ '--color-border-strong': 'var(--color-neutral-300)',
207
+ '--color-border-accent': 'var(--color-brand-300)',
208
+ ...STATUS_LIGHT,
209
+ '--color-status-info': 'var(--color-brand-500)',
210
+ '--color-status-info-bg': 'var(--color-brand-50)',
211
+ };
212
+ }
213
+
214
+ function deriveDark(): Record<string, string> {
215
+ return {
216
+ '--color-bg-primary': 'var(--color-neutral-900)',
217
+ '--color-bg-secondary': 'var(--color-neutral-800)',
218
+ '--color-bg-surface': 'var(--color-neutral-700)',
219
+ '--color-bg-accent': 'var(--color-brand-950)',
220
+ '--color-bg-inverse': 'var(--color-neutral-50)',
221
+ '--color-text-primary': 'var(--color-neutral-100)',
222
+ '--color-text-secondary': 'var(--color-neutral-300)',
223
+ '--color-text-muted': 'var(--color-neutral-400)',
224
+ '--color-text-accent': 'var(--color-brand-300)',
225
+ '--color-text-inverse': 'var(--color-neutral-900)',
226
+ '--color-text-on-accent': 'var(--color-neutral-0)',
227
+ '--color-border-default': 'var(--color-neutral-700)',
228
+ '--color-border-subtle': 'var(--color-neutral-800)',
229
+ '--color-border-strong': 'var(--color-neutral-600)',
230
+ '--color-border-accent': 'var(--color-brand-700)',
231
+ ...STATUS_DARK,
232
+ '--color-status-info': 'var(--color-brand-300)',
233
+ '--color-status-info-bg': 'var(--color-brand-950)',
234
+ };
235
+ }
236
+
237
+ // ─── CSS generation ───────────────────────────────────────────────────────────
238
+
239
+ function toVars(tokens: Record<string, string>, indent = ' '): string {
240
+ return Object.entries(tokens)
241
+ .map(([k, v]) => `${indent}${k}: ${v};`)
242
+ .join('\n');
243
+ }
244
+
245
+ function paletteVars(palette: BrandPalette): string {
246
+ return (Object.keys(SHADE_LIGHTNESS) as unknown as Shade[])
247
+ .map((shade) => ` --color-brand-${shade}: ${palette[shade]};`)
248
+ .join('\n');
249
+ }
250
+
251
+ function neutralVars(neutral: NeutralPalette): string {
252
+ return ([0, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as NeutralShade[])
253
+ .map((shade) => ` --color-neutral-${shade}: ${neutral[shade]};`)
254
+ .join('\n');
255
+ }
256
+
257
+ /**
258
+ * Generate a complete brand CSS string from a palette or seed color.
259
+ * Write the output to a .css file and import it in your globals.css.
260
+ */
261
+ export function createBrand(input: CreateBrandInput): string {
262
+ const { name, neutral = DEFAULT_NEUTRAL } = input;
263
+ const palette = 'seed' in input ? generatePaletteFromSeed(input.seed) : input.palette;
264
+
265
+ const light = deriveLight();
266
+ const dark = deriveDark();
267
+
268
+ return `/* ${name} brand — generated by createBrand (@arun-dev/tokens) */
269
+ /* Re-run createBrand to regenerate after palette changes. */
270
+
271
+ :root {
272
+ ${paletteVars(palette)}
273
+
274
+ ${neutralVars(neutral)}
275
+
276
+ ${toVars(light)}
277
+ }
278
+
279
+ @media (prefers-color-scheme: dark) {
280
+ :root:not([data-theme='light']) {
281
+ ${toVars(dark, ' ')}
282
+ }
283
+ }
284
+
285
+ [data-theme='dark'] {
286
+ ${toVars(dark)}
287
+ }
288
+
289
+ [data-theme='light'] {
290
+ ${toVars(light)}
291
+ }
292
+ `;
293
+ }
@@ -0,0 +1,9 @@
1
+ /* Documentation values — cannot be used inside @media conditions (CSS limitation).
2
+ Use the raw pixel values in @media queries and comment the matching token name. */
3
+ :root {
4
+ --breakpoint-sm: 640px;
5
+ --breakpoint-md: 768px;
6
+ --breakpoint-lg: 1024px;
7
+ --breakpoint-xl: 1280px;
8
+ --breakpoint-2xl: 1536px;
9
+ }
@@ -0,0 +1,10 @@
1
+ :root {
2
+ --z-base: 0;
3
+ --z-raised: 10;
4
+ --z-sticky: 50; /* sticky headers and floating navs — above page content, below modals */
5
+ --z-overlay: 100;
6
+ --z-modal: 200;
7
+ --z-toast: 300;
8
+ --z-tooltip: 400;
9
+ --z-skip: 999; /* skip-to-content link — must float above all UI layers */
10
+ }
@@ -0,0 +1,25 @@
1
+ /* Inter Variable Font — SIL Open Font License 1.1 (free for commercial use)
2
+ Source: @fontsource-variable/inter
3
+ Two files cover all weights (100–900) in both styles. */
4
+
5
+ @font-face {
6
+ font-family: 'Inter';
7
+ font-style: normal;
8
+ font-weight: 100 900;
9
+ font-display: swap;
10
+ src: url('../fonts/inter-latin-wght-normal.woff2') format('woff2');
11
+ unicode-range:
12
+ U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
13
+ U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
14
+ }
15
+
16
+ @font-face {
17
+ font-family: 'Inter';
18
+ font-style: italic;
19
+ font-weight: 100 900;
20
+ font-display: swap;
21
+ src: url('../fonts/inter-latin-wght-italic.woff2') format('woff2');
22
+ unicode-range:
23
+ U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
24
+ U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
25
+ }
@@ -0,0 +1,10 @@
1
+ :root {
2
+ --duration-instant: 0ms;
3
+ --duration-fast: 150ms;
4
+ --duration-normal: 250ms;
5
+ --duration-slow: 400ms;
6
+
7
+ --ease-smooth: cubic-bezier(0.4, 0, 0.2, 1);
8
+ --ease-in: cubic-bezier(0.4, 0, 1, 1);
9
+ --ease-out: cubic-bezier(0, 0, 0.2, 1);
10
+ }
@@ -0,0 +1,9 @@
1
+ :root {
2
+ --radius-none: 0;
3
+ --radius-sm: 0.25rem;
4
+ --radius-md: 0.375rem;
5
+ --radius-lg: 0.5rem;
6
+ --radius-xl: 0.75rem;
7
+ --radius-2xl: 1rem;
8
+ --radius-full: 9999px;
9
+ }
@@ -0,0 +1,12 @@
1
+ :root {
2
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
3
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
4
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
5
+ --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
6
+
7
+ /* Backdrop blur scale */
8
+ --blur-sm: 4px;
9
+ --blur-md: 8px;
10
+ --blur-lg: 12px;
11
+ --blur-xl: 16px;
12
+ }