@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/README.md
CHANGED
|
@@ -51,6 +51,24 @@ const { filename, source, registration } = serializeCustomerTheme(pair); // cust
|
|
|
51
51
|
- `deriveTheme` solves both members of the pair independently to WCAG AA by binary-searching lightness; it never returns a below-AA theme, because it throws instead of returning one if the solver can't clear the matrix.
|
|
52
52
|
- `serializeCustomerTheme` emits `customers/<name>.ts` source in the CLI's own layout (`pnpm new-theme`'s `Palette` + `SlotMap`), extended to a pair: two `SlotMap`s (light and dark) and, unlike the CLI's override-free scaffold, the solved `ThemeDef` overrides each member needs to clear AA — the same kind of hand-tuned correction `acmeOverrides` carries in `customers/acme.ts`, but derived rather than hand-tuned.
|
|
53
53
|
|
|
54
|
+
## Building a theme outside this repo
|
|
55
|
+
|
|
56
|
+
`@handamade/psi-tokens/theme` (D81) builds a gated theme from a definition the consumer keeps in its own repository, so a client brand never has to ship in this package:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { buildThemeCss, ThemeGateError } from "@handamade/psi-tokens/theme";
|
|
60
|
+
|
|
61
|
+
const { css, warnings } = buildThemeCss("acme-portal", {
|
|
62
|
+
palette: { /* anchors */ },
|
|
63
|
+
slots: { ink, canvas, accent, success, warning, danger },
|
|
64
|
+
base: "light", // or "dark" — e.g. a dark sidebar sub-tree
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The definition has the same shape as a `customers/<name>.ts` theme. It passes the same gates as Psi's shipped themes, which run the same code (`src/theme/gate.ts`): a WCAG AA contrast failure or a D46 scope violation throws `ThemeGateError`, naming every failure. The CSS is scoped to `[data-psi-theme="<name>"]`; load it after `base.css` and set the attribute on any element.
|
|
69
|
+
|
|
70
|
+
From a build script: `psi-theme <definition.mjs|.ts> --name <name> [--out <file.css>]`, where the definition module default-exports the theme. Exit codes: 0 built, 1 refused by a gate, 2 usage error.
|
|
71
|
+
|
|
54
72
|
## Machine-readable artifacts
|
|
55
73
|
|
|
56
74
|
For AI and tooling, see [llms.txt](./llms.txt):
|
|
@@ -58,6 +76,7 @@ For AI and tooling, see [llms.txt](./llms.txt):
|
|
|
58
76
|
- `dist/guidance.json` — Variant intent, usage rules, state derivation
|
|
59
77
|
- `dist/dtcg/{theme}.json` — W3C DTCG format export
|
|
60
78
|
- `dist/components/{name}.vars.css` — Component-specific token declarations
|
|
79
|
+
- `dist/theme/`, `dist/cli/psi-theme.js` — Compiled `./theme` subpath and the `psi-theme` bin (D81)
|
|
61
80
|
- `dist/generate/` — Compiled `./generate` subpath (`parsePrompt`, `deriveTheme`, `serializeCustomerTheme`, and lower-level helpers)
|
|
62
81
|
|
|
63
82
|
## Note
|
package/dist/base.css
CHANGED
|
@@ -85,6 +85,9 @@
|
|
|
85
85
|
--psi-z-nav: 100;
|
|
86
86
|
--psi-z-overlay: 1000;
|
|
87
87
|
--psi-z-tooltip: 1100;
|
|
88
|
+
--psi-focus-ring-width: 2px;
|
|
89
|
+
--psi-focus-ring-offset: 2px;
|
|
90
|
+
--psi-focus-ring-offset-inset: -2px;
|
|
88
91
|
|
|
89
92
|
--psi-font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
|
90
93
|
--psi-font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// D81 — psi-theme <definition> --name <name> [--out <file>]
|
|
3
|
+
// The definition is an ES module whose default export is a CustomerTheme.
|
|
4
|
+
// Exit codes: 0 built · 1 refused by a gate or invalid · 2 usage error.
|
|
5
|
+
import { writeFileSync } from "node:fs";
|
|
6
|
+
import { resolve as resolvePath } from "node:path";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
import { buildThemeCss } from "../theme/index.js";
|
|
9
|
+
const USAGE = "usage: psi-theme <definition-module> --name <theme-name> [--out <file.css>]";
|
|
10
|
+
export async function run(argv) {
|
|
11
|
+
const args = [...argv];
|
|
12
|
+
const take = (flag) => {
|
|
13
|
+
const i = args.indexOf(flag);
|
|
14
|
+
if (i === -1)
|
|
15
|
+
return undefined;
|
|
16
|
+
const value = args[i + 1];
|
|
17
|
+
args.splice(i, 2);
|
|
18
|
+
return value;
|
|
19
|
+
};
|
|
20
|
+
const name = take("--name");
|
|
21
|
+
const out = take("--out");
|
|
22
|
+
const definition = args[0];
|
|
23
|
+
if (!definition || !name || args.length !== 1) {
|
|
24
|
+
console.error(USAGE);
|
|
25
|
+
return 2;
|
|
26
|
+
}
|
|
27
|
+
let theme;
|
|
28
|
+
try {
|
|
29
|
+
const mod = (await import(pathToFileURL(resolvePath(definition)).href));
|
|
30
|
+
if (!mod.default)
|
|
31
|
+
throw new Error(`${definition} has no default export`);
|
|
32
|
+
theme = mod.default;
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
console.error(`psi-theme: cannot load ${definition}: ${e.message}`);
|
|
36
|
+
return 2;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const { css, warnings } = buildThemeCss(name, theme);
|
|
40
|
+
for (const w of warnings)
|
|
41
|
+
console.warn(`psi-theme: gamut warning [${name}] ${w}`);
|
|
42
|
+
if (out)
|
|
43
|
+
writeFileSync(out, css);
|
|
44
|
+
else
|
|
45
|
+
process.stdout.write(css);
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
console.error(`psi-theme: ${e.message}`);
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Run when executed as a program, not when imported by a test.
|
|
54
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
55
|
+
run(process.argv.slice(2)).then((code) => process.exit(code));
|
|
56
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** AppShell component tokens (--psi-app-shell-*) — D88.
|
|
2
|
+
*
|
|
3
|
+
* sidebar-width is a deliberate default for the sidebar column of the frame.
|
|
4
|
+
* 16rem is a standalone literal, matching --psi-toolbar-control-width: no
|
|
5
|
+
* shared scale reaches this range (sizeScale tops at 48, spacingScale at 144,
|
|
6
|
+
* both for heights and gaps, not column widths). The header's height stays
|
|
7
|
+
* --psi-navbar-height: the NavBar owns it.
|
|
8
|
+
*
|
|
9
|
+
* sidebar-bg, sidebar-fg and focus-ring alias the semantic layer, so under a
|
|
10
|
+
* `data-psi-theme="dark"` sidebar inside a light page they re-resolve dark
|
|
11
|
+
* (component tokens are declared under `:where(:root, [data-psi-theme])`).
|
|
12
|
+
* The theme paints no background of its own: the sidebar paints sidebar-bg.
|
|
13
|
+
*
|
|
14
|
+
* No contrast pair added: fgPrimary on bgPrimary is already gated in every
|
|
15
|
+
* theme, dark included, and the pinned pair count does not move. */
|
|
16
|
+
export declare const appShellVars: Record<string, string>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** AppShell component tokens (--psi-app-shell-*) — D88.
|
|
2
|
+
*
|
|
3
|
+
* sidebar-width is a deliberate default for the sidebar column of the frame.
|
|
4
|
+
* 16rem is a standalone literal, matching --psi-toolbar-control-width: no
|
|
5
|
+
* shared scale reaches this range (sizeScale tops at 48, spacingScale at 144,
|
|
6
|
+
* both for heights and gaps, not column widths). The header's height stays
|
|
7
|
+
* --psi-navbar-height: the NavBar owns it.
|
|
8
|
+
*
|
|
9
|
+
* sidebar-bg, sidebar-fg and focus-ring alias the semantic layer, so under a
|
|
10
|
+
* `data-psi-theme="dark"` sidebar inside a light page they re-resolve dark
|
|
11
|
+
* (component tokens are declared under `:where(:root, [data-psi-theme])`).
|
|
12
|
+
* The theme paints no background of its own: the sidebar paints sidebar-bg.
|
|
13
|
+
*
|
|
14
|
+
* No contrast pair added: fgPrimary on bgPrimary is already gated in every
|
|
15
|
+
* theme, dark included, and the pinned pair count does not move. */
|
|
16
|
+
export const appShellVars = {
|
|
17
|
+
"sidebar-width": "16rem",
|
|
18
|
+
"sidebar-bg": "var(--psi-bg-primary)",
|
|
19
|
+
"sidebar-fg": "var(--psi-fg-primary)",
|
|
20
|
+
"focus-ring": "var(--psi-border-focus)",
|
|
21
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Banner component tokens (--psi-banner-*) — D86. Pure indirection, same
|
|
2
|
+
* posture as toast.ts (D64) and table.ts (D62).
|
|
3
|
+
*
|
|
4
|
+
* Unlike Toast, a Banner is a tinted surface: it spans the page edge to edge
|
|
5
|
+
* and is meant to be seen, so the variant tints the whole strip. Neutral sits
|
|
6
|
+
* on fillNeutral3, the status variants on the fillTint* family.
|
|
7
|
+
*
|
|
8
|
+
* The body text is fgPrimary on all four surfaces; fgPrimary on each
|
|
9
|
+
* fillTint* is gated at 4.5 in contrast-matrix.ts (D86). The icon foregrounds
|
|
10
|
+
* are not text and ride the status-foreground pairs already gated there. */
|
|
11
|
+
export declare const bannerVars: Record<string, string>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Banner component tokens (--psi-banner-*) — D86. Pure indirection, same
|
|
2
|
+
* posture as toast.ts (D64) and table.ts (D62).
|
|
3
|
+
*
|
|
4
|
+
* Unlike Toast, a Banner is a tinted surface: it spans the page edge to edge
|
|
5
|
+
* and is meant to be seen, so the variant tints the whole strip. Neutral sits
|
|
6
|
+
* on fillNeutral3, the status variants on the fillTint* family.
|
|
7
|
+
*
|
|
8
|
+
* The body text is fgPrimary on all four surfaces; fgPrimary on each
|
|
9
|
+
* fillTint* is gated at 4.5 in contrast-matrix.ts (D86). The icon foregrounds
|
|
10
|
+
* are not text and ride the status-foreground pairs already gated there. */
|
|
11
|
+
export const bannerVars = {
|
|
12
|
+
"bg-neutral": "var(--psi-fill-neutral3)",
|
|
13
|
+
"bg-success": "var(--psi-fill-tint-success)",
|
|
14
|
+
"bg-warning": "var(--psi-fill-tint-warning)",
|
|
15
|
+
"bg-danger": "var(--psi-fill-tint-danger)",
|
|
16
|
+
fg: "var(--psi-fg-primary)",
|
|
17
|
+
"icon-fg-neutral": "var(--psi-fg-secondary)",
|
|
18
|
+
"icon-fg-success": "var(--psi-fg-success)",
|
|
19
|
+
"icon-fg-warning": "var(--psi-fg-warning)",
|
|
20
|
+
"icon-fg-danger": "var(--psi-fg-danger)",
|
|
21
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
@layer psi.components {
|
|
2
|
+
:where(:root, [data-psi-theme]) {
|
|
3
|
+
--psi-banner-bg-neutral: var(--psi-fill-neutral3);
|
|
4
|
+
--psi-banner-bg-success: var(--psi-fill-tint-success);
|
|
5
|
+
--psi-banner-bg-warning: var(--psi-fill-tint-warning);
|
|
6
|
+
--psi-banner-bg-danger: var(--psi-fill-tint-danger);
|
|
7
|
+
--psi-banner-fg: var(--psi-fg-primary);
|
|
8
|
+
--psi-banner-icon-fg-neutral: var(--psi-fg-secondary);
|
|
9
|
+
--psi-banner-icon-fg-success: var(--psi-fg-success);
|
|
10
|
+
--psi-banner-icon-fg-warning: var(--psi-fg-warning);
|
|
11
|
+
--psi-banner-icon-fg-danger: var(--psi-fg-danger);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Button component variants */
|
|
2
|
+
export declare const BUTTON_VARIANTS: readonly ["accent", "accent-subtle", "neutral", "neutral-subtle", "ghost", "danger", "danger-subtle", "outline"];
|
|
3
|
+
export type ButtonVariant = (typeof BUTTON_VARIANTS)[number];
|
|
4
|
+
/** Button component tokens (--psi-button-*). Values may reference semantic
|
|
5
|
+
* tokens and the component's own tokens; hover/active derive from the
|
|
6
|
+
* component's own bg so one override retunes all states (principle 3). */
|
|
7
|
+
export declare const buttonVars: Record<string, string>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** Button component variants */
|
|
2
|
+
export const BUTTON_VARIANTS = [
|
|
3
|
+
"accent",
|
|
4
|
+
"accent-subtle",
|
|
5
|
+
"neutral",
|
|
6
|
+
"neutral-subtle",
|
|
7
|
+
"ghost",
|
|
8
|
+
"danger",
|
|
9
|
+
"danger-subtle",
|
|
10
|
+
"outline",
|
|
11
|
+
];
|
|
12
|
+
/** Button component tokens (--psi-button-*). Values may reference semantic
|
|
13
|
+
* tokens and the component's own tokens; hover/active derive from the
|
|
14
|
+
* component's own bg so one override retunes all states (principle 3). */
|
|
15
|
+
export const buttonVars = {
|
|
16
|
+
"accent-bg": "var(--psi-fill-accent)",
|
|
17
|
+
"accent-bg-hover": "oklch(from var(--psi-button-accent-bg) calc(l - 0.04) c h)",
|
|
18
|
+
"accent-bg-active": "oklch(from var(--psi-button-accent-bg) calc(l - 0.08) c h)",
|
|
19
|
+
"accent-fg": "var(--psi-fg-on-accent)",
|
|
20
|
+
"accent-subtle-bg": "var(--psi-fill-tint-accent)",
|
|
21
|
+
"accent-subtle-bg-hover": "oklch(from var(--psi-button-accent-subtle-bg) l c h / 0.18)",
|
|
22
|
+
"accent-subtle-bg-active": "oklch(from var(--psi-button-accent-subtle-bg) l c h / 0.24)",
|
|
23
|
+
"accent-subtle-fg": "var(--psi-fg-accent)",
|
|
24
|
+
"neutral-bg": "var(--psi-fill-neutral3)",
|
|
25
|
+
"neutral-bg-hover": "oklch(from var(--psi-button-neutral-bg) calc(l - 0.04) c h)",
|
|
26
|
+
"neutral-bg-active": "oklch(from var(--psi-button-neutral-bg) calc(l - 0.08) c h)",
|
|
27
|
+
"neutral-fg": "var(--psi-fg-primary)",
|
|
28
|
+
"neutral-subtle-bg": "var(--psi-fill-neutral1)",
|
|
29
|
+
"neutral-subtle-bg-hover": "oklch(from var(--psi-button-neutral-subtle-bg) calc(l - 0.04) c h)",
|
|
30
|
+
"neutral-subtle-bg-active": "oklch(from var(--psi-button-neutral-subtle-bg) calc(l - 0.08) c h)",
|
|
31
|
+
"neutral-subtle-fg": "var(--psi-fg-primary)",
|
|
32
|
+
"ghost-bg": "transparent",
|
|
33
|
+
"ghost-bg-hover": "var(--psi-fill-neutral3)",
|
|
34
|
+
"ghost-bg-active": "var(--psi-fill-neutral4)",
|
|
35
|
+
"ghost-fg": "var(--psi-fg-primary)",
|
|
36
|
+
"danger-bg": "var(--psi-fill-danger)",
|
|
37
|
+
"danger-bg-hover": "oklch(from var(--psi-button-danger-bg) calc(l - 0.04) c h)",
|
|
38
|
+
"danger-bg-active": "oklch(from var(--psi-button-danger-bg) calc(l - 0.08) c h)",
|
|
39
|
+
"danger-fg": "var(--psi-fg-static-white)",
|
|
40
|
+
"danger-subtle-bg": "var(--psi-fill-tint-danger)",
|
|
41
|
+
"danger-subtle-bg-hover": "oklch(from var(--psi-button-danger-subtle-bg) l c h / 0.18)",
|
|
42
|
+
"danger-subtle-bg-active": "oklch(from var(--psi-button-danger-subtle-bg) l c h / 0.24)",
|
|
43
|
+
"danger-subtle-fg": "var(--psi-fg-danger)",
|
|
44
|
+
"outline-bg": "transparent",
|
|
45
|
+
"outline-bg-hover": "var(--psi-fill-accent)",
|
|
46
|
+
"outline-bg-active": "oklch(from var(--psi-fill-accent) calc(l - 0.04) c h)",
|
|
47
|
+
"outline-fg": "var(--psi-fg-primary)",
|
|
48
|
+
// Hover fills accent; label follows the accent variant's binding (fgOnAccent,
|
|
49
|
+
// D37) — the literal "canvas label" fails AA in default light/dark.
|
|
50
|
+
"outline-fg-hover": "var(--psi-button-accent-fg)",
|
|
51
|
+
"outline-border": "var(--psi-border-strong)",
|
|
52
|
+
"focus-ring": "var(--psi-border-focus)",
|
|
53
|
+
// ── Shape (D56) — size-invariant; IconButton binds this too ──
|
|
54
|
+
radius: "var(--psi-control-radius)",
|
|
55
|
+
// ── Size ramp (D54) — label ramp; IconButton binds -height for both axes ──
|
|
56
|
+
"24-height": "var(--psi-control-24-height)",
|
|
57
|
+
"32-height": "var(--psi-control-32-height)",
|
|
58
|
+
"40-height": "var(--psi-control-40-height)",
|
|
59
|
+
"48-height": "var(--psi-control-48-height)",
|
|
60
|
+
"24-padding-inline": "var(--psi-control-24-padding-inline)",
|
|
61
|
+
"32-padding-inline": "var(--psi-control-32-padding-inline)",
|
|
62
|
+
"40-padding-inline": "var(--psi-control-40-padding-inline)",
|
|
63
|
+
"48-padding-inline": "var(--psi-control-48-padding-inline)",
|
|
64
|
+
"24-padding-inline-icon": "var(--psi-control-24-padding-inline-icon)",
|
|
65
|
+
"32-padding-inline-icon": "var(--psi-control-32-padding-inline-icon)",
|
|
66
|
+
"40-padding-inline-icon": "var(--psi-control-40-padding-inline-icon)",
|
|
67
|
+
"48-padding-inline-icon": "var(--psi-control-48-padding-inline-icon)",
|
|
68
|
+
"24-gap": "var(--psi-control-24-gap)",
|
|
69
|
+
"32-gap": "var(--psi-control-32-gap)",
|
|
70
|
+
"40-gap": "var(--psi-control-40-gap)",
|
|
71
|
+
"48-gap": "var(--psi-control-48-gap)",
|
|
72
|
+
"24-font": "var(--psi-control-24-font)",
|
|
73
|
+
"32-font": "var(--psi-control-32-font)",
|
|
74
|
+
"40-font": "var(--psi-control-40-font)",
|
|
75
|
+
"48-font": "var(--psi-control-48-font)",
|
|
76
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Checkbox component tokens (--psi-checkbox-*). */
|
|
2
|
+
export const checkboxVars = {
|
|
3
|
+
fg: "var(--psi-fg-primary)",
|
|
4
|
+
"box-bg": "var(--psi-bg-primary)",
|
|
5
|
+
"box-border": "var(--psi-border-neutral)",
|
|
6
|
+
"box-bg-checked": "var(--psi-fill-accent)",
|
|
7
|
+
"box-border-checked": "var(--psi-fill-accent)",
|
|
8
|
+
// D56: tracks a sharper theme down, never rounder than radius-4 — the box
|
|
9
|
+
// is ~16px, so the 8px control default would read as a circle.
|
|
10
|
+
"box-radius": "min(var(--psi-control-radius), var(--psi-radius-4))",
|
|
11
|
+
"check-fg": "var(--psi-fg-on-accent)",
|
|
12
|
+
"focus-ring": "var(--psi-border-focus)",
|
|
13
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Shared sized-control ramp (--psi-control-*) — D54/D55. A token-only family
|
|
2
|
+
* with no component behind it, same posture as surface.ts (D51): Button,
|
|
3
|
+
* IconButton, Input and Select alias it and nothing binds it directly (the
|
|
4
|
+
* psi/component-tokens-only stylelint rule makes that a lint error).
|
|
5
|
+
*
|
|
6
|
+
* Height and gap are shared across roles. Padding and font fork: a centred
|
|
7
|
+
* label wants more air than a left-aligned value, and labels are medium
|
|
8
|
+
* while values are regular. Input and Select both bind the value ramp, so
|
|
9
|
+
* they cannot diverge from each other — that is the point of the family.
|
|
10
|
+
*
|
|
11
|
+
* The icon inset is the label ramp minus the text inset, derived so a
|
|
12
|
+
* text-only control renders pixel-identical to pre-D54 output:
|
|
13
|
+
*
|
|
14
|
+
* size | p (icon side) | text inset | raw gap | icon side | icon-text | text side
|
|
15
|
+
* 24 | 6 | 2 | 2 | 6 | 4 | 8
|
|
16
|
+
* 32 | 8 | 4 | 4 | 8 | 8 | 12
|
|
17
|
+
* 40 | 12 | 4 | 4 | 12 | 8 | 16
|
|
18
|
+
* 48 | 16 | 4 | 4 | 16 | 8 | 20
|
|
19
|
+
*
|
|
20
|
+
* The emitted padding-inline-icon and gap tokens are the effective columns. */
|
|
21
|
+
export declare const controlVars: Record<string, string>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Shared sized-control ramp (--psi-control-*) — D54/D55. A token-only family
|
|
2
|
+
* with no component behind it, same posture as surface.ts (D51): Button,
|
|
3
|
+
* IconButton, Input and Select alias it and nothing binds it directly (the
|
|
4
|
+
* psi/component-tokens-only stylelint rule makes that a lint error).
|
|
5
|
+
*
|
|
6
|
+
* Height and gap are shared across roles. Padding and font fork: a centred
|
|
7
|
+
* label wants more air than a left-aligned value, and labels are medium
|
|
8
|
+
* while values are regular. Input and Select both bind the value ramp, so
|
|
9
|
+
* they cannot diverge from each other — that is the point of the family.
|
|
10
|
+
*
|
|
11
|
+
* The icon inset is the label ramp minus the text inset, derived so a
|
|
12
|
+
* text-only control renders pixel-identical to pre-D54 output:
|
|
13
|
+
*
|
|
14
|
+
* size | p (icon side) | text inset | raw gap | icon side | icon-text | text side
|
|
15
|
+
* 24 | 6 | 2 | 2 | 6 | 4 | 8
|
|
16
|
+
* 32 | 8 | 4 | 4 | 8 | 8 | 12
|
|
17
|
+
* 40 | 12 | 4 | 4 | 12 | 8 | 16
|
|
18
|
+
* 48 | 16 | 4 | 4 | 16 | 8 | 20
|
|
19
|
+
*
|
|
20
|
+
* The emitted padding-inline-icon and gap tokens are the effective columns. */
|
|
21
|
+
export const controlVars = {
|
|
22
|
+
// ── Shared across roles ────────────────────────────────────────
|
|
23
|
+
"24-height": "var(--psi-size-24)",
|
|
24
|
+
"32-height": "var(--psi-size-32)",
|
|
25
|
+
"40-height": "var(--psi-size-40)",
|
|
26
|
+
"48-height": "var(--psi-size-48)",
|
|
27
|
+
"24-gap": "var(--psi-space-4)",
|
|
28
|
+
"32-gap": "var(--psi-space-8)",
|
|
29
|
+
"40-gap": "var(--psi-space-8)",
|
|
30
|
+
"48-gap": "var(--psi-space-8)",
|
|
31
|
+
// ── Label ramp (Button, IconButton) ────────────────────────────
|
|
32
|
+
"24-padding-inline": "var(--psi-space-8)",
|
|
33
|
+
"32-padding-inline": "var(--psi-space-12)",
|
|
34
|
+
"40-padding-inline": "var(--psi-space-16)",
|
|
35
|
+
"48-padding-inline": "var(--psi-space-20)",
|
|
36
|
+
"24-padding-inline-icon": "var(--psi-space-6)",
|
|
37
|
+
"32-padding-inline-icon": "var(--psi-space-8)",
|
|
38
|
+
"40-padding-inline-icon": "var(--psi-space-12)",
|
|
39
|
+
"48-padding-inline-icon": "var(--psi-space-16)",
|
|
40
|
+
"24-font": "var(--psi-text-12-16-medium)",
|
|
41
|
+
"32-font": "var(--psi-text-14-20-medium)",
|
|
42
|
+
"40-font": "var(--psi-text-16-24-medium)",
|
|
43
|
+
"48-font": "var(--psi-text-18-28-medium)",
|
|
44
|
+
// ── Value ramp (Input, Select) ─────────────────────────────────
|
|
45
|
+
"value-24-padding-inline": "var(--psi-space-8)",
|
|
46
|
+
"value-32-padding-inline": "var(--psi-space-8)",
|
|
47
|
+
"value-40-padding-inline": "var(--psi-space-12)",
|
|
48
|
+
"value-48-padding-inline": "var(--psi-space-16)",
|
|
49
|
+
"value-24-font": "var(--psi-text-12-16-regular)",
|
|
50
|
+
"value-32-font": "var(--psi-text-14-20-regular)",
|
|
51
|
+
"value-40-font": "var(--psi-text-16-24-regular)",
|
|
52
|
+
"value-48-font": "var(--psi-text-18-28-regular)",
|
|
53
|
+
// ── Size-invariant ─────────────────────────────────────────────
|
|
54
|
+
// Radius does not ride the size ramp: border-radius sits on each
|
|
55
|
+
// component's base rule, not in its .size{n} blocks, and all four sizes
|
|
56
|
+
// resolve to radius-8 today. One dial keeps it that way and gives a theme
|
|
57
|
+
// a single control-shape override (D56).
|
|
58
|
+
radius: "var(--psi-radius-8)",
|
|
59
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** DescriptionList component tokens (--psi-description-list-*) — D70. Pure
|
|
2
|
+
* indirection onto the semantic foreground family; exists because component
|
|
3
|
+
* CSS may only bind its own family (psi/component-tokens-only).
|
|
4
|
+
*
|
|
5
|
+
* Colour only. Spacing comes from the global --psi-space-* scale, which the
|
|
6
|
+
* scope gate already allows component CSS to bind directly, so aliasing it
|
|
7
|
+
* here would add a layer that buys nothing. */
|
|
8
|
+
export declare const descriptionListVars: Record<string, string>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** DescriptionList component tokens (--psi-description-list-*) — D70. Pure
|
|
2
|
+
* indirection onto the semantic foreground family; exists because component
|
|
3
|
+
* CSS may only bind its own family (psi/component-tokens-only).
|
|
4
|
+
*
|
|
5
|
+
* Colour only. Spacing comes from the global --psi-space-* scale, which the
|
|
6
|
+
* scope gate already allows component CSS to bind directly, so aliasing it
|
|
7
|
+
* here would add a layer that buys nothing. */
|
|
8
|
+
export const descriptionListVars = {
|
|
9
|
+
/** The <dt>. Secondary by design: the value is the content, the term is its label. */
|
|
10
|
+
"term-fg": "var(--psi-fg-secondary)",
|
|
11
|
+
/** The <dd>. */
|
|
12
|
+
"value-fg": "var(--psi-fg-primary)",
|
|
13
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Dialog component tokens (--psi-dialog-*). Elevated surface — binds the
|
|
2
|
+
* shared --psi-surface-* family (D51); backdrop reuses the heavy scrim the
|
|
3
|
+
* NavBar glass treatment uses (D50). `fg` binds the panel's body foreground
|
|
4
|
+
* so the native <dialog> UA stylesheet's `color: CanvasText` never leaks
|
|
5
|
+
* through on dark themes (VR catch). */
|
|
6
|
+
export declare const dialogVars: Record<string, string>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Dialog component tokens (--psi-dialog-*). Elevated surface — binds the
|
|
2
|
+
* shared --psi-surface-* family (D51); backdrop reuses the heavy scrim the
|
|
3
|
+
* NavBar glass treatment uses (D50). `fg` binds the panel's body foreground
|
|
4
|
+
* so the native <dialog> UA stylesheet's `color: CanvasText` never leaks
|
|
5
|
+
* through on dark themes (VR catch). */
|
|
6
|
+
export const dialogVars = {
|
|
7
|
+
bg: "var(--psi-surface-bg)",
|
|
8
|
+
border: "var(--psi-surface-border)",
|
|
9
|
+
radius: "var(--psi-surface-radius)",
|
|
10
|
+
backdrop: "var(--psi-scrim-heavy)",
|
|
11
|
+
fg: "var(--psi-fg-primary)",
|
|
12
|
+
"title-fg": "var(--psi-fg-primary)",
|
|
13
|
+
// D82 — the ring on the <dialog> and on a drawer's scrolling panel, which
|
|
14
|
+
// are tab stops themselves; the controls inside carry their own.
|
|
15
|
+
"focus-ring": "var(--psi-border-focus)",
|
|
16
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Field component tokens (--psi-field-*). Label/message/error colors alias
|
|
2
|
+
* gated semantic fg tokens; gap is the label→control→message grid gap (D49). */
|
|
3
|
+
export const fieldVars = {
|
|
4
|
+
"label-fg": "var(--psi-fg-secondary)",
|
|
5
|
+
"message-fg": "var(--psi-fg-tertiary)",
|
|
6
|
+
"error-fg": "var(--psi-fg-danger)",
|
|
7
|
+
"marker-fg": "var(--psi-fg-danger)",
|
|
8
|
+
// D84: the word a required label shows instead of the asterisk. The label's
|
|
9
|
+
// own colour — gated at 4.5 on every surface — not marker-fg, which would
|
|
10
|
+
// make every required label read as an error, and not fg-tertiary, the one
|
|
11
|
+
// foreground the contrast matrix does not gate.
|
|
12
|
+
"required-text-fg": "var(--psi-fg-secondary)",
|
|
13
|
+
gap: "var(--psi-space-6)",
|
|
14
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** InlineAlert component tokens (--psi-inline-alert-*) — D86. Pure
|
|
2
|
+
* indirection, same posture as banner.ts.
|
|
3
|
+
*
|
|
4
|
+
* The same tinted surface as Banner, but it sits inside content, so it adds a
|
|
5
|
+
* 1px border in the variant's colour and a radius. Neutral sits on
|
|
6
|
+
* fillNeutral3 with borderNeutral; the status variants draw the border from
|
|
7
|
+
* the status foreground (fgSuccess and fgWarning carry the `border` scope
|
|
8
|
+
* since D86, so the D46 scope gate accepts these bindings).
|
|
9
|
+
*
|
|
10
|
+
* The body text is fgPrimary on all four surfaces; fgPrimary on each
|
|
11
|
+
* fillTint* is gated at 4.5 in contrast-matrix.ts (D86). */
|
|
12
|
+
export declare const inlineAlertVars: Record<string, string>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** InlineAlert component tokens (--psi-inline-alert-*) — D86. Pure
|
|
2
|
+
* indirection, same posture as banner.ts.
|
|
3
|
+
*
|
|
4
|
+
* The same tinted surface as Banner, but it sits inside content, so it adds a
|
|
5
|
+
* 1px border in the variant's colour and a radius. Neutral sits on
|
|
6
|
+
* fillNeutral3 with borderNeutral; the status variants draw the border from
|
|
7
|
+
* the status foreground (fgSuccess and fgWarning carry the `border` scope
|
|
8
|
+
* since D86, so the D46 scope gate accepts these bindings).
|
|
9
|
+
*
|
|
10
|
+
* The body text is fgPrimary on all four surfaces; fgPrimary on each
|
|
11
|
+
* fillTint* is gated at 4.5 in contrast-matrix.ts (D86). */
|
|
12
|
+
export const inlineAlertVars = {
|
|
13
|
+
"bg-neutral": "var(--psi-fill-neutral3)",
|
|
14
|
+
"bg-success": "var(--psi-fill-tint-success)",
|
|
15
|
+
"bg-warning": "var(--psi-fill-tint-warning)",
|
|
16
|
+
"bg-danger": "var(--psi-fill-tint-danger)",
|
|
17
|
+
fg: "var(--psi-fg-primary)",
|
|
18
|
+
"icon-fg-neutral": "var(--psi-fg-secondary)",
|
|
19
|
+
"icon-fg-success": "var(--psi-fg-success)",
|
|
20
|
+
"icon-fg-warning": "var(--psi-fg-warning)",
|
|
21
|
+
"icon-fg-danger": "var(--psi-fg-danger)",
|
|
22
|
+
"border-neutral": "var(--psi-border-neutral)",
|
|
23
|
+
"border-success": "var(--psi-fg-success)",
|
|
24
|
+
"border-warning": "var(--psi-fg-warning)",
|
|
25
|
+
"border-danger": "var(--psi-fg-danger)",
|
|
26
|
+
radius: "var(--psi-radius-8)",
|
|
27
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
@layer psi.components {
|
|
2
|
+
:where(:root, [data-psi-theme]) {
|
|
3
|
+
--psi-inline-alert-bg-neutral: var(--psi-fill-neutral3);
|
|
4
|
+
--psi-inline-alert-bg-success: var(--psi-fill-tint-success);
|
|
5
|
+
--psi-inline-alert-bg-warning: var(--psi-fill-tint-warning);
|
|
6
|
+
--psi-inline-alert-bg-danger: var(--psi-fill-tint-danger);
|
|
7
|
+
--psi-inline-alert-fg: var(--psi-fg-primary);
|
|
8
|
+
--psi-inline-alert-icon-fg-neutral: var(--psi-fg-secondary);
|
|
9
|
+
--psi-inline-alert-icon-fg-success: var(--psi-fg-success);
|
|
10
|
+
--psi-inline-alert-icon-fg-warning: var(--psi-fg-warning);
|
|
11
|
+
--psi-inline-alert-icon-fg-danger: var(--psi-fg-danger);
|
|
12
|
+
--psi-inline-alert-border-neutral: var(--psi-border-neutral);
|
|
13
|
+
--psi-inline-alert-border-success: var(--psi-fg-success);
|
|
14
|
+
--psi-inline-alert-border-warning: var(--psi-fg-warning);
|
|
15
|
+
--psi-inline-alert-border-danger: var(--psi-fg-danger);
|
|
16
|
+
--psi-inline-alert-radius: var(--psi-radius-8);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Input component tokens (--psi-input-*). */
|
|
2
|
+
export const inputVars = {
|
|
3
|
+
bg: "var(--psi-bg-primary)",
|
|
4
|
+
fg: "var(--psi-fg-primary)",
|
|
5
|
+
placeholder: "var(--psi-fg-tertiary)",
|
|
6
|
+
border: "var(--psi-border-neutral)",
|
|
7
|
+
"border-hover": "var(--psi-border-strong)",
|
|
8
|
+
"border-error": "var(--psi-fg-danger)",
|
|
9
|
+
"focus-ring": "var(--psi-border-focus)",
|
|
10
|
+
// ── Shape (D56) ──
|
|
11
|
+
radius: "var(--psi-control-radius)",
|
|
12
|
+
// ── Size ramp (D54) — value ramp, shared with Select ──
|
|
13
|
+
"24-height": "var(--psi-control-24-height)",
|
|
14
|
+
"32-height": "var(--psi-control-32-height)",
|
|
15
|
+
"40-height": "var(--psi-control-40-height)",
|
|
16
|
+
"48-height": "var(--psi-control-48-height)",
|
|
17
|
+
"24-padding-inline": "var(--psi-control-value-24-padding-inline)",
|
|
18
|
+
"32-padding-inline": "var(--psi-control-value-32-padding-inline)",
|
|
19
|
+
"40-padding-inline": "var(--psi-control-value-40-padding-inline)",
|
|
20
|
+
"48-padding-inline": "var(--psi-control-value-48-padding-inline)",
|
|
21
|
+
"24-font": "var(--psi-control-value-24-font)",
|
|
22
|
+
"32-font": "var(--psi-control-value-32-font)",
|
|
23
|
+
"40-font": "var(--psi-control-value-40-font)",
|
|
24
|
+
"48-font": "var(--psi-control-value-48-font)",
|
|
25
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Menu component tokens (--psi-menu-*) — D53. Container is pure indirection
|
|
2
|
+
* onto the shared surface family (same posture as panel.ts and dialog.ts);
|
|
3
|
+
* item states reuse Button's ghost recipe rather than introducing anchors, so
|
|
4
|
+
* a brand retuning --psi-surface-* gets Menu for free. item-fg-disabled binds
|
|
5
|
+
* --psi-fg-quaternary (not the plan's --psi-fg-muted, which does not exist);
|
|
6
|
+
* fg-quaternary is the lowest-contrast foreground available in the theme. */
|
|
7
|
+
export declare const menuVars: Record<string, string>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Menu component tokens (--psi-menu-*) — D53. Container is pure indirection
|
|
2
|
+
* onto the shared surface family (same posture as panel.ts and dialog.ts);
|
|
3
|
+
* item states reuse Button's ghost recipe rather than introducing anchors, so
|
|
4
|
+
* a brand retuning --psi-surface-* gets Menu for free. item-fg-disabled binds
|
|
5
|
+
* --psi-fg-quaternary (not the plan's --psi-fg-muted, which does not exist);
|
|
6
|
+
* fg-quaternary is the lowest-contrast foreground available in the theme. */
|
|
7
|
+
export const menuVars = {
|
|
8
|
+
bg: "var(--psi-surface-bg)",
|
|
9
|
+
border: "var(--psi-surface-border)",
|
|
10
|
+
radius: "var(--psi-surface-radius)",
|
|
11
|
+
fg: "var(--psi-fg-primary)",
|
|
12
|
+
"item-bg": "transparent",
|
|
13
|
+
"item-bg-hover": "var(--psi-fill-neutral3)",
|
|
14
|
+
"item-bg-active": "var(--psi-fill-neutral4)",
|
|
15
|
+
"item-fg": "var(--psi-fg-primary)",
|
|
16
|
+
"item-fg-danger": "var(--psi-fg-danger)",
|
|
17
|
+
"item-fg-disabled": "var(--psi-fg-quaternary)",
|
|
18
|
+
"separator-border": "var(--psi-border-faint)",
|
|
19
|
+
// D82 — the ring on a focused item; until then the browser drew its own.
|
|
20
|
+
"focus-ring": "var(--psi-border-focus)",
|
|
21
|
+
};
|