@eos-ui/tokens 0.0.0-stage → 0.1.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.
package/dist/theme.js ADDED
@@ -0,0 +1,278 @@
1
+ import { defaults, groups } from "./generated/defaults.js";
2
+ import { contrast, fromOklch, luminance, parseHex, reachContrast, settleContrast, tint, toOklch, withLightness, } from "./color.js";
3
+ /** Cascade order of the library. Later layers win, and each layer only builds on the ones before it. */
4
+ export const LAYER_ORDER = "e.reset, e.tokens, e.base, e.primitives, e.blocks, e.controls, e.composites, e.containers, e.account, e.page";
5
+ const RADII = {
6
+ sharp: { sm: "0px", md: "2px", lg: "4px" },
7
+ default: { sm: "4px", md: "8px", lg: "16px" },
8
+ round: { sm: "8px", md: "14px", lg: "24px" },
9
+ };
10
+ const PAIRS = [
11
+ { fg: "ink", bg: "surface", min: 7, role: "Body text" },
12
+ { fg: "ink", bg: "surface-tint", min: 7, role: "Text on the tinted ground" },
13
+ { fg: "muted", bg: "surface", min: 4.5, role: "Secondary text" },
14
+ { fg: "on-primary", bg: "primary", min: 4.5, role: "Primary button label" },
15
+ { fg: "primary", bg: "surface", min: 4.5, role: "Links and small primary text" },
16
+ { fg: "on-secondary", bg: "secondary", min: 4.5, role: "Secondary button label" },
17
+ { fg: "on-danger", bg: "danger", min: 4.5, role: "Danger button label" },
18
+ { fg: "danger", bg: "surface", min: 4.5, role: "Error text" },
19
+ { fg: "danger", bg: "danger-soft", min: 4.5, role: "Danger notice text" },
20
+ { fg: "warning-strong", bg: "warning-soft", min: 4.5, role: "Warning notice text" },
21
+ { fg: "secondary", bg: "secondary-soft", min: 4.5, role: "Success notice text" },
22
+ { fg: "border", bg: "surface", min: 3, role: "Card outline (non-text)" },
23
+ { fg: "border-strong", bg: "surface", min: 3, role: "Control edge (non-text)" },
24
+ { fg: "ink", bg: "background", min: 7, role: "Text on the page" },
25
+ { fg: "muted", bg: "background", min: 4.5, role: "Secondary text on the page" },
26
+ { fg: "primary", bg: "background", min: 4.5, role: "Links on the page" },
27
+ { fg: "border", bg: "background", min: 3, role: "Outline on the page (non-text)" },
28
+ ];
29
+ const DARK_BELOW = 0.179;
30
+ const BLACK = "#000000";
31
+ const WHITE = "#ffffff";
32
+ const DARK_DANGER = "#d33b30";
33
+ const DARK_WARNING = "#ffc857";
34
+ const DARK_WELL = "inset 0 2px 0 0 rgba(0, 0, 0, 0.55)";
35
+ const MOTION = {
36
+ default: {},
37
+ snappy: { "duration-tap": "50ms", "duration-move": "140ms" },
38
+ gentle: { "duration-tap": "140ms", "duration-move": "380ms", "easing-out": "cubic-bezier(0.4, 0, 0.2, 1)" },
39
+ none: { "duration-tap": "0ms", "duration-move": "0ms" },
40
+ };
41
+ const positive = (n) => typeof n === "number" && Number.isFinite(n) && n > 0 ? n : undefined;
42
+ const px = (n) => `${Math.round(n * 100) / 100}px`;
43
+ function scaleTokens(out, names, factor) {
44
+ const f = positive(factor);
45
+ if (f === undefined || f === 1)
46
+ return;
47
+ for (const name of names)
48
+ out[name] = px(Math.max(1, Math.round(parseFloat(defaults[name]) * f)));
49
+ }
50
+ function toneHue(soft, strong, tone) {
51
+ const hue = toOklch(defaults[soft]).h;
52
+ return tone ? hue + toOklch(tone).h - toOklch(defaults[strong]).h : hue;
53
+ }
54
+ function deriveStrokes(input, out) {
55
+ const hair = positive(input.strokes?.hair);
56
+ const focus = positive(input.strokes?.focus);
57
+ if (hair !== undefined) {
58
+ out["stroke-hair"] = px(hair);
59
+ out["stroke-card"] = px(hair);
60
+ out["stroke-edge"] = px(hair * 1.5);
61
+ }
62
+ if (focus !== undefined) {
63
+ out["stroke-focus"] = px(focus);
64
+ out["focus-gap"] = px(focus);
65
+ out["focus-gap-field"] = "-1px";
66
+ }
67
+ }
68
+ const SOFT = [
69
+ [1, 2, 0, 0.08],
70
+ [4, 8, -2, 0.1],
71
+ [12, 24, -6, 0.14],
72
+ [24, 48, -12, 0.18],
73
+ ];
74
+ const LIFTED = [
75
+ [1, 2, 0, 0.1],
76
+ [6, 12, -3, 0.12],
77
+ [18, 36, -8, 0.18],
78
+ [32, 64, -16, 0.2],
79
+ ];
80
+ const CARD = [
81
+ [1, 2, 0, 0.12],
82
+ [4, 8, -2, 0.08],
83
+ ];
84
+ function shadowRgb(out, dark) {
85
+ const ground = toOklch(out["background"]);
86
+ const [r, g, b] = parseHex(fromOklch({ l: dark ? 0.07 : 0.2613, c: Math.min(ground.c, dark ? 0.03 : 0.0408), h: ground.h }));
87
+ return `${r}, ${g}, ${b}`;
88
+ }
89
+ function layers(set, dark, base) {
90
+ return set
91
+ .map(([y, blur, spread, alpha]) => `0 ${y}px ${blur}px ${spread}px rgba(${base}, ${Math.round(Math.min(0.9, dark ? alpha * 2.4 : alpha) * 100) / 100})`)
92
+ .join(", ");
93
+ }
94
+ function deriveElevation(style, out, dark) {
95
+ const ring = (w) => `0 0 0 ${w} ${out["border"]}`;
96
+ const hair = out["stroke-hair"];
97
+ const card = style === "bold" ? "2px" : hair;
98
+ out["stroke-card"] = card;
99
+ const base = shadowRgb(out, dark);
100
+ if (style === "lifted") {
101
+ out["edge"] = `${ring(card)}, ${layers(CARD, dark, base)}`;
102
+ out["float"] = `${ring(card)}, ${layers(LIFTED, dark, base)}`;
103
+ }
104
+ else if (style === "bold") {
105
+ out["edge"] = ring(card);
106
+ out["well"] = `inset 0 3px 0 0 ${dark ? "rgba(0, 0, 0, 0.55)" : "rgba(0, 0, 0, 0.14)"}`;
107
+ out["float"] = `${ring(card)}, 4px 4px 0 0 rgba(${base}, ${dark ? 0.75 : 0.32}), 0 14px 28px -10px rgba(${base}, ${dark ? 0.7 : 0.22})`;
108
+ }
109
+ else {
110
+ out["edge"] = ring(card);
111
+ out["float"] = `${ring(card)}, ${layers(SOFT, dark, base)}`;
112
+ }
113
+ }
114
+ function ensure(out, token, against, min) {
115
+ const value = out[token];
116
+ if (contrast(value, against) < min)
117
+ out[token] = reachContrast(value, against, min);
118
+ }
119
+ function deriveGround(input, out, dark) {
120
+ const surface = input.surface ?? out["surface"];
121
+ if (input.surface) {
122
+ out["surface"] = input.surface;
123
+ const hue = toOklch(input.brand ?? out["brand-blue"]).h;
124
+ const l = toOklch(input.surface).l;
125
+ out["surface-tint"] = fromOklch({ l: Math.min(1, Math.max(0, dark ? l + 0.06 : l - 0.045)), c: 0.025, h: hue });
126
+ for (const [token, hueSource] of [
127
+ ["secondary-soft", toOklch(input.accent ?? defaults["secondary-soft"]).h],
128
+ ["danger-soft", toneHue("danger-soft", "danger", input.danger)],
129
+ ["warning-soft", toneHue("warning-soft", "warning-strong", input.warning)],
130
+ ]) {
131
+ const base = toOklch(defaults[token]);
132
+ const chroma = dark ? 0.05 : token === "secondary-soft" && input.accent ? 0.04 : base.c;
133
+ const offset = token === "secondary-soft" && input.accent ? -0.04 : base.l - 1;
134
+ out[token] = withLightness(surface, dark ? l + 0.07 : l + offset, chroma, hueSource);
135
+ }
136
+ }
137
+ out["background"] = input.background ?? (input.surface ? surface : out["background"]);
138
+ const foreground = input.foreground ?? (input.surface ? (dark ? WHITE : defaults["ink"]) : undefined);
139
+ if (foreground) {
140
+ out["ink"] = foreground;
141
+ out["muted"] = settleContrast(foreground, surface, 7);
142
+ out["border"] = settleContrast(foreground, surface, 3.4);
143
+ out["border-strong"] = settleContrast(foreground, surface, 4.5);
144
+ }
145
+ }
146
+ function derive(input) {
147
+ const out = { ...defaults };
148
+ const dark = input.surface !== undefined && luminance(input.surface) < DARK_BELOW;
149
+ if (dark) {
150
+ for (const t of ["on-primary", "on-secondary", "on-danger"])
151
+ out[t] = BLACK;
152
+ out["warning-strong"] = input.warning ?? DARK_WARNING;
153
+ out["scrim"] = "rgb(0 0 0 / 0.7)";
154
+ out["well"] = DARK_WELL;
155
+ }
156
+ const dangerBase = input.danger ?? (dark ? DARK_DANGER : undefined);
157
+ if (dangerBase)
158
+ out["danger"] = reachContrast(dangerBase, out["on-danger"], 7);
159
+ if (input.brand)
160
+ out["brand-blue"] = input.brand;
161
+ if (input.brand || dark)
162
+ out["primary"] = reachContrast(out["brand-blue"], out["on-primary"], 7);
163
+ if (input.accent)
164
+ out["brand-green"] = input.accent;
165
+ if (input.accent || dark)
166
+ out["secondary"] = reachContrast(out["brand-green"], out["on-secondary"], 7);
167
+ if (input.brand && !input.surface)
168
+ out["surface-tint"] = tint(input.brand);
169
+ if (input.accent && !input.surface)
170
+ out["secondary-soft"] = tint(input.accent, 0.96, 0.04);
171
+ deriveGround(input, out, dark);
172
+ if (!input.surface) {
173
+ for (const [token, tone, strong] of [
174
+ ["danger-soft", input.danger, "danger"],
175
+ ["warning-soft", input.warning, "warning-strong"],
176
+ ]) {
177
+ const hue = toneHue(token, strong, tone);
178
+ const base = toOklch(defaults[token]);
179
+ if (tone && hue !== base.h)
180
+ out[token] = withLightness(defaults[token], base.l, base.c, hue);
181
+ }
182
+ }
183
+ if (input.warning && !dark)
184
+ out["warning-strong"] = reachContrast(input.warning, out["surface"], 7);
185
+ const surface = out["surface"];
186
+ if (input.surface) {
187
+ ensure(out, "primary", surface, 4.5);
188
+ ensure(out, "secondary", surface, 4.5);
189
+ ensure(out, "secondary", out["secondary-soft"], 4.5);
190
+ }
191
+ if (input.surface || input.danger) {
192
+ ensure(out, "danger", surface, 4.5);
193
+ ensure(out, "danger", out["danger-soft"], 4.5);
194
+ }
195
+ if (input.surface || input.warning) {
196
+ ensure(out, "warning-strong", surface, 4.5);
197
+ ensure(out, "warning-strong", out["warning-soft"], 4.5);
198
+ }
199
+ if (input.fonts?.sans)
200
+ out["font-sans"] = input.fonts.sans;
201
+ if (input.fonts?.mono)
202
+ out["font-mono"] = input.fonts.mono;
203
+ if (input.radius) {
204
+ const r = typeof input.radius === "string" ? RADII[input.radius] : { ...RADII.default, ...input.radius };
205
+ out["radius-sm"] = r.sm;
206
+ out["radius-md"] = r.md;
207
+ out["radius-lg"] = r.lg;
208
+ }
209
+ scaleTokens(out, groups.fontSize, input.textScale);
210
+ scaleTokens(out, groups.spacing, input.spaceScale);
211
+ deriveStrokes(input, out);
212
+ Object.assign(out, MOTION[input.motion ?? "default"]);
213
+ const height = positive(input.controlHeight);
214
+ if (height !== undefined) {
215
+ out["control-height"] = px(height);
216
+ out["chip-height"] = px(Math.max(2, Math.round(height / 4) * 2));
217
+ }
218
+ if (input.elevation || input.background || input.surface || input.foreground || positive(input.strokes?.hair) !== undefined) {
219
+ deriveElevation(input.elevation ?? "outline", out, dark);
220
+ }
221
+ Object.assign(out, input.tokens);
222
+ return out;
223
+ }
224
+ function resolve(tokens, name, seen = new Set()) {
225
+ const v = tokens[name] ?? "";
226
+ const m = /^var\(--([a-z-]+)\)$/.exec(v);
227
+ if (!m || seen.has(name))
228
+ return v;
229
+ seen.add(name);
230
+ return resolve(tokens, m[1], seen);
231
+ }
232
+ export function checkContrast(tokens) {
233
+ const issues = [];
234
+ for (const { fg, bg, min, role } of PAIRS) {
235
+ const a = resolve(tokens, fg);
236
+ const b = resolve(tokens, bg);
237
+ if (!a.startsWith("#") || !b.startsWith("#"))
238
+ continue;
239
+ const ratio = contrast(a, b);
240
+ if (ratio < min)
241
+ issues.push({ pair: [fg, bg], ratio: Math.round(ratio * 100) / 100, min, role });
242
+ }
243
+ return issues;
244
+ }
245
+ const WASHES = [
246
+ "--e-wash-hover: color-mix(in srgb, var(--e-ink) 8%, transparent);",
247
+ "--e-wash-press: color-mix(in srgb, var(--e-ink) 15%, transparent);",
248
+ "--e-wash-ring: color-mix(in srgb, var(--e-ink) 38%, transparent);",
249
+ ];
250
+ export function defineTheme(input = {}) {
251
+ const name = input.name ?? "custom";
252
+ const tokens = derive(input);
253
+ const issues = checkContrast(tokens);
254
+ if (input.strict && issues.length) {
255
+ throw new Error("Theme fails contrast:\n" +
256
+ issues.map((i) => ` ${i.role}: ${i.pair.join(" on ")} is ${i.ratio}:1, needs ${i.min}:1`).join("\n"));
257
+ }
258
+ return {
259
+ name,
260
+ tokens,
261
+ issues,
262
+ css({ selector = ":root", layer = true, header = false } = {}) {
263
+ const decls = Object.entries(tokens)
264
+ .map(([k, v]) => ` --e-${k}: ${v};`)
265
+ .join("\n");
266
+ const derived = `${selector}, [data-e-theme] {\n${WASHES.map((w) => " " + w).join("\n")}\n}`;
267
+ const rules = `${selector} {\n${decls}\n}\n${derived}\n`;
268
+ const body = layer ? `@layer e.tokens {\n${rules}}\n` : rules;
269
+ return (header ? `/* Generated from tokens.json. Theme: ${name}. */\n@layer ${LAYER_ORDER};\n` : "") + body;
270
+ },
271
+ apply(el = document.documentElement) {
272
+ for (const [k, v] of Object.entries(tokens))
273
+ el.style.setProperty(`--e-${k}`, v);
274
+ el.setAttribute("data-e-theme", name);
275
+ },
276
+ };
277
+ }
278
+ export { defaults, groups };
@@ -0,0 +1,75 @@
1
+ /* Generated from tokens.json. Theme: light. */
2
+ @layer e.reset, e.tokens, e.base, e.primitives, e.blocks, e.controls, e.composites, e.containers, e.account, e.page;
3
+ @layer e.tokens {
4
+ :root {
5
+ --e-brand-blue: #2c95b4;
6
+ --e-brand-green: #72c451;
7
+ --e-primary: #17586d;
8
+ --e-on-primary: #ffffff;
9
+ --e-secondary: #2b5c1b;
10
+ --e-on-secondary: #ffffff;
11
+ --e-secondary-soft: #e8f5e1;
12
+ --e-danger: #962019;
13
+ --e-on-danger: #ffffff;
14
+ --e-danger-soft: #fbe9e7;
15
+ --e-warning-strong: #714a00;
16
+ --e-warning-soft: #fdf3dc;
17
+ --e-background: #ffffff;
18
+ --e-surface: #ffffff;
19
+ --e-surface-tint: #eaf4f8;
20
+ --e-ink: #000000;
21
+ --e-scrim: rgb(11 40 52 / 0.55);
22
+ --e-muted: #515151;
23
+ --e-border: #7f8f96;
24
+ --e-border-strong: #767676;
25
+ --e-focus-ring: var(--e-ink);
26
+ --e-focus-field: var(--e-primary);
27
+ --e-space-1: 4px;
28
+ --e-space-2: 8px;
29
+ --e-space-3: 12px;
30
+ --e-space-4: 16px;
31
+ --e-space-6: 24px;
32
+ --e-space-8: 32px;
33
+ --e-radius-sm: 4px;
34
+ --e-radius-md: 8px;
35
+ --e-radius-lg: 16px;
36
+ --e-radius-pill: 999px;
37
+ --e-edge: 0 0 0 1px #7f8f96;
38
+ --e-well: inset 0 2px 0 0 rgba(0, 0, 0, 0.1);
39
+ --e-float: 0 0 0 1px #7f8f96, 0 1px 2px 0px rgba(36, 36, 36, 0.08), 0 4px 8px -2px rgba(36, 36, 36, 0.1), 0 12px 24px -6px rgba(36, 36, 36, 0.14), 0 24px 48px -12px rgba(36, 36, 36, 0.18);
40
+ --e-text-xs: 12px;
41
+ --e-text-sm: 14px;
42
+ --e-text-md: 16px;
43
+ --e-text-lg: 18px;
44
+ --e-text-xl: 20px;
45
+ --e-text-2xl: 24px;
46
+ --e-text-3xl: 30px;
47
+ --e-text-4xl: 36px;
48
+ --e-control-height: 48px;
49
+ --e-chip-height: 24px;
50
+ --e-measure: 62ch;
51
+ --e-stroke-hair: 1px;
52
+ --e-stroke-card: 1px;
53
+ --e-stroke-edge: 1.5px;
54
+ --e-stroke-focus: 2px;
55
+ --e-focus-gap: 2px;
56
+ --e-focus-gap-field: -1px;
57
+ --e-depth: 2px;
58
+ --e-duration-tap: 80ms;
59
+ --e-duration-move: 220ms;
60
+ --e-easing-out: cubic-bezier(0.2, 0.8, 0.2, 1);
61
+ --e-z-raised: 1;
62
+ --e-z-sticky: 5;
63
+ --e-z-tooltip: 55;
64
+ --e-z-modal: 40;
65
+ --e-z-popover: 45;
66
+ --e-z-toast: 50;
67
+ --e-font-sans: "Jost", Futura, "Century Gothic", "Avenir Next", system-ui, sans-serif;
68
+ --e-font-mono: "Spline Sans Mono", ui-monospace, Menlo, Consolas, monospace;
69
+ }
70
+ :root, [data-e-theme] {
71
+ --e-wash-hover: color-mix(in srgb, var(--e-ink) 8%, transparent);
72
+ --e-wash-press: color-mix(in srgb, var(--e-ink) 15%, transparent);
73
+ --e-wash-ring: color-mix(in srgb, var(--e-ink) 38%, transparent);
74
+ }
75
+ }
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "@eos-ui/tokens",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "Eos design tokens: CSS custom properties, typed values and a theme generator with contrast checks.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "sideEffects": [
8
+ "**/*.css"
9
+ ],
10
+ "files": [
11
+ "dist",
12
+ "src/tokens.json"
13
+ ],
14
+ "engines": {
15
+ "node": ">=22"
16
+ },
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "./tokens.css": "./dist/tokens.css",
25
+ "./tokens.json": "./src/tokens.json"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public",
29
+ "provenance": true
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/Dame-G/eos.git",
34
+ "directory": "packages/tokens"
35
+ },
36
+ "devDependencies": {
37
+ "vitest": "^5.0.5"
38
+ },
39
+ "homepage": "https://eos.dame.gg",
40
+ "bugs": {
41
+ "url": "https://github.com/Dame-G/eos/issues"
42
+ },
43
+ "keywords": [
44
+ "vue",
45
+ "vue3",
46
+ "design-system",
47
+ "components",
48
+ "accessible",
49
+ "a11y",
50
+ "design-tokens",
51
+ "theming"
52
+ ],
53
+ "scripts": {
54
+ "generate": "node scripts/generate.mjs",
55
+ "build": "node scripts/build.mjs",
56
+ "typecheck": "node scripts/generate.mjs && tsc -p tsconfig.json --noEmit",
57
+ "test": "node scripts/generate.mjs && vitest run"
58
+ }
6
59
  }