@arun-dev/tokens 0.36.0 → 0.36.1

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.
@@ -1,277 +1,359 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/createBrand.ts
21
- var createBrand_exports = {};
22
- __export(createBrand_exports, {
23
- CONTRAST_REQUIREMENTS: () => CONTRAST_REQUIREMENTS,
24
- DEFAULT_NEUTRAL: () => DEFAULT_NEUTRAL,
25
- contrastRatio: () => contrastRatio,
26
- createBrand: () => createBrand,
27
- generatePaletteFromSeed: () => generatePaletteFromSeed
28
- });
29
- module.exports = __toCommonJS(createBrand_exports);
30
- var DEFAULT_NEUTRAL = {
31
- 0: "#ffffff",
32
- 50: "#f8fafc",
33
- 100: "#f1f5f9",
34
- 200: "#e2e8f0",
35
- 300: "#cbd5e1",
36
- 400: "#96a5ba",
37
- 500: "#64748b",
38
- 600: "#475569",
39
- 700: "#334155",
40
- 800: "#1e293b",
41
- 900: "#0f172a",
42
- 950: "#020617"
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/createBrand.ts
3
+ const DEFAULT_NEUTRAL = {
4
+ 0: "#ffffff",
5
+ 50: "#f8fafc",
6
+ 100: "#f1f5f9",
7
+ 200: "#e2e8f0",
8
+ 300: "#cbd5e1",
9
+ 400: "#96a5ba",
10
+ 500: "#64748b",
11
+ 600: "#475569",
12
+ 700: "#334155",
13
+ 800: "#1e293b",
14
+ 900: "#0f172a",
15
+ 950: "#020617"
43
16
  };
44
17
  function hexToRgb(hex) {
45
- const h = hex.replace("#", "");
46
- return [
47
- parseInt(h.slice(0, 2), 16) / 255,
48
- parseInt(h.slice(2, 4), 16) / 255,
49
- parseInt(h.slice(4, 6), 16) / 255
50
- ];
18
+ const h = hex.replace("#", "");
19
+ return [
20
+ parseInt(h.slice(0, 2), 16) / 255,
21
+ parseInt(h.slice(2, 4), 16) / 255,
22
+ parseInt(h.slice(4, 6), 16) / 255
23
+ ];
51
24
  }
52
25
  function rgbToHsl(r, g, b) {
53
- const max = Math.max(r, g, b);
54
- const min = Math.min(r, g, b);
55
- const l = (max + min) / 2;
56
- if (max === min) return [0, 0, l * 100];
57
- const d = max - min;
58
- const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
59
- let h;
60
- switch (max) {
61
- case r:
62
- h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
63
- break;
64
- case g:
65
- h = ((b - r) / d + 2) / 6;
66
- break;
67
- default:
68
- h = ((r - g) / d + 4) / 6;
69
- }
70
- return [h * 360, s * 100, l * 100];
26
+ const max = Math.max(r, g, b);
27
+ const min = Math.min(r, g, b);
28
+ const l = (max + min) / 2;
29
+ if (max === min) return [
30
+ 0,
31
+ 0,
32
+ l * 100
33
+ ];
34
+ const d = max - min;
35
+ const s = l > .5 ? d / (2 - max - min) : d / (max + min);
36
+ let h;
37
+ switch (max) {
38
+ case r:
39
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
40
+ break;
41
+ case g:
42
+ h = ((b - r) / d + 2) / 6;
43
+ break;
44
+ default: h = ((r - g) / d + 4) / 6;
45
+ }
46
+ return [
47
+ h * 360,
48
+ s * 100,
49
+ l * 100
50
+ ];
71
51
  }
72
52
  function hslToHex(h, s, l) {
73
- const sl = s / 100;
74
- const ll = l / 100;
75
- const a = sl * Math.min(ll, 1 - ll);
76
- const f = (n) => {
77
- const k = (n + h / 30) % 12;
78
- const color = ll - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
79
- return Math.round(255 * color).toString(16).padStart(2, "0");
80
- };
81
- return `#${f(0)}${f(8)}${f(4)}`;
53
+ const sl = s / 100;
54
+ const ll = l / 100;
55
+ const a = sl * Math.min(ll, 1 - ll);
56
+ const f = (n) => {
57
+ const k = (n + h / 30) % 12;
58
+ const color = ll - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
59
+ return Math.round(255 * color).toString(16).padStart(2, "0");
60
+ };
61
+ return `#${f(0)}${f(8)}${f(4)}`;
82
62
  }
63
+ /** WCAG 2 relative luminance of a #rrggbb colour. */
83
64
  function relativeLuminance(hex) {
84
- const [r, g, b] = hexToRgb(hex).map(
85
- (v) => v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4
86
- );
87
- return 0.2126 * r + 0.7152 * g + 0.0722 * b;
65
+ const [r, g, b] = hexToRgb(hex).map((v) => v <= .03928 ? v / 12.92 : ((v + .055) / 1.055) ** 2.4);
66
+ return .2126 * r + .7152 * g + .0722 * b;
88
67
  }
68
+ /** WCAG 2 contrast ratio between two #rrggbb colours, from 1 to 21. */
89
69
  function contrastRatio(a, b) {
90
- const lighter = Math.max(relativeLuminance(a), relativeLuminance(b));
91
- const darker = Math.min(relativeLuminance(a), relativeLuminance(b));
92
- return (lighter + 0.05) / (darker + 0.05);
70
+ const lighter = Math.max(relativeLuminance(a), relativeLuminance(b));
71
+ const darker = Math.min(relativeLuminance(a), relativeLuminance(b));
72
+ return (lighter + .05) / (darker + .05);
93
73
  }
94
- var SHADE_LIGHTNESS = {
95
- 50: 97,
96
- 100: 93,
97
- 200: 86,
98
- 300: 75,
99
- 400: 64,
100
- 500: 53,
101
- 600: 44,
102
- 700: 36,
103
- 800: 28,
104
- 900: 20,
105
- 950: 14
74
+ /** Lightness targets for each shade step */
75
+ const SHADE_LIGHTNESS = {
76
+ 50: 97,
77
+ 100: 93,
78
+ 200: 86,
79
+ 300: 75,
80
+ 400: 64,
81
+ 500: 53,
82
+ 600: 44,
83
+ 700: 36,
84
+ 800: 28,
85
+ 900: 20,
86
+ 950: 14
106
87
  };
88
+ /**
89
+ * Generate an 11-step palette from a seed hex color.
90
+ * Uses HSL color space — perceptually approximate but dependency-free.
91
+ */
107
92
  function generatePaletteFromSeed(seed) {
108
- const [r, g, b] = hexToRgb(seed);
109
- const [h, s] = rgbToHsl(r, g, b);
110
- const entries = Object.entries(SHADE_LIGHTNESS).map(
111
- ([shade, targetL]) => {
112
- const adjustedS = targetL > 80 ? s * 0.55 : targetL < 25 ? s * 0.75 : s;
113
- return [Number(shade), hslToHex(h, Math.min(adjustedS, 100), targetL)];
114
- }
115
- );
116
- return Object.fromEntries(entries);
93
+ const [r, g, b] = hexToRgb(seed);
94
+ const [h, s] = rgbToHsl(r, g, b);
95
+ const entries = Object.entries(SHADE_LIGHTNESS).map(([shade, targetL]) => {
96
+ const adjustedS = targetL > 80 ? s * .55 : targetL < 25 ? s * .75 : s;
97
+ return [Number(shade), hslToHex(h, Math.min(adjustedS, 100), targetL)];
98
+ });
99
+ return Object.fromEntries(entries);
117
100
  }
118
- var STATUS_LIGHT = {
119
- "--color-status-success": "#15803d",
120
- "--color-status-success-bg": "#f0fdf4",
121
- "--color-status-error": "#b91c1c",
122
- "--color-status-error-bg": "#fef2f2",
123
- "--color-status-warning": "#b45309",
124
- "--color-status-warning-bg": "#fffbeb"
101
+ const STATUS_LIGHT = {
102
+ "--color-status-success": "#15803d",
103
+ "--color-status-success-bg": "#f0fdf4",
104
+ "--color-status-error": "#b91c1c",
105
+ "--color-status-error-bg": "#fef2f2",
106
+ "--color-status-warning": "#b45309",
107
+ "--color-status-warning-bg": "#fffbeb"
125
108
  };
126
- var STATUS_DARK = {
127
- "--color-status-success": "#4ade80",
128
- "--color-status-success-bg": "#052e16",
129
- "--color-status-error": "#f87171",
130
- "--color-status-error-bg": "#450a0a",
131
- "--color-status-warning": "#fbbf24",
132
- "--color-status-warning-bg": "#451a03"
109
+ const STATUS_DARK = {
110
+ "--color-status-success": "#4ade80",
111
+ "--color-status-success-bg": "#052e16",
112
+ "--color-status-error": "#f87171",
113
+ "--color-status-error-bg": "#450a0a",
114
+ "--color-status-warning": "#fbbf24",
115
+ "--color-status-warning-bg": "#451a03"
133
116
  };
134
117
  function deriveLight() {
135
- return {
136
- "--color-bg-primary": "var(--color-neutral-0)",
137
- "--color-bg-secondary": "var(--color-neutral-50)",
138
- "--color-bg-surface": "var(--color-neutral-100)",
139
- "--color-bg-accent": "var(--color-brand-50)",
140
- "--color-bg-inverse": "var(--color-neutral-900)",
141
- "--color-text-primary": "var(--color-neutral-900)",
142
- "--color-text-secondary": "var(--color-neutral-700)",
143
- "--color-text-muted": "var(--color-neutral-600)",
144
- "--color-text-accent": "var(--color-brand-700)",
145
- "--color-text-inverse": "var(--color-neutral-50)",
146
- "--color-text-on-accent": "var(--color-neutral-0)",
147
- "--color-border-default": "var(--color-neutral-200)",
148
- "--color-border-subtle": "var(--color-neutral-100)",
149
- "--color-border-strong": "var(--color-neutral-300)",
150
- "--color-border-accent": "var(--color-brand-300)",
151
- ...STATUS_LIGHT,
152
- // 600, not 500: the info Badge's text sits on brand-50, and 500 there was 3.99:1.
153
- "--color-status-info": "var(--color-brand-600)",
154
- "--color-status-info-bg": "var(--color-brand-50)"
155
- };
118
+ return {
119
+ "--color-bg-primary": "var(--color-neutral-0)",
120
+ "--color-bg-secondary": "var(--color-neutral-50)",
121
+ "--color-bg-surface": "var(--color-neutral-100)",
122
+ "--color-bg-accent": "var(--color-brand-50)",
123
+ "--color-bg-inverse": "var(--color-neutral-900)",
124
+ "--color-text-primary": "var(--color-neutral-900)",
125
+ "--color-text-secondary": "var(--color-neutral-700)",
126
+ "--color-text-muted": "var(--color-neutral-600)",
127
+ "--color-text-accent": "var(--color-brand-700)",
128
+ "--color-text-inverse": "var(--color-neutral-50)",
129
+ "--color-text-on-accent": "var(--color-neutral-0)",
130
+ "--color-border-default": "var(--color-neutral-200)",
131
+ "--color-border-subtle": "var(--color-neutral-100)",
132
+ "--color-border-strong": "var(--color-neutral-300)",
133
+ "--color-border-accent": "var(--color-brand-300)",
134
+ ...STATUS_LIGHT,
135
+ "--color-status-info": "var(--color-brand-600)",
136
+ "--color-status-info-bg": "var(--color-brand-50)"
137
+ };
156
138
  }
157
139
  function deriveDark() {
158
- return {
159
- "--color-bg-primary": "var(--color-neutral-900)",
160
- "--color-bg-secondary": "var(--color-neutral-800)",
161
- "--color-bg-surface": "var(--color-neutral-700)",
162
- "--color-bg-accent": "var(--color-brand-950)",
163
- "--color-bg-inverse": "var(--color-neutral-50)",
164
- "--color-text-primary": "var(--color-neutral-100)",
165
- "--color-text-secondary": "var(--color-neutral-300)",
166
- "--color-text-muted": "var(--color-neutral-400)",
167
- "--color-text-accent": "var(--color-brand-300)",
168
- "--color-text-inverse": "var(--color-neutral-900)",
169
- // The accent is light in dark mode, so the text on it is dark.
170
- "--color-text-on-accent": "var(--color-neutral-950)",
171
- "--color-border-default": "var(--color-neutral-700)",
172
- "--color-border-subtle": "var(--color-neutral-800)",
173
- "--color-border-strong": "var(--color-neutral-600)",
174
- "--color-border-accent": "var(--color-brand-700)",
175
- ...STATUS_DARK,
176
- "--color-status-info": "var(--color-brand-300)",
177
- "--color-status-info-bg": "var(--color-brand-950)"
178
- };
140
+ return {
141
+ "--color-bg-primary": "var(--color-neutral-900)",
142
+ "--color-bg-secondary": "var(--color-neutral-800)",
143
+ "--color-bg-surface": "var(--color-neutral-700)",
144
+ "--color-bg-accent": "var(--color-brand-950)",
145
+ "--color-bg-inverse": "var(--color-neutral-50)",
146
+ "--color-text-primary": "var(--color-neutral-100)",
147
+ "--color-text-secondary": "var(--color-neutral-300)",
148
+ "--color-text-muted": "var(--color-neutral-400)",
149
+ "--color-text-accent": "var(--color-brand-300)",
150
+ "--color-text-inverse": "var(--color-neutral-900)",
151
+ "--color-text-on-accent": "var(--color-neutral-950)",
152
+ "--color-border-default": "var(--color-neutral-700)",
153
+ "--color-border-subtle": "var(--color-neutral-800)",
154
+ "--color-border-strong": "var(--color-neutral-600)",
155
+ "--color-border-accent": "var(--color-brand-700)",
156
+ ...STATUS_DARK,
157
+ "--color-status-info": "var(--color-brand-300)",
158
+ "--color-status-info-bg": "var(--color-brand-950)"
159
+ };
179
160
  }
180
- var CONTRAST_REQUIREMENTS = [
181
- // Body text on every page surface.
182
- { foreground: "--color-text-primary", background: "--color-bg-primary", min: 7 },
183
- { foreground: "--color-text-primary", background: "--color-bg-secondary", min: 7 },
184
- { foreground: "--color-text-primary", background: "--color-bg-surface", min: 4.5 },
185
- { foreground: "--color-text-secondary", background: "--color-bg-primary", min: 7 },
186
- { foreground: "--color-text-muted", background: "--color-bg-primary", min: 4.5 },
187
- { foreground: "--color-text-inverse", background: "--color-bg-inverse", min: 7 },
188
- // A selected row: a TreeView Item's text on its tint.
189
- { foreground: "--color-text-primary", background: "--color-bg-accent", min: 7 },
190
- // The accent as text: links, accent chips, focus outlines.
191
- { foreground: "--color-text-accent", background: "--color-bg-primary", min: 7 },
192
- { foreground: "--color-text-accent", background: "--color-bg-secondary", min: 4.5 },
193
- { foreground: "--color-text-accent", background: "--color-bg-surface", min: 4.5 },
194
- { foreground: "--color-text-accent", background: "--color-bg-accent", min: 4.5 },
195
- // The accent as a fill: primary Button, checked Checkbox and Radio, the current page.
196
- { foreground: "--color-text-on-accent", background: "--color-text-accent", min: 7 },
197
- // Status tones: a Badge's text on its tint, an Alert's or Toast's accent on the page.
198
- { foreground: "--color-status-success", background: "--color-status-success-bg", min: 4.5 },
199
- { foreground: "--color-status-error", background: "--color-status-error-bg", min: 4.5 },
200
- { foreground: "--color-status-warning", background: "--color-status-warning-bg", min: 4.5 },
201
- { foreground: "--color-status-info", background: "--color-status-info-bg", min: 4.5 },
202
- { foreground: "--color-status-info", background: "--color-bg-primary", min: 3 },
203
- // Calendar day tags: a holiday's number as text, the other marks as shapes on the page.
204
- { foreground: "--color-status-error", background: "--color-bg-primary", min: 4.5 },
205
- { foreground: "--color-status-warning", background: "--color-bg-primary", min: 3 },
206
- { foreground: "--color-status-success", background: "--color-bg-primary", min: 3 }
161
+ /**
162
+ * Every pairing of semantic tokens that @arun-dev/ui draws one on the other, with the WCAG
163
+ * contrast it needs: 7 where the default brand is audited AAA, 4.5 for other text, 3 for
164
+ * icons, accent bars and focus outlines. A brand that skips createBrand should meet these too.
165
+ */
166
+ const CONTRAST_REQUIREMENTS = [
167
+ {
168
+ foreground: "--color-text-primary",
169
+ background: "--color-bg-primary",
170
+ min: 7
171
+ },
172
+ {
173
+ foreground: "--color-text-primary",
174
+ background: "--color-bg-secondary",
175
+ min: 7
176
+ },
177
+ {
178
+ foreground: "--color-text-primary",
179
+ background: "--color-bg-surface",
180
+ min: 4.5
181
+ },
182
+ {
183
+ foreground: "--color-text-secondary",
184
+ background: "--color-bg-primary",
185
+ min: 7
186
+ },
187
+ {
188
+ foreground: "--color-text-muted",
189
+ background: "--color-bg-primary",
190
+ min: 4.5
191
+ },
192
+ {
193
+ foreground: "--color-text-inverse",
194
+ background: "--color-bg-inverse",
195
+ min: 7
196
+ },
197
+ {
198
+ foreground: "--color-text-primary",
199
+ background: "--color-bg-accent",
200
+ min: 7
201
+ },
202
+ {
203
+ foreground: "--color-text-accent",
204
+ background: "--color-bg-primary",
205
+ min: 7
206
+ },
207
+ {
208
+ foreground: "--color-text-accent",
209
+ background: "--color-bg-secondary",
210
+ min: 4.5
211
+ },
212
+ {
213
+ foreground: "--color-text-accent",
214
+ background: "--color-bg-surface",
215
+ min: 4.5
216
+ },
217
+ {
218
+ foreground: "--color-text-accent",
219
+ background: "--color-bg-accent",
220
+ min: 4.5
221
+ },
222
+ {
223
+ foreground: "--color-text-on-accent",
224
+ background: "--color-text-accent",
225
+ min: 7
226
+ },
227
+ {
228
+ foreground: "--color-status-success",
229
+ background: "--color-status-success-bg",
230
+ min: 4.5
231
+ },
232
+ {
233
+ foreground: "--color-status-error",
234
+ background: "--color-status-error-bg",
235
+ min: 4.5
236
+ },
237
+ {
238
+ foreground: "--color-status-warning",
239
+ background: "--color-status-warning-bg",
240
+ min: 4.5
241
+ },
242
+ {
243
+ foreground: "--color-status-info",
244
+ background: "--color-status-info-bg",
245
+ min: 4.5
246
+ },
247
+ {
248
+ foreground: "--color-status-info",
249
+ background: "--color-bg-primary",
250
+ min: 3
251
+ },
252
+ {
253
+ foreground: "--color-status-error",
254
+ background: "--color-bg-primary",
255
+ min: 4.5
256
+ },
257
+ {
258
+ foreground: "--color-status-warning",
259
+ background: "--color-bg-primary",
260
+ min: 3
261
+ },
262
+ {
263
+ foreground: "--color-status-success",
264
+ background: "--color-bg-primary",
265
+ min: 3
266
+ }
207
267
  ];
208
- var BRAND_VAR = /^var\(--color-brand-(\d+)\)$/;
209
- var NEUTRAL_VAR = /^var\(--color-neutral-(\d+)\)$/;
268
+ const BRAND_VAR = /^var\(--color-brand-(\d+)\)$/;
269
+ const NEUTRAL_VAR = /^var\(--color-neutral-(\d+)\)$/;
270
+ /** The colour a semantic value stands for: a brand shade, a neutral shade or a literal. */
210
271
  function resolveColor(value, palette, neutral) {
211
- const brand = BRAND_VAR.exec(value);
212
- if (brand) return palette[Number(brand[1])];
213
- const gray = NEUTRAL_VAR.exec(value);
214
- if (gray) return neutral[Number(gray[1])];
215
- return value;
272
+ const brand = BRAND_VAR.exec(value);
273
+ if (brand) return palette[Number(brand[1])];
274
+ const gray = NEUTRAL_VAR.exec(value);
275
+ if (gray) return neutral[Number(gray[1])];
276
+ return value;
216
277
  }
278
+ /** The same colour, lighter or darker by `step` HSL points; its hue and saturation kept. */
217
279
  function shiftLightness(hex, step) {
218
- const [h, s, l] = rgbToHsl(...hexToRgb(hex));
219
- return hslToHex(h, s, Math.min(100, Math.max(0, l + step)));
280
+ const [h, s, l] = rgbToHsl(...hexToRgb(hex));
281
+ return hslToHex(h, s, Math.min(100, Math.max(0, l + step)));
220
282
  }
221
- var SHADES = Object.keys(SHADE_LIGHTNESS).map(Number);
283
+ const SHADES = Object.keys(SHADE_LIGHTNESS).map(Number);
284
+ /**
285
+ * Moves the shades of a seeded palette until every pairing in CONTRAST_REQUIREMENTS passes in
286
+ * both themes. HSL lightness is not how light a colour looks — a yellow at the 700 step is
287
+ * still bright — so fixed steps alone cannot promise contrast.
288
+ *
289
+ * In each failing pair the brand shade moves, away from the colour it sits on or under: the
290
+ * foreground's when both are brand shades. Hue and saturation stay, so the brand still reads
291
+ * as its seed. The scale is then kept in order, light to dark, by moving its neighbours on.
292
+ */
222
293
  function fitContrast(palette, neutral) {
223
- const fitted = { ...palette };
224
- const themes = [deriveLight(), deriveDark()];
225
- for (let pass = 0; pass < 4; pass++) {
226
- let moved = false;
227
- for (const theme of themes) {
228
- for (const { foreground, background, min } of CONTRAST_REQUIREMENTS) {
229
- const fgValue = theme[foreground] ?? "";
230
- const bgValue = theme[background] ?? "";
231
- const fgShade = BRAND_VAR.exec(fgValue)?.[1];
232
- const bgShade = BRAND_VAR.exec(bgValue)?.[1];
233
- const shade = Number(fgShade ?? bgShade);
234
- if (!fgShade && !bgShade) continue;
235
- const other = resolveColor(fgShade ? bgValue : fgValue, fitted, neutral);
236
- const step = relativeLuminance(other) > 0.18 ? -1 : 1;
237
- for (let i = 0; i < 100 && contrastRatio(fitted[shade], other) < min; i++) {
238
- fitted[shade] = shiftLightness(fitted[shade], step);
239
- moved = true;
240
- }
241
- }
242
- }
243
- const middle = SHADES.indexOf(500);
244
- for (let i = middle + 1; i < SHADES.length; i++) {
245
- const [before, shade] = [SHADES[i - 1], SHADES[i]];
246
- for (let n = 0; n < 100 && relativeLuminance(fitted[shade]) >= relativeLuminance(fitted[before]); n++) {
247
- fitted[shade] = shiftLightness(fitted[shade], -1);
248
- }
249
- }
250
- for (let i = middle - 1; i >= 0; i--) {
251
- const [after, shade] = [SHADES[i + 1], SHADES[i]];
252
- for (let n = 0; n < 100 && relativeLuminance(fitted[shade]) <= relativeLuminance(fitted[after]); n++) {
253
- fitted[shade] = shiftLightness(fitted[shade], 1);
254
- }
255
- }
256
- if (!moved) break;
257
- }
258
- return fitted;
294
+ const fitted = { ...palette };
295
+ const themes = [deriveLight(), deriveDark()];
296
+ for (let pass = 0; pass < 4; pass++) {
297
+ let moved = false;
298
+ for (const theme of themes) for (const { foreground, background, min } of CONTRAST_REQUIREMENTS) {
299
+ const fgValue = theme[foreground] ?? "";
300
+ const bgValue = theme[background] ?? "";
301
+ const fgShade = BRAND_VAR.exec(fgValue)?.[1];
302
+ const bgShade = BRAND_VAR.exec(bgValue)?.[1];
303
+ const shade = Number(fgShade ?? bgShade);
304
+ if (!fgShade && !bgShade) continue;
305
+ const other = resolveColor(fgShade ? bgValue : fgValue, fitted, neutral);
306
+ const step = relativeLuminance(other) > .18 ? -1 : 1;
307
+ for (let i = 0; i < 100 && contrastRatio(fitted[shade], other) < min; i++) {
308
+ fitted[shade] = shiftLightness(fitted[shade], step);
309
+ moved = true;
310
+ }
311
+ }
312
+ const middle = SHADES.indexOf(500);
313
+ for (let i = middle + 1; i < SHADES.length; i++) {
314
+ const [before, shade] = [SHADES[i - 1], SHADES[i]];
315
+ for (let n = 0; n < 100 && relativeLuminance(fitted[shade]) >= relativeLuminance(fitted[before]); n++) fitted[shade] = shiftLightness(fitted[shade], -1);
316
+ }
317
+ for (let i = middle - 1; i >= 0; i--) {
318
+ const [after, shade] = [SHADES[i + 1], SHADES[i]];
319
+ for (let n = 0; n < 100 && relativeLuminance(fitted[shade]) <= relativeLuminance(fitted[after]); n++) fitted[shade] = shiftLightness(fitted[shade], 1);
320
+ }
321
+ if (!moved) break;
322
+ }
323
+ return fitted;
259
324
  }
260
325
  function toVars(tokens, indent = " ") {
261
- return Object.entries(tokens).map(([k, v]) => `${indent}${k}: ${v};`).join("\n");
326
+ return Object.entries(tokens).map(([k, v]) => `${indent}${k}: ${v};`).join("\n");
262
327
  }
263
328
  function paletteVars(palette) {
264
- return Object.keys(SHADE_LIGHTNESS).map((shade) => ` --color-brand-${shade}: ${palette[shade]};`).join("\n");
329
+ return Object.keys(SHADE_LIGHTNESS).map((shade) => ` --color-brand-${shade}: ${palette[shade]};`).join("\n");
265
330
  }
266
331
  function neutralVars(neutral) {
267
- return [0, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950].map((shade) => ` --color-neutral-${shade}: ${neutral[shade]};`).join("\n");
332
+ return [
333
+ 0,
334
+ 50,
335
+ 100,
336
+ 200,
337
+ 300,
338
+ 400,
339
+ 500,
340
+ 600,
341
+ 700,
342
+ 800,
343
+ 900,
344
+ 950
345
+ ].map((shade) => ` --color-neutral-${shade}: ${neutral[shade]};`).join("\n");
268
346
  }
347
+ /**
348
+ * Generate a complete brand CSS string from a palette or seed color.
349
+ * Write the output to a .css file and import it in your globals.css.
350
+ */
269
351
  function createBrand(input) {
270
- const { name, neutral = DEFAULT_NEUTRAL } = input;
271
- const palette = "seed" in input ? fitContrast(generatePaletteFromSeed(input.seed), neutral) : input.palette;
272
- const light = deriveLight();
273
- const dark = deriveDark();
274
- return `/* ${name} brand \u2014 generated by createBrand (@arun-dev/tokens) */
352
+ const { name, neutral = DEFAULT_NEUTRAL } = input;
353
+ const palette = "seed" in input ? fitContrast(generatePaletteFromSeed(input.seed), neutral) : input.palette;
354
+ const light = deriveLight();
355
+ const dark = deriveDark();
356
+ return `/* ${name} brand — generated by createBrand (@arun-dev/tokens) */
275
357
  /* Re-run createBrand to regenerate after palette changes. */
276
358
 
277
359
  :root {
@@ -297,11 +379,9 @@ ${toVars(light)}
297
379
  }
298
380
  `;
299
381
  }
300
- // Annotate the CommonJS export names for ESM import in node:
301
- 0 && (module.exports = {
302
- CONTRAST_REQUIREMENTS,
303
- DEFAULT_NEUTRAL,
304
- contrastRatio,
305
- createBrand,
306
- generatePaletteFromSeed
307
- });
382
+ //#endregion
383
+ exports.CONTRAST_REQUIREMENTS = CONTRAST_REQUIREMENTS;
384
+ exports.DEFAULT_NEUTRAL = DEFAULT_NEUTRAL;
385
+ exports.contrastRatio = contrastRatio;
386
+ exports.createBrand = createBrand;
387
+ exports.generatePaletteFromSeed = generatePaletteFromSeed;