@handamade/psi-tokens 0.19.0 → 0.21.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 +19 -0
- package/dist/base.css +3 -0
- package/dist/cli/psi-theme.d.ts +2 -0
- package/dist/cli/psi-theme.js +56 -0
- package/dist/components/app-shell.d.ts +16 -0
- package/dist/components/app-shell.js +21 -0
- package/dist/components/app-shell.vars.css +8 -0
- package/dist/components/banner.d.ts +11 -0
- package/dist/components/banner.js +21 -0
- package/dist/components/banner.vars.css +13 -0
- package/dist/components/button.d.ts +7 -0
- package/dist/components/button.js +76 -0
- package/dist/components/card.d.ts +2 -0
- package/dist/components/card.js +7 -0
- package/dist/components/checkbox.d.ts +2 -0
- package/dist/components/checkbox.js +13 -0
- package/dist/components/control.d.ts +21 -0
- package/dist/components/control.js +59 -0
- package/dist/components/description-list.d.ts +8 -0
- package/dist/components/description-list.js +13 -0
- package/dist/components/dialog.d.ts +6 -0
- package/dist/components/dialog.js +16 -0
- package/dist/components/dialog.vars.css +1 -0
- package/dist/components/field.d.ts +3 -0
- package/dist/components/field.js +14 -0
- package/dist/components/field.vars.css +1 -0
- package/dist/components/inline-alert.d.ts +12 -0
- package/dist/components/inline-alert.js +27 -0
- package/dist/components/inline-alert.vars.css +18 -0
- package/dist/components/input.d.ts +2 -0
- package/dist/components/input.js +25 -0
- package/dist/components/media.d.ts +3 -0
- package/dist/components/media.js +5 -0
- package/dist/components/menu.d.ts +7 -0
- package/dist/components/menu.js +21 -0
- package/dist/components/menu.vars.css +1 -0
- package/dist/components/nav-tree.d.ts +12 -0
- package/dist/components/nav-tree.js +26 -0
- package/dist/components/nav-tree.vars.css +12 -0
- package/dist/components/navbar.d.ts +3 -0
- package/dist/components/navbar.js +14 -0
- package/dist/components/navbar.vars.css +1 -0
- package/dist/components/navtree.d.ts +12 -0
- package/dist/components/navtree.js +26 -0
- package/dist/components/navtree.vars.css +12 -0
- package/dist/components/panel.d.ts +5 -0
- package/dist/components/panel.js +10 -0
- package/dist/components/registry.d.ts +1 -0
- package/dist/components/registry.js +58 -0
- package/dist/components/select.d.ts +2 -0
- package/dist/components/select.js +38 -0
- package/dist/components/skeleton.d.ts +4 -0
- package/dist/components/skeleton.js +7 -0
- package/dist/components/skeleton.vars.css +6 -0
- package/dist/components/skip-link.d.ts +6 -0
- package/dist/components/skip-link.js +12 -0
- package/dist/components/skip-link.vars.css +9 -0
- package/dist/components/skiplink.d.ts +6 -0
- package/dist/components/skiplink.js +12 -0
- package/dist/components/skiplink.vars.css +9 -0
- package/dist/components/surface.d.ts +5 -0
- package/dist/components/surface.js +10 -0
- package/dist/components/switch.d.ts +2 -0
- package/dist/components/switch.js +8 -0
- package/dist/components/table.d.ts +12 -0
- package/dist/components/table.js +30 -0
- package/dist/components/tabs.d.ts +14 -0
- package/dist/components/tabs.js +26 -0
- package/dist/components/tag.d.ts +4 -0
- package/dist/components/tag.js +26 -0
- package/dist/components/toast.d.ts +19 -0
- package/dist/components/toast.js +28 -0
- package/dist/components/toolbar.d.ts +13 -0
- package/dist/components/toolbar.js +15 -0
- package/dist/components/tooltip.d.ts +2 -0
- package/dist/components/tooltip.js +7 -0
- package/dist/components.css +76 -0
- package/dist/contrast-matrix.js +8 -0
- package/dist/dsl/validator.d.ts +12 -0
- package/dist/dsl/validator.js +97 -0
- package/dist/dtcg/acme.json +18 -2
- package/dist/dtcg/dark.json +18 -2
- package/dist/dtcg/ember.json +18 -2
- package/dist/dtcg/light.json +18 -2
- package/dist/emit/css.d.ts +30 -0
- package/dist/emit/css.js +120 -0
- package/dist/gamut.d.ts +4 -0
- package/dist/gamut.js +44 -0
- package/dist/guidance.json +2 -2
- package/dist/resolved/acme.json +9 -2
- package/dist/resolved/dark.json +9 -2
- package/dist/resolved/ember.json +9 -2
- package/dist/resolved/light.json +9 -2
- package/dist/scope-gate.d.ts +15 -0
- package/dist/scope-gate.js +87 -0
- package/dist/scope-map.json +108 -3
- package/dist/scopes.d.ts +6 -0
- package/dist/scopes.js +88 -0
- package/dist/theme/gate.d.ts +19 -0
- package/dist/theme/gate.js +44 -0
- package/dist/theme/index.d.ts +16 -0
- package/dist/theme/index.js +23 -0
- package/dist/themes/dark.js +2 -2
- package/dist/themes/light.js +4 -2
- package/llms.txt +12 -0
- package/package.json +9 -1
package/dist/components.css
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
@layer psi.components {
|
|
2
|
+
:where(:root, [data-psi-theme]) {
|
|
3
|
+
--psi-app-shell-sidebar-width: 16rem;
|
|
4
|
+
--psi-app-shell-sidebar-bg: var(--psi-bg-primary);
|
|
5
|
+
--psi-app-shell-sidebar-fg: var(--psi-fg-primary);
|
|
6
|
+
--psi-app-shell-focus-ring: var(--psi-border-focus);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@layer psi.components {
|
|
11
|
+
:where(:root, [data-psi-theme]) {
|
|
12
|
+
--psi-banner-bg-neutral: var(--psi-fill-neutral3);
|
|
13
|
+
--psi-banner-bg-success: var(--psi-fill-tint-success);
|
|
14
|
+
--psi-banner-bg-warning: var(--psi-fill-tint-warning);
|
|
15
|
+
--psi-banner-bg-danger: var(--psi-fill-tint-danger);
|
|
16
|
+
--psi-banner-fg: var(--psi-fg-primary);
|
|
17
|
+
--psi-banner-icon-fg-neutral: var(--psi-fg-secondary);
|
|
18
|
+
--psi-banner-icon-fg-success: var(--psi-fg-success);
|
|
19
|
+
--psi-banner-icon-fg-warning: var(--psi-fg-warning);
|
|
20
|
+
--psi-banner-icon-fg-danger: var(--psi-fg-danger);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
1
24
|
@layer psi.components {
|
|
2
25
|
:where(:root, [data-psi-theme]) {
|
|
3
26
|
--psi-button-accent-bg: var(--psi-fill-accent);
|
|
@@ -123,6 +146,7 @@
|
|
|
123
146
|
--psi-dialog-backdrop: var(--psi-scrim-heavy);
|
|
124
147
|
--psi-dialog-fg: var(--psi-fg-primary);
|
|
125
148
|
--psi-dialog-title-fg: var(--psi-fg-primary);
|
|
149
|
+
--psi-dialog-focus-ring: var(--psi-border-focus);
|
|
126
150
|
}
|
|
127
151
|
}
|
|
128
152
|
|
|
@@ -132,6 +156,7 @@
|
|
|
132
156
|
--psi-field-message-fg: var(--psi-fg-tertiary);
|
|
133
157
|
--psi-field-error-fg: var(--psi-fg-danger);
|
|
134
158
|
--psi-field-marker-fg: var(--psi-fg-danger);
|
|
159
|
+
--psi-field-required-text-fg: var(--psi-fg-secondary);
|
|
135
160
|
--psi-field-gap: var(--psi-space-6);
|
|
136
161
|
}
|
|
137
162
|
}
|
|
@@ -161,6 +186,25 @@
|
|
|
161
186
|
}
|
|
162
187
|
}
|
|
163
188
|
|
|
189
|
+
@layer psi.components {
|
|
190
|
+
:where(:root, [data-psi-theme]) {
|
|
191
|
+
--psi-inline-alert-bg-neutral: var(--psi-fill-neutral3);
|
|
192
|
+
--psi-inline-alert-bg-success: var(--psi-fill-tint-success);
|
|
193
|
+
--psi-inline-alert-bg-warning: var(--psi-fill-tint-warning);
|
|
194
|
+
--psi-inline-alert-bg-danger: var(--psi-fill-tint-danger);
|
|
195
|
+
--psi-inline-alert-fg: var(--psi-fg-primary);
|
|
196
|
+
--psi-inline-alert-icon-fg-neutral: var(--psi-fg-secondary);
|
|
197
|
+
--psi-inline-alert-icon-fg-success: var(--psi-fg-success);
|
|
198
|
+
--psi-inline-alert-icon-fg-warning: var(--psi-fg-warning);
|
|
199
|
+
--psi-inline-alert-icon-fg-danger: var(--psi-fg-danger);
|
|
200
|
+
--psi-inline-alert-border-neutral: var(--psi-border-neutral);
|
|
201
|
+
--psi-inline-alert-border-success: var(--psi-fg-success);
|
|
202
|
+
--psi-inline-alert-border-warning: var(--psi-fg-warning);
|
|
203
|
+
--psi-inline-alert-border-danger: var(--psi-fg-danger);
|
|
204
|
+
--psi-inline-alert-radius: var(--psi-radius-8);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
164
208
|
@layer psi.components {
|
|
165
209
|
:where(:root, [data-psi-theme]) {
|
|
166
210
|
--psi-media-tint: none;
|
|
@@ -180,6 +224,7 @@
|
|
|
180
224
|
--psi-menu-item-fg-danger: var(--psi-fg-danger);
|
|
181
225
|
--psi-menu-item-fg-disabled: var(--psi-fg-quaternary);
|
|
182
226
|
--psi-menu-separator-border: var(--psi-border-faint);
|
|
227
|
+
--psi-menu-focus-ring: var(--psi-border-focus);
|
|
183
228
|
}
|
|
184
229
|
}
|
|
185
230
|
|
|
@@ -192,6 +237,7 @@
|
|
|
192
237
|
--psi-navbar-link-fg: var(--psi-fg-secondary);
|
|
193
238
|
--psi-navbar-link-fg-hover: var(--psi-fg-primary);
|
|
194
239
|
--psi-navbar-focus-ring: var(--psi-border-focus);
|
|
240
|
+
--psi-navbar-gutter: var(--psi-gutter);
|
|
195
241
|
}
|
|
196
242
|
}
|
|
197
243
|
|
|
@@ -238,6 +284,36 @@
|
|
|
238
284
|
}
|
|
239
285
|
}
|
|
240
286
|
|
|
287
|
+
@layer psi.components {
|
|
288
|
+
:where(:root, [data-psi-theme]) {
|
|
289
|
+
--psi-skeleton-bg: var(--psi-fill-neutral4);
|
|
290
|
+
--psi-skeleton-radius: var(--psi-radius-4);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
@layer psi.components {
|
|
295
|
+
:where(:root, [data-psi-theme]) {
|
|
296
|
+
--psi-nav-tree-item-fg: var(--psi-fg-secondary);
|
|
297
|
+
--psi-nav-tree-item-fg-current: var(--psi-fg-primary);
|
|
298
|
+
--psi-nav-tree-item-bg-hover: var(--psi-fill-neutral3);
|
|
299
|
+
--psi-nav-tree-item-bg-current: var(--psi-fill-neutral4);
|
|
300
|
+
--psi-nav-tree-indicator: var(--psi-fill-accent);
|
|
301
|
+
--psi-nav-tree-group-fg: var(--psi-fg-secondary);
|
|
302
|
+
--psi-nav-tree-radius: var(--psi-control-radius);
|
|
303
|
+
--psi-nav-tree-focus-ring: var(--psi-border-focus);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
@layer psi.components {
|
|
308
|
+
:where(:root, [data-psi-theme]) {
|
|
309
|
+
--psi-skip-link-bg: var(--psi-surface-bg);
|
|
310
|
+
--psi-skip-link-fg: var(--psi-fg-primary);
|
|
311
|
+
--psi-skip-link-border: var(--psi-surface-border);
|
|
312
|
+
--psi-skip-link-radius: var(--psi-control-radius);
|
|
313
|
+
--psi-skip-link-focus-ring: var(--psi-border-focus);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
241
317
|
@layer psi.components {
|
|
242
318
|
:where(:root, [data-psi-theme]) {
|
|
243
319
|
--psi-surface-bg: var(--psi-bg-secondary);
|
package/dist/contrast-matrix.js
CHANGED
|
@@ -108,6 +108,14 @@ export const wcagAAPairs = [
|
|
|
108
108
|
// the fgAccent/fillTintAccent pair above: a selected row keeps ordinary
|
|
109
109
|
// body text, it does not recolor it to the accent.
|
|
110
110
|
{ fg: "fgPrimary", bg: "fillTintAccent", minRatio: 4.5 },
|
|
111
|
+
// Body text on the status tints (D86): Banner and InlineAlert set their
|
|
112
|
+
// title and body in fgPrimary on fill-tint-{success,warning,danger}. The
|
|
113
|
+
// tints are 12–15% of the status foreground over the canvas, so every
|
|
114
|
+
// shipped theme clears this by a wide margin; it is gated because a
|
|
115
|
+
// consumer theme built with psi-theme (D81) is held to the same matrix.
|
|
116
|
+
{ fg: "fgPrimary", bg: "fillTintSuccess", minRatio: 4.5 },
|
|
117
|
+
{ fg: "fgPrimary", bg: "fillTintWarning", minRatio: 4.5 },
|
|
118
|
+
{ fg: "fgPrimary", bg: "fillTintDanger", minRatio: 4.5 },
|
|
111
119
|
// Toast's status icon on the elevated surface (D64/D65). The three pairs
|
|
112
120
|
// above gate these foregrounds on bgPrimary; --psi-surface-bg — what Toast,
|
|
113
121
|
// Menu and Dialog all alias — resolves to bgSecondary, which was ungated for
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SlotMap, ThemeDef } from "./types.js";
|
|
2
|
+
export declare class ValidationError extends Error {
|
|
3
|
+
readonly path: string[];
|
|
4
|
+
constructor(message: string, path?: string[]);
|
|
5
|
+
}
|
|
6
|
+
export declare function validate(theme: ThemeDef, slots: SlotMap, existingNames?: string[]): void;
|
|
7
|
+
/** D46: the same token name must carry identical scopes in every theme —
|
|
8
|
+
* scopes are a property of the token's meaning, not of a theme. */
|
|
9
|
+
export declare function validateScopeConsistency(themes: Record<string, ThemeDef>): void;
|
|
10
|
+
/** D46 follow-up (HAN-21): reject semantic token kebab names that a
|
|
11
|
+
* scale-family lookup would shadow before the semantic lookup runs. */
|
|
12
|
+
export declare function validateNoScalePrefixShadow(kebabNames: readonly string[]): void;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { isKnownScopeEntry } from "../scopes.js";
|
|
2
|
+
// ── Validation error ───────────────────────────────────────────────
|
|
3
|
+
export class ValidationError extends Error {
|
|
4
|
+
path;
|
|
5
|
+
constructor(message, path = []) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "ValidationError";
|
|
8
|
+
this.path = path;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
// ── Cycle detection ────────────────────────────────────────────────
|
|
12
|
+
function detectCycles(theme) {
|
|
13
|
+
const visited = new Set();
|
|
14
|
+
const inStack = new Set();
|
|
15
|
+
function dfs(name, chain) {
|
|
16
|
+
if (inStack.has(name)) {
|
|
17
|
+
const cycleStart = chain.indexOf(name);
|
|
18
|
+
const cycle = chain.slice(cycleStart).concat(name);
|
|
19
|
+
throw new ValidationError(`Circular reference detected: ${cycle.join(" → ")}`, cycle);
|
|
20
|
+
}
|
|
21
|
+
if (visited.has(name))
|
|
22
|
+
return;
|
|
23
|
+
inStack.add(name);
|
|
24
|
+
chain.push(name);
|
|
25
|
+
const token = theme[name];
|
|
26
|
+
if (token?.from.type === "ref") {
|
|
27
|
+
dfs(token.from.name, chain);
|
|
28
|
+
}
|
|
29
|
+
chain.pop();
|
|
30
|
+
inStack.delete(name);
|
|
31
|
+
visited.add(name);
|
|
32
|
+
}
|
|
33
|
+
for (const name of Object.keys(theme)) {
|
|
34
|
+
dfs(name, []);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// ── Main validator ─────────────────────────────────────────────────
|
|
38
|
+
export function validate(theme, slots, existingNames = []) {
|
|
39
|
+
const slotNames = new Set(Object.keys(slots));
|
|
40
|
+
const tokenNames = new Set(Object.keys(theme));
|
|
41
|
+
// Check for duplicate names against existingNames
|
|
42
|
+
for (const name of tokenNames) {
|
|
43
|
+
if (existingNames.includes(name)) {
|
|
44
|
+
throw new ValidationError(`Duplicate token name: "${name}" already exists`, [name]);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Validate each token's source
|
|
48
|
+
for (const [name, token] of Object.entries(theme)) {
|
|
49
|
+
const { from } = token;
|
|
50
|
+
if (from.type === "slot") {
|
|
51
|
+
if (!slotNames.has(from.name)) {
|
|
52
|
+
throw new ValidationError(`Unknown slot "${from.name}" in token "${name}"`, [name]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else if (from.type === "ref") {
|
|
56
|
+
if (!tokenNames.has(from.name)) {
|
|
57
|
+
throw new ValidationError(`Unknown ref "${from.name}" in token "${name}"`, [name]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const entry of token.scopes ?? []) {
|
|
61
|
+
if (!isKnownScopeEntry(entry)) {
|
|
62
|
+
throw new ValidationError(`Unknown scope entry "${entry}" in token "${name}" — not a known CSS property or declared group`, [name]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Detect cycles in ref chains
|
|
67
|
+
detectCycles(theme);
|
|
68
|
+
}
|
|
69
|
+
/** D46: the same token name must carry identical scopes in every theme —
|
|
70
|
+
* scopes are a property of the token's meaning, not of a theme. */
|
|
71
|
+
export function validateScopeConsistency(themes) {
|
|
72
|
+
const seen = new Map();
|
|
73
|
+
for (const [themeName, theme] of Object.entries(themes)) {
|
|
74
|
+
for (const [name, def] of Object.entries(theme)) {
|
|
75
|
+
const key = JSON.stringify(def.scopes ?? null);
|
|
76
|
+
const prior = seen.get(name);
|
|
77
|
+
if (prior === undefined) {
|
|
78
|
+
seen.set(name, { theme: themeName, scopes: key });
|
|
79
|
+
}
|
|
80
|
+
else if (prior.scopes !== key) {
|
|
81
|
+
throw new ValidationError(`Scope drift for token "${name}": ${prior.theme} declares ${prior.scopes}, ${themeName} declares ${key}`, [name]);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Both gates resolve --psi-* names scale-first, so a semantic token whose
|
|
87
|
+
// kebab name starts with a scale-family prefix would be silently shadowed.
|
|
88
|
+
const SCALE_FAMILY_PREFIX = /^(space|size|radius|text|font|duration|ease|z|focus-ring)-/;
|
|
89
|
+
/** D46 follow-up (HAN-21): reject semantic token kebab names that a
|
|
90
|
+
* scale-family lookup would shadow before the semantic lookup runs. */
|
|
91
|
+
export function validateNoScalePrefixShadow(kebabNames) {
|
|
92
|
+
for (const name of kebabNames) {
|
|
93
|
+
if (SCALE_FAMILY_PREFIX.test(name)) {
|
|
94
|
+
throw new ValidationError(`Semantic token "${name}" starts with a scale-family prefix — it would be shadowed by the scale lookup in the build and stylelint gates`, [name]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
package/dist/dtcg/acme.json
CHANGED
|
@@ -155,7 +155,8 @@
|
|
|
155
155
|
"$extensions": {
|
|
156
156
|
"psi": {
|
|
157
157
|
"scopes": [
|
|
158
|
-
"text"
|
|
158
|
+
"text",
|
|
159
|
+
"border"
|
|
159
160
|
]
|
|
160
161
|
}
|
|
161
162
|
}
|
|
@@ -167,7 +168,8 @@
|
|
|
167
168
|
"$extensions": {
|
|
168
169
|
"psi": {
|
|
169
170
|
"scopes": [
|
|
170
|
-
"text"
|
|
171
|
+
"text",
|
|
172
|
+
"border"
|
|
171
173
|
]
|
|
172
174
|
}
|
|
173
175
|
}
|
|
@@ -607,6 +609,20 @@
|
|
|
607
609
|
"$type": "dimension",
|
|
608
610
|
"$value": "1.5rem"
|
|
609
611
|
}
|
|
612
|
+
},
|
|
613
|
+
"focusRing": {
|
|
614
|
+
"width": {
|
|
615
|
+
"$type": "dimension",
|
|
616
|
+
"$value": "2px"
|
|
617
|
+
},
|
|
618
|
+
"offset": {
|
|
619
|
+
"$type": "dimension",
|
|
620
|
+
"$value": "2px"
|
|
621
|
+
},
|
|
622
|
+
"offsetInset": {
|
|
623
|
+
"$type": "dimension",
|
|
624
|
+
"$value": "-2px"
|
|
625
|
+
}
|
|
610
626
|
}
|
|
611
627
|
},
|
|
612
628
|
"typography": {
|
package/dist/dtcg/dark.json
CHANGED
|
@@ -155,7 +155,8 @@
|
|
|
155
155
|
"$extensions": {
|
|
156
156
|
"psi": {
|
|
157
157
|
"scopes": [
|
|
158
|
-
"text"
|
|
158
|
+
"text",
|
|
159
|
+
"border"
|
|
159
160
|
]
|
|
160
161
|
}
|
|
161
162
|
}
|
|
@@ -167,7 +168,8 @@
|
|
|
167
168
|
"$extensions": {
|
|
168
169
|
"psi": {
|
|
169
170
|
"scopes": [
|
|
170
|
-
"text"
|
|
171
|
+
"text",
|
|
172
|
+
"border"
|
|
171
173
|
]
|
|
172
174
|
}
|
|
173
175
|
}
|
|
@@ -607,6 +609,20 @@
|
|
|
607
609
|
"$type": "dimension",
|
|
608
610
|
"$value": "1.5rem"
|
|
609
611
|
}
|
|
612
|
+
},
|
|
613
|
+
"focusRing": {
|
|
614
|
+
"width": {
|
|
615
|
+
"$type": "dimension",
|
|
616
|
+
"$value": "2px"
|
|
617
|
+
},
|
|
618
|
+
"offset": {
|
|
619
|
+
"$type": "dimension",
|
|
620
|
+
"$value": "2px"
|
|
621
|
+
},
|
|
622
|
+
"offsetInset": {
|
|
623
|
+
"$type": "dimension",
|
|
624
|
+
"$value": "-2px"
|
|
625
|
+
}
|
|
610
626
|
}
|
|
611
627
|
},
|
|
612
628
|
"typography": {
|
package/dist/dtcg/ember.json
CHANGED
|
@@ -155,7 +155,8 @@
|
|
|
155
155
|
"$extensions": {
|
|
156
156
|
"psi": {
|
|
157
157
|
"scopes": [
|
|
158
|
-
"text"
|
|
158
|
+
"text",
|
|
159
|
+
"border"
|
|
159
160
|
]
|
|
160
161
|
}
|
|
161
162
|
}
|
|
@@ -167,7 +168,8 @@
|
|
|
167
168
|
"$extensions": {
|
|
168
169
|
"psi": {
|
|
169
170
|
"scopes": [
|
|
170
|
-
"text"
|
|
171
|
+
"text",
|
|
172
|
+
"border"
|
|
171
173
|
]
|
|
172
174
|
}
|
|
173
175
|
}
|
|
@@ -607,6 +609,20 @@
|
|
|
607
609
|
"$type": "dimension",
|
|
608
610
|
"$value": "1.5rem"
|
|
609
611
|
}
|
|
612
|
+
},
|
|
613
|
+
"focusRing": {
|
|
614
|
+
"width": {
|
|
615
|
+
"$type": "dimension",
|
|
616
|
+
"$value": "2px"
|
|
617
|
+
},
|
|
618
|
+
"offset": {
|
|
619
|
+
"$type": "dimension",
|
|
620
|
+
"$value": "2px"
|
|
621
|
+
},
|
|
622
|
+
"offsetInset": {
|
|
623
|
+
"$type": "dimension",
|
|
624
|
+
"$value": "-2px"
|
|
625
|
+
}
|
|
610
626
|
}
|
|
611
627
|
},
|
|
612
628
|
"typography": {
|
package/dist/dtcg/light.json
CHANGED
|
@@ -155,7 +155,8 @@
|
|
|
155
155
|
"$extensions": {
|
|
156
156
|
"psi": {
|
|
157
157
|
"scopes": [
|
|
158
|
-
"text"
|
|
158
|
+
"text",
|
|
159
|
+
"border"
|
|
159
160
|
]
|
|
160
161
|
}
|
|
161
162
|
}
|
|
@@ -167,7 +168,8 @@
|
|
|
167
168
|
"$extensions": {
|
|
168
169
|
"psi": {
|
|
169
170
|
"scopes": [
|
|
170
|
-
"text"
|
|
171
|
+
"text",
|
|
172
|
+
"border"
|
|
171
173
|
]
|
|
172
174
|
}
|
|
173
175
|
}
|
|
@@ -607,6 +609,20 @@
|
|
|
607
609
|
"$type": "dimension",
|
|
608
610
|
"$value": "1.5rem"
|
|
609
611
|
}
|
|
612
|
+
},
|
|
613
|
+
"focusRing": {
|
|
614
|
+
"width": {
|
|
615
|
+
"$type": "dimension",
|
|
616
|
+
"$value": "2px"
|
|
617
|
+
},
|
|
618
|
+
"offset": {
|
|
619
|
+
"$type": "dimension",
|
|
620
|
+
"$value": "2px"
|
|
621
|
+
},
|
|
622
|
+
"offsetInset": {
|
|
623
|
+
"$type": "dimension",
|
|
624
|
+
"$value": "-2px"
|
|
625
|
+
}
|
|
610
626
|
}
|
|
611
627
|
},
|
|
612
628
|
"typography": {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ChannelOp, Palette, SlotMap, ThemeDef, TokenDef } from "../dsl/types.js";
|
|
2
|
+
import type { BrandFonts } from "../themes/customers/index.js";
|
|
3
|
+
export declare function camelToKebab(str: string): string;
|
|
4
|
+
export declare function tokenVar(name: string): string;
|
|
5
|
+
export declare function paletteVar(name: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Convert a ChannelOp to its CSS representation within oklch(from ...).
|
|
8
|
+
* - set → literal value
|
|
9
|
+
* - delta → calc(channel +/- value)
|
|
10
|
+
* - cap → min(channel, value)
|
|
11
|
+
*/
|
|
12
|
+
export declare function opToCSS(channel: string, op: ChannelOp): string;
|
|
13
|
+
/**
|
|
14
|
+
* Assemble the full oklch(from var(...) ...) expression for a token.
|
|
15
|
+
*
|
|
16
|
+
* Slot-sourced token with ops:
|
|
17
|
+
* oklch(from var(--psi-palette-sapphire) 0.65 min(c, 0.23) h)
|
|
18
|
+
*
|
|
19
|
+
* Ref-sourced token with alpha:
|
|
20
|
+
* oklch(from var(--psi-fg-primary) l c h / 0.7)
|
|
21
|
+
*
|
|
22
|
+
* No-op (just a slot reference, no ops, no alpha):
|
|
23
|
+
* var(--psi-palette-platinum)
|
|
24
|
+
*/
|
|
25
|
+
export declare function tokenToLiveCSS(def: TokenDef, slots: SlotMap): string;
|
|
26
|
+
export declare function emitBaseCSS(palette: Palette): string;
|
|
27
|
+
export declare function emitThemeCSS(themeName: string, theme: ThemeDef, palette: Palette, slots: SlotMap, opts?: {
|
|
28
|
+
fonts?: BrandFonts;
|
|
29
|
+
componentOverrides?: Record<string, string>;
|
|
30
|
+
}): string;
|
package/dist/emit/css.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
2
|
+
export function camelToKebab(str) {
|
|
3
|
+
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
4
|
+
}
|
|
5
|
+
export function tokenVar(name) {
|
|
6
|
+
return `--psi-${camelToKebab(name)}`;
|
|
7
|
+
}
|
|
8
|
+
export function paletteVar(name) {
|
|
9
|
+
return `--psi-palette-${name}`;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Convert a ChannelOp to its CSS representation within oklch(from ...).
|
|
13
|
+
* - set → literal value
|
|
14
|
+
* - delta → calc(channel +/- value)
|
|
15
|
+
* - cap → min(channel, value)
|
|
16
|
+
*/
|
|
17
|
+
export function opToCSS(channel, op) {
|
|
18
|
+
switch (op.op) {
|
|
19
|
+
case "set":
|
|
20
|
+
return `${op.value}`;
|
|
21
|
+
case "delta": {
|
|
22
|
+
const sign = op.value >= 0 ? "+" : "-";
|
|
23
|
+
const abs = Math.abs(op.value);
|
|
24
|
+
return `calc(${channel} ${sign} ${abs})`;
|
|
25
|
+
}
|
|
26
|
+
case "cap":
|
|
27
|
+
return `min(${channel}, ${op.value})`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Assemble the full oklch(from var(...) ...) expression for a token.
|
|
32
|
+
*
|
|
33
|
+
* Slot-sourced token with ops:
|
|
34
|
+
* oklch(from var(--psi-palette-sapphire) 0.65 min(c, 0.23) h)
|
|
35
|
+
*
|
|
36
|
+
* Ref-sourced token with alpha:
|
|
37
|
+
* oklch(from var(--psi-fg-primary) l c h / 0.7)
|
|
38
|
+
*
|
|
39
|
+
* No-op (just a slot reference, no ops, no alpha):
|
|
40
|
+
* var(--psi-palette-platinum)
|
|
41
|
+
*/
|
|
42
|
+
export function tokenToLiveCSS(def, slots) {
|
|
43
|
+
const hasOps = def.l || def.c || def.h;
|
|
44
|
+
const hasAlpha = def.alpha !== undefined;
|
|
45
|
+
// Determine the source CSS var
|
|
46
|
+
let sourceVar;
|
|
47
|
+
if (def.from.type === "slot") {
|
|
48
|
+
const paletteName = slots[def.from.name];
|
|
49
|
+
sourceVar = `var(${paletteVar(paletteName)})`;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
sourceVar = `var(${tokenVar(def.from.name)})`;
|
|
53
|
+
}
|
|
54
|
+
// If no operations and no alpha, just reference the var directly
|
|
55
|
+
if (!hasOps && !hasAlpha) {
|
|
56
|
+
return sourceVar;
|
|
57
|
+
}
|
|
58
|
+
// Build oklch(from ...) expression
|
|
59
|
+
const lPart = def.l ? opToCSS("l", def.l) : "l";
|
|
60
|
+
const cPart = def.c ? opToCSS("c", def.c) : "c";
|
|
61
|
+
const hPart = def.h ? opToCSS("h", def.h) : "h";
|
|
62
|
+
let expr = `oklch(from ${sourceVar} ${lPart} ${cPart} ${hPart}`;
|
|
63
|
+
if (hasAlpha) {
|
|
64
|
+
expr += ` / ${def.alpha}`;
|
|
65
|
+
}
|
|
66
|
+
expr += `)`;
|
|
67
|
+
return expr;
|
|
68
|
+
}
|
|
69
|
+
// ── Base CSS (palette) ────────────────────────────────────────────
|
|
70
|
+
export function emitBaseCSS(palette) {
|
|
71
|
+
const lines = [];
|
|
72
|
+
lines.push("@layer psi.base, psi.theme, psi.components, psi.utilities;");
|
|
73
|
+
lines.push("");
|
|
74
|
+
lines.push("@layer psi.base {");
|
|
75
|
+
lines.push(" :root {");
|
|
76
|
+
for (const [name, entry] of Object.entries(palette)) {
|
|
77
|
+
lines.push(` ${paletteVar(name)}: oklch(${entry.l} ${entry.c} ${entry.h});`);
|
|
78
|
+
}
|
|
79
|
+
lines.push(" }");
|
|
80
|
+
lines.push("}");
|
|
81
|
+
return lines.join("\n") + "\n";
|
|
82
|
+
}
|
|
83
|
+
// ── Theme CSS (semantic tokens) ───────────────────────────────────
|
|
84
|
+
export function emitThemeCSS(themeName, theme, palette, slots, opts) {
|
|
85
|
+
const lines = [];
|
|
86
|
+
// Determine selector
|
|
87
|
+
const selector = themeName === "light"
|
|
88
|
+
? `:root, [data-psi-theme="light"]`
|
|
89
|
+
: `[data-psi-theme="${themeName}"]`;
|
|
90
|
+
lines.push("@layer psi.theme {");
|
|
91
|
+
lines.push(` ${selector} {`);
|
|
92
|
+
// Scope this theme's own palette vars inside its selector block so a
|
|
93
|
+
// customer brand's colors ship only with that theme, and two customers
|
|
94
|
+
// that happen to reuse an anchor name can't collide in a shared :root.
|
|
95
|
+
for (const [name, entry] of Object.entries(palette)) {
|
|
96
|
+
lines.push(` ${paletteVar(name)}: oklch(${entry.l} ${entry.c} ${entry.h});`);
|
|
97
|
+
}
|
|
98
|
+
for (const [name, def] of Object.entries(theme)) {
|
|
99
|
+
const cssVar = tokenVar(name);
|
|
100
|
+
const value = tokenToLiveCSS(def, slots);
|
|
101
|
+
lines.push(` ${cssVar}: ${value};`);
|
|
102
|
+
}
|
|
103
|
+
if (opts?.fonts) {
|
|
104
|
+
for (const [role, stack] of Object.entries(opts.fonts)) {
|
|
105
|
+
lines.push(` --psi-font-${role}: ${stack};`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
lines.push(" }");
|
|
109
|
+
lines.push("}");
|
|
110
|
+
if (opts?.componentOverrides && Object.keys(opts.componentOverrides).length > 0) {
|
|
111
|
+
lines.push("@layer psi.components {");
|
|
112
|
+
lines.push(` [data-psi-theme="${themeName}"] {`);
|
|
113
|
+
for (const [name, value] of Object.entries(opts.componentOverrides)) {
|
|
114
|
+
lines.push(` --psi-${name}: ${value};`);
|
|
115
|
+
}
|
|
116
|
+
lines.push(" }");
|
|
117
|
+
lines.push("}");
|
|
118
|
+
}
|
|
119
|
+
return lines.join("\n") + "\n";
|
|
120
|
+
}
|
package/dist/gamut.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Palette, SlotMap, ThemeDef } from "./dsl/types.js";
|
|
2
|
+
import type { ResolvedTheme } from "./dsl/resolver.js";
|
|
3
|
+
/** Warn when sRGB clamping moved a resolved value by ΔE2000 > 2 (D19). */
|
|
4
|
+
export declare function gamutWarnings(resolved: ResolvedTheme, theme: ThemeDef, palette: Palette, slots: SlotMap): string[];
|
package/dist/gamut.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { differenceCiede2000 } from "culori";
|
|
2
|
+
const dE = differenceCiede2000();
|
|
3
|
+
function applyOp(base, op) {
|
|
4
|
+
if (!op)
|
|
5
|
+
return base;
|
|
6
|
+
if (op.op === "set")
|
|
7
|
+
return op.value;
|
|
8
|
+
if (op.op === "delta")
|
|
9
|
+
return base + op.value;
|
|
10
|
+
return Math.min(base, op.value); // cap
|
|
11
|
+
}
|
|
12
|
+
/** Warn when sRGB clamping moved a resolved value by ΔE2000 > 2 (D19). */
|
|
13
|
+
export function gamutWarnings(resolved, theme, palette, slots) {
|
|
14
|
+
const warnings = [];
|
|
15
|
+
const raw = new Map();
|
|
16
|
+
// Recomputes pre-clamp OKLCH values by replaying token operations WITHOUT sRGB clamping.
|
|
17
|
+
// Unlike the resolver (which clamps at each step), this intentionally propagates unclamped values
|
|
18
|
+
// through ref chains so the warning reflects cumulative drift from authored intent. Do not "fix" this
|
|
19
|
+
// to match the resolver's clamping behavior — the unclamped result is required to diagnose palette issues.
|
|
20
|
+
function rawOf(name) {
|
|
21
|
+
const hit = raw.get(name);
|
|
22
|
+
if (hit)
|
|
23
|
+
return hit;
|
|
24
|
+
const def = theme[name];
|
|
25
|
+
const base = def.from.type === "slot"
|
|
26
|
+
? palette[slots[def.from.name]]
|
|
27
|
+
: rawOf(def.from.name);
|
|
28
|
+
const val = {
|
|
29
|
+
l: applyOp(base.l, def.l),
|
|
30
|
+
c: applyOp(base.c, def.c),
|
|
31
|
+
h: applyOp(base.h, def.h),
|
|
32
|
+
};
|
|
33
|
+
raw.set(name, val);
|
|
34
|
+
return val;
|
|
35
|
+
}
|
|
36
|
+
for (const name of Object.keys(theme)) {
|
|
37
|
+
const pre = { mode: "oklch", ...rawOf(name) };
|
|
38
|
+
const post = { mode: "oklch", ...resolved[name].oklch };
|
|
39
|
+
const d = dE(pre, post);
|
|
40
|
+
if (d > 2)
|
|
41
|
+
warnings.push(`${name}: clamped ΔE ${d.toFixed(1)} (${JSON.stringify(rawOf(name))} → hex ${resolved[name].hex}) — adjust the palette anchor`);
|
|
42
|
+
}
|
|
43
|
+
return warnings;
|
|
44
|
+
}
|
package/dist/guidance.json
CHANGED
|
@@ -54,14 +54,14 @@
|
|
|
54
54
|
"Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.",
|
|
55
55
|
"--psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).",
|
|
56
56
|
"Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.",
|
|
57
|
-
"Field is for a control with a *visible* label. A toolbar filter control named by aria-label or placeholder takes neither a Field nor a hand-rolled label row — compare filter-toolbar (no labels) with table-pagination (\"Rows per page\" visible, so Field).",
|
|
57
|
+
"Field is for a control with a *visible* label. A toolbar filter control named by aria-label or placeholder takes neither a Field nor a hand-rolled label row — compare filter-toolbar (no labels) with table-pagination (\"Rows per page\" visible, so Field). Filters with visible labels submitted together by a Find button are filter-form: a Toolbar as=\"form\" align=\"end\" of Fields (D87).",
|
|
58
58
|
"Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group."
|
|
59
59
|
],
|
|
60
60
|
"states": {
|
|
61
61
|
"hover": "L - 0.04",
|
|
62
62
|
"active": "L - 0.08",
|
|
63
63
|
"disabled": "element opacity 0.4 (keeps hue)",
|
|
64
|
-
"focus": "
|
|
64
|
+
"focus": "outline: var(--psi-focus-ring-width) solid var(--psi-{component}-focus-ring) on :focus-visible, at --psi-focus-ring-offset (outside) or --psi-focus-ring-offset-inset (inside) — D82"
|
|
65
65
|
},
|
|
66
66
|
"typographyDefaults": {
|
|
67
67
|
"body": "16-24-regular",
|
package/dist/resolved/acme.json
CHANGED
|
@@ -157,7 +157,8 @@
|
|
|
157
157
|
"hex": "#007048",
|
|
158
158
|
"formula": "slot(success) → L set(0.48), C cap(0.1086)",
|
|
159
159
|
"scopes": [
|
|
160
|
-
"text"
|
|
160
|
+
"text",
|
|
161
|
+
"border"
|
|
161
162
|
]
|
|
162
163
|
},
|
|
163
164
|
"fgWarning": {
|
|
@@ -170,7 +171,8 @@
|
|
|
170
171
|
"hex": "#775800",
|
|
171
172
|
"formula": "slot(warning) → L set(0.48), C cap(0.0984)",
|
|
172
173
|
"scopes": [
|
|
173
|
-
"text"
|
|
174
|
+
"text",
|
|
175
|
+
"border"
|
|
174
176
|
]
|
|
175
177
|
},
|
|
176
178
|
"fgDanger": {
|
|
@@ -574,6 +576,11 @@
|
|
|
574
576
|
"tooltip": 1100
|
|
575
577
|
}
|
|
576
578
|
},
|
|
579
|
+
"focusRing": {
|
|
580
|
+
"width": 2,
|
|
581
|
+
"offset": 2,
|
|
582
|
+
"offsetInset": -2
|
|
583
|
+
},
|
|
577
584
|
"scopes": {
|
|
578
585
|
"space": [
|
|
579
586
|
"gap"
|