@hanzo/design 0.5.16 → 0.5.18
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 +1 -1
- package/assets/fonts/{LICENSE-Geist.txt → LICENSE-Zen.txt} +1 -0
- package/assets/fonts/Zen-Variable.woff2 +0 -0
- package/assets/fonts/ZenMono-Variable.woff2 +0 -0
- package/dist/brand.cjs +62 -0
- package/dist/index.cjs +92 -0
- package/dist/preference.cjs +225 -0
- package/dist/tokens.gen.cjs +597 -0
- package/dist/tokens.gen.d.ts +4 -4
- package/dist/tokens.gen.js +4 -4
- package/guidelines/DESIGN.md +36 -26
- package/package.json +5 -5
- package/prompts/studio-designer.md +9 -2
- package/scripts/check-dist.mjs +20 -0
- package/scripts/check-tokens.mjs +6 -27
- package/scripts/cjs.mjs +43 -0
- package/scripts/gen-tokens.mjs +0 -78
- package/src/tokens.gen.ts +4 -4
- package/styles.css +28 -14
- package/tokens/fonts.css +27 -13
- package/assets/fonts/Geist-Variable.woff2 +0 -0
- package/assets/fonts/GeistMono-Variable.woff2 +0 -0
- package/tailwind.css +0 -1162
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ import { HanzoLogo } from "@hanzo/design/components/core/HanzoLogo.jsx";
|
|
|
81
81
|
| `content/` | The words — brand voice and taglines. |
|
|
82
82
|
| `docs/` | How to use the system — integrate the tokens, theme, extend. |
|
|
83
83
|
| `guidelines/` | Specimen cards — color, type, spacing, brand, iconography — the visual reference. |
|
|
84
|
-
| `assets/fonts/` |
|
|
84
|
+
| `assets/fonts/` | Zen + Zen Mono, self-hosted (two variable `.woff2`, 141 KB, SIL OFL-1.1). No Google Fonts request. |
|
|
85
85
|
| `assets/` | The mark, wordmark, favicon, provider + partner logos, brand imagery. |
|
|
86
86
|
| `scripts/check-tokens.mjs` | The gate: every token file is served, every internal `var()` resolves, and the contrast floors hold. Runs on `npm run build`. |
|
|
87
87
|
| `ui_kits/` | Composed surfaces (e.g. `SiteChrome`) assembled from the components. |
|
|
Binary file
|
|
Binary file
|
package/dist/brand.cjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// A brand's declared theme, projected onto the semantic tokens.
|
|
3
|
+
//
|
|
4
|
+
// A brand package (@luxfi/brand, @zooai/brand) carries its own light and dark
|
|
5
|
+
// theme in brand.json. This is the ONE place that says which semantic token each
|
|
6
|
+
// of those answers for — the design system owns the token contract, a brand owns
|
|
7
|
+
// the values, and neither holds a copy of the other.
|
|
8
|
+
//
|
|
9
|
+
// It exists because that mapping was about to be written twice: once in each
|
|
10
|
+
// brand package to generate a stylesheet, and once in every app that resolves its
|
|
11
|
+
// brand at runtime and cannot import a build-time stylesheet. Two copies of a
|
|
12
|
+
// mapping is how a brand comes to look like itself in one surface and not
|
|
13
|
+
// another.
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.themeToTokens = themeToTokens;
|
|
16
|
+
exports.applyBrandTheme = applyBrandTheme;
|
|
17
|
+
/** theme key → the semantic tokens it answers for. */
|
|
18
|
+
const OWNS = {
|
|
19
|
+
surface1: ['--background'],
|
|
20
|
+
surface2: ['--card', '--popover', '--muted'],
|
|
21
|
+
surface3: ['--accent'],
|
|
22
|
+
neutral1: ['--foreground', '--card-foreground', '--popover-foreground', '--accent-foreground'],
|
|
23
|
+
neutral2: ['--muted-foreground'],
|
|
24
|
+
neutral3: [],
|
|
25
|
+
accent1: ['--primary'],
|
|
26
|
+
accent2: [],
|
|
27
|
+
accent3: [],
|
|
28
|
+
border: ['--border'],
|
|
29
|
+
success: ['--state-success'],
|
|
30
|
+
warning: ['--state-warning'],
|
|
31
|
+
error: ['--state-error'],
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* The tokens a brand's theme sets, as `{ '--background': '#000', … }`.
|
|
35
|
+
*
|
|
36
|
+
* Only ground, ink, accent and the edges drawn on them. The radius scale, the
|
|
37
|
+
* type ramp, spacing, motion and z are NOT here and must not be: they are the
|
|
38
|
+
* system's grammar rather than a brand's voice, and a brand that redefines them
|
|
39
|
+
* is a fork wearing a stylesheet.
|
|
40
|
+
*/
|
|
41
|
+
function themeToTokens(theme) {
|
|
42
|
+
const out = {};
|
|
43
|
+
for (const [key, tokens] of Object.entries(OWNS)) {
|
|
44
|
+
const value = theme[key];
|
|
45
|
+
if (!value)
|
|
46
|
+
continue;
|
|
47
|
+
for (const token of tokens)
|
|
48
|
+
out[token] = value;
|
|
49
|
+
}
|
|
50
|
+
// --primary carries ink on top of it, so the accent names its own contrast
|
|
51
|
+
// partner rather than leaving whatever the previous theme set.
|
|
52
|
+
if (theme.accent1 && theme.surface1)
|
|
53
|
+
out['--primary-foreground'] = theme.surface1;
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/** Apply a brand's theme to a live document — for an app that resolves its brand
|
|
57
|
+
* at runtime and so cannot import a build-time stylesheet. */
|
|
58
|
+
function applyBrandTheme(theme, el) {
|
|
59
|
+
for (const [name, value] of Object.entries(themeToTokens(theme))) {
|
|
60
|
+
el.style.setProperty(name, value);
|
|
61
|
+
}
|
|
62
|
+
}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.RATIO_MAX = exports.RATIO_MIN = exports.TYPE_MAX = exports.TYPE_MIN = exports.isColor = exports.css = exports.vars = void 0;
|
|
18
|
+
exports.cssVar = cssVar;
|
|
19
|
+
exports.tokenValue = tokenValue;
|
|
20
|
+
exports.injectDesignCss = injectDesignCss;
|
|
21
|
+
// @hanzo/design — the ONE programmatic control plane for Hanzo's look & feel.
|
|
22
|
+
//
|
|
23
|
+
// The look/feel is authored ONCE as CSS custom properties in tokens/*.css
|
|
24
|
+
// (monochrome, dark-default — "one hue through an opacity ladder"). This module
|
|
25
|
+
// exposes those exact tokens to code, generated from the CSS so the two can
|
|
26
|
+
// never drift. Change a token in the CSS → the stylesheet AND every code
|
|
27
|
+
// consumer (the @hanzogui/shell theme, Tamagui, any TS surface) update together.
|
|
28
|
+
//
|
|
29
|
+
// import '@hanzo/design/styles.css' // the CSS layer (unchanged)
|
|
30
|
+
// import { colors, spacing, radius, cssVar } from '@hanzo/design' // the code layer
|
|
31
|
+
//
|
|
32
|
+
__exportStar(require("./tokens.gen.cjs"), exports);
|
|
33
|
+
__exportStar(require("./brand.cjs"), exports);
|
|
34
|
+
const tokens_gen_js_1 = require("./tokens.gen.cjs");
|
|
35
|
+
/**
|
|
36
|
+
* A `var(--name, <authored literal>)` reference to a token — the ONE way code
|
|
37
|
+
* should reach a token, so it resolves through the live CSS cascade (honoring
|
|
38
|
+
* the viewer's light/dark theme and any brand fork) rather than baking a value.
|
|
39
|
+
*
|
|
40
|
+
* background: cssVar('--background') // → "var(--background, #000000)"
|
|
41
|
+
* color: cssVar('foreground', '#fff')// → "var(--foreground, #fff)"
|
|
42
|
+
*
|
|
43
|
+
* The name is checked AGAINST THE STYLESHEET at compile time. That check used to
|
|
44
|
+
* be opted out of with `| (string & {})`, which is how `cssVar('surface-1')`
|
|
45
|
+
* shipped: the token did not exist, `var(--surface-1)` resolved to nothing, and
|
|
46
|
+
* a menu painted transparent with no error anywhere. An undefined custom
|
|
47
|
+
* property fails SILENTLY, so the type is the only place it can be caught.
|
|
48
|
+
*
|
|
49
|
+
* When no explicit fallback is given the token's own authored literal is used,
|
|
50
|
+
* so the reference still paints on a host that has not loaded the CSS layer.
|
|
51
|
+
*/
|
|
52
|
+
function cssVar(name, fallback) {
|
|
53
|
+
const n = (name.startsWith('--') ? name : `--${name}`);
|
|
54
|
+
const lit = fallback ?? tokens_gen_js_1.cssVars[n];
|
|
55
|
+
return lit ? `var(${n}, ${lit})` : `var(${n})`;
|
|
56
|
+
}
|
|
57
|
+
/** The raw authored value of a token (the literal from the CSS), or `undefined`. */
|
|
58
|
+
function tokenValue(name) {
|
|
59
|
+
return tokens_gen_js_1.cssVars[name];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Inject the design-system stylesheet from code (idempotent) for surfaces that
|
|
63
|
+
* cannot use a bundler CSS import (e.g. a runtime-mounted island). Prefer the
|
|
64
|
+
* static `import '@hanzo/design/styles.css'` where a bundler is available.
|
|
65
|
+
* No-op outside the browser.
|
|
66
|
+
*
|
|
67
|
+
* `href` is REQUIRED: this used to default to esm.sh, which silently made a
|
|
68
|
+
* third-party CDN the origin of the entire token layer for anyone who called it
|
|
69
|
+
* bare. Pass a URL you serve.
|
|
70
|
+
*/
|
|
71
|
+
function injectDesignCss(href) {
|
|
72
|
+
if (typeof document === 'undefined')
|
|
73
|
+
return;
|
|
74
|
+
if (document.querySelector('link[data-hanzo-design]'))
|
|
75
|
+
return;
|
|
76
|
+
const l = document.createElement('link');
|
|
77
|
+
l.rel = 'stylesheet';
|
|
78
|
+
l.href = href;
|
|
79
|
+
l.setAttribute('data-hanzo-design', '');
|
|
80
|
+
document.head.appendChild(l);
|
|
81
|
+
}
|
|
82
|
+
// A person's own reading of the system — type size, density, accent — as CSS
|
|
83
|
+
// custom properties. Pure: it maps a preference to variables and returns them,
|
|
84
|
+
// so an app, an embedded preview and a server render all apply it the same way.
|
|
85
|
+
var preference_js_1 = require("./preference.cjs");
|
|
86
|
+
Object.defineProperty(exports, "vars", { enumerable: true, get: function () { return preference_js_1.vars; } });
|
|
87
|
+
Object.defineProperty(exports, "css", { enumerable: true, get: function () { return preference_js_1.css; } });
|
|
88
|
+
Object.defineProperty(exports, "isColor", { enumerable: true, get: function () { return preference_js_1.isColor; } });
|
|
89
|
+
Object.defineProperty(exports, "TYPE_MIN", { enumerable: true, get: function () { return preference_js_1.TYPE_MIN; } });
|
|
90
|
+
Object.defineProperty(exports, "TYPE_MAX", { enumerable: true, get: function () { return preference_js_1.TYPE_MAX; } });
|
|
91
|
+
Object.defineProperty(exports, "RATIO_MIN", { enumerable: true, get: function () { return preference_js_1.RATIO_MIN; } });
|
|
92
|
+
Object.defineProperty(exports, "RATIO_MAX", { enumerable: true, get: function () { return preference_js_1.RATIO_MAX; } });
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* A person's own reading of the system: type size, density, accent.
|
|
4
|
+
*
|
|
5
|
+
* Three knobs, and each is ONE multiplier on a whole axis — never a restated
|
|
6
|
+
* ramp. The ramps live in `tokens/*.css`, authored once, and each rung carries
|
|
7
|
+
* its own `calc(<base> * var(--type-scale, 1))`. So a preference sets three
|
|
8
|
+
* numbers and every rung follows, including rungs added later and rungs this
|
|
9
|
+
* file has never heard of.
|
|
10
|
+
*
|
|
11
|
+
* That is not a style choice; it is the fix for a real bug. The first version of
|
|
12
|
+
* this module kept its own copy of the type ramp so it could recompute each
|
|
13
|
+
* rung, and the copy was WRONG — it had `lg: 1rem` and `xl: 1.125rem` (16px and
|
|
14
|
+
* 18px) while `tokens/typography.css` says `0.9375rem` and `1.0625rem` (15px and
|
|
15
|
+
* 17px). Setting a preference of 1 — "leave it alone" — would have silently
|
|
16
|
+
* resized two rungs of the published design. A second copy of a value is a
|
|
17
|
+
* second source of truth, and it drifted before anyone used it.
|
|
18
|
+
*
|
|
19
|
+
* Because the knobs are plain multipliers, any OTHER ramp can opt in the same
|
|
20
|
+
* way. @hanzo/gui compiles its own `--f-size-*` scale for the 1600-odd
|
|
21
|
+
* `fontSize="$n"` call sites in the apps; an app that redeclares those as
|
|
22
|
+
* `calc(<its px> * var(--type-scale, 1))` gets the same control with no change
|
|
23
|
+
* at scale 1.
|
|
24
|
+
*
|
|
25
|
+
* It is a pure function on purpose: it maps a preference to custom properties
|
|
26
|
+
* and returns them, touching no document. That is what lets an app, an embedded
|
|
27
|
+
* preview and a server render apply it identically.
|
|
28
|
+
*/
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.MODULAR_MAX = exports.MODULAR_MIN = exports.RATIO_MAX = exports.RATIO_MIN = exports.TYPE_MAX = exports.TYPE_MIN = void 0;
|
|
31
|
+
exports.isColor = isColor;
|
|
32
|
+
exports.vars = vars;
|
|
33
|
+
exports.css = css;
|
|
34
|
+
/**
|
|
35
|
+
* The type multiplier is CLAMPED, and the bounds are not arbitrary.
|
|
36
|
+
*
|
|
37
|
+
* Below 0.85 the smallest rung (--text-xs, 11px) drops under 9.4px, which stops
|
|
38
|
+
* being small and starts being unreadable — and a preference that lets someone
|
|
39
|
+
* render their own tools illegible is a trap, not a choice. Above 1.4 the
|
|
40
|
+
* chrome stops fitting its own containers: this app's builder header already
|
|
41
|
+
* overlaps its actions below 1440px at scale 1.
|
|
42
|
+
*/
|
|
43
|
+
exports.TYPE_MIN = 0.85;
|
|
44
|
+
exports.TYPE_MAX = 1.4;
|
|
45
|
+
/**
|
|
46
|
+
* The ratio's bounds are looser than type's, because the ramp defends its own
|
|
47
|
+
* floor.
|
|
48
|
+
*
|
|
49
|
+
* `tokens/typography.css` floors --text-xs/sm with `max()`, so the failure mode
|
|
50
|
+
* that forces type's tight clamp — a knob quietly rendering 9px labels — cannot
|
|
51
|
+
* happen on this axis however it combines with type. What is left to bound is
|
|
52
|
+
* only whether the ramp still READS as a ramp: at 0.75 the app register is
|
|
53
|
+
* within a hair of uniform, and past 1.5 a page's h2 has left its own body text
|
|
54
|
+
* behind entirely.
|
|
55
|
+
*/
|
|
56
|
+
exports.RATIO_MIN = 0.75;
|
|
57
|
+
exports.RATIO_MAX = 1.5;
|
|
58
|
+
/**
|
|
59
|
+
* A MODULAR scale — the classical one, where each display rung is the one below
|
|
60
|
+
* it times a fixed ratio. Golden is 1.618; the musical intervals designers name
|
|
61
|
+
* are 1.2 (minor third), 1.25 (major third), 1.333 (perfect fourth) and 1.5
|
|
62
|
+
* (perfect fifth).
|
|
63
|
+
*
|
|
64
|
+
* This is a different RULE from `ratio`, not another dial on it. `ratio` tunes
|
|
65
|
+
* the contrast of the ramp `tokens/typography.css` authored; this REPLACES that
|
|
66
|
+
* ramp's display half with a geometric one. Naming a golden-ratio preset as a
|
|
67
|
+
* contrast value would have been the dishonest version of this feature.
|
|
68
|
+
*
|
|
69
|
+
* Bounded below 1.05 because a ratio at 1 is not a scale — every display rung
|
|
70
|
+
* collapses onto the one before it — and above 2 because doubling every step
|
|
71
|
+
* puts the fourth rung past a phone's whole width.
|
|
72
|
+
*
|
|
73
|
+
* Golden is exactly 1.618 here and is NOT clamped down to something tamer,
|
|
74
|
+
* because a scale that quietly gives you not-golden when you asked for golden is
|
|
75
|
+
* worse than one that refuses. What makes that safe is the ceiling below: this
|
|
76
|
+
* ramp has EIGHT display rungs and a classical modular scale is used with about
|
|
77
|
+
* four, so at 1.618 the eighth would be 799px. Measured, --text-8xl and
|
|
78
|
+
* --text-9xl are referenced in 15 files across the fleet, so that is a broken
|
|
79
|
+
* page and not a hypothetical.
|
|
80
|
+
*/
|
|
81
|
+
exports.MODULAR_MIN = 1.05;
|
|
82
|
+
exports.MODULAR_MAX = 2;
|
|
83
|
+
/**
|
|
84
|
+
* The rungs a modular scale REGENERATES, in order, and the one it starts from.
|
|
85
|
+
*
|
|
86
|
+
* It deliberately stops at the display register. A geometric scale through the
|
|
87
|
+
* interface rungs is unusable at any real ratio: at 1.25 from a 14px base the
|
|
88
|
+
* next rungs are 17.5 and 21.9, so the 13px nav label and the 15px lead — the
|
|
89
|
+
* near-linear steps a dense interface is built on — do not exist. `xl` is the
|
|
90
|
+
* anchor because it is the last interface rung, so the display half continues
|
|
91
|
+
* from where the interface ends rather than restarting under it.
|
|
92
|
+
*/
|
|
93
|
+
const DISPLAY_RUNGS = ["2xl", "3xl", "4xl", "5xl", "6xl", "7xl", "8xl", "9xl"];
|
|
94
|
+
const MODULAR_ANCHOR_REM = 1.0625; // --text-xl, 17px
|
|
95
|
+
/**
|
|
96
|
+
* Density moves SPACING only, and its range is much tighter than type's.
|
|
97
|
+
*
|
|
98
|
+
* Spacing compounds: a page nests padding inside gap inside margin, so a 0.75
|
|
99
|
+
* multiplier is already three-quarters of every one of those in sequence. Below
|
|
100
|
+
* that, touch targets fall under the 44px floor `base.css` sets for coarse
|
|
101
|
+
* pointers, and the control that promised comfort takes it away.
|
|
102
|
+
*/
|
|
103
|
+
const DENSITY = {
|
|
104
|
+
compact: 0.85,
|
|
105
|
+
default: 1,
|
|
106
|
+
comfortable: 1.15,
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* A face is chosen from the ones `tokens/fonts.css` already declares, and it is
|
|
110
|
+
* set by REFERENCE — `var(--font-serif)`, never a family list restated here.
|
|
111
|
+
*
|
|
112
|
+
* That is what keeps a preference from pinning a face: when the token file
|
|
113
|
+
* changes what "mono" means, or a brand overrides it, a person who chose mono
|
|
114
|
+
* follows. Writing `Georgia, serif` here would freeze this file's idea of serif
|
|
115
|
+
* into every document that ever stored the preference.
|
|
116
|
+
*
|
|
117
|
+
* `default` is deliberately absent from the map rather than mapped to
|
|
118
|
+
* `var(--font-sans)`: the axis writes `--font-sans`, so resolving it to itself
|
|
119
|
+
* is a cycle, and "no opinion" is already how every other axis says default.
|
|
120
|
+
*/
|
|
121
|
+
const FACE = {
|
|
122
|
+
system: "ui-sans-serif, system-ui, -apple-system, sans-serif",
|
|
123
|
+
serif: "var(--font-serif)",
|
|
124
|
+
mono: "var(--font-mono)",
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* The measure moves the CONTAINERS, not the columns.
|
|
128
|
+
*
|
|
129
|
+
* `--grid-columns` is 12 because layouts are authored against 12; changing it
|
|
130
|
+
* re-flows every span a page declares and is a different page, not a wider one.
|
|
131
|
+
* What a reader actually means by "wider" is how far the text runs before it
|
|
132
|
+
* wraps, which is `--container-*` — so that is the knob, and the grid inside it
|
|
133
|
+
* is untouched.
|
|
134
|
+
*/
|
|
135
|
+
const MEASURE = {
|
|
136
|
+
narrow: { max: "64rem", prose: "40rem", wide: "58rem" },
|
|
137
|
+
wide: { max: "96rem", prose: "56rem", wide: "86rem" },
|
|
138
|
+
};
|
|
139
|
+
const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
|
|
140
|
+
/** Trim to 4dp so a multiplier cannot emit a 17-digit float into a stylesheet. */
|
|
141
|
+
const round = (n) => String(Math.round(n * 10000) / 10000);
|
|
142
|
+
/**
|
|
143
|
+
* Is this a colour, or is it something being smuggled into a style attribute?
|
|
144
|
+
*
|
|
145
|
+
* A preference is user input and its destination is CSS. `#fff`, `rgb(...)`,
|
|
146
|
+
* `oklch(...)` and the bare keywords are colours; anything carrying a `;`, a
|
|
147
|
+
* `}`, or a `url(` is trying to be a second declaration, and the answer is to
|
|
148
|
+
* drop the axis rather than to sanitise a string into something plausible.
|
|
149
|
+
*/
|
|
150
|
+
function isColor(v) {
|
|
151
|
+
const s = v.trim();
|
|
152
|
+
if (!s || s.length > 64)
|
|
153
|
+
return false;
|
|
154
|
+
if (/[;{}()]/.test(s) && !/^(rgb|rgba|hsl|hsla|oklch|oklab|lab|lch|color)\([^;{}]*\)$/i.test(s))
|
|
155
|
+
return false;
|
|
156
|
+
return (/^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(s) ||
|
|
157
|
+
/^(rgb|rgba|hsl|hsla|oklch|oklab|lab|lch|color)\([^;{}]*\)$/i.test(s) ||
|
|
158
|
+
/^[a-z]{3,20}$/i.test(s));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The custom properties a preference produces.
|
|
162
|
+
*
|
|
163
|
+
* Only the axes actually set appear, so an app can spread the result over
|
|
164
|
+
* whatever it already has without a default silently overriding a brand.
|
|
165
|
+
*/
|
|
166
|
+
function vars(p) {
|
|
167
|
+
const out = {};
|
|
168
|
+
if (typeof p.type === "number" && Number.isFinite(p.type)) {
|
|
169
|
+
out["--type-scale"] = round(clamp(p.type, exports.TYPE_MIN, exports.TYPE_MAX));
|
|
170
|
+
}
|
|
171
|
+
if (typeof p.ratio === "number" && Number.isFinite(p.ratio)) {
|
|
172
|
+
out["--type-ratio"] = round(clamp(p.ratio, exports.RATIO_MIN, exports.RATIO_MAX));
|
|
173
|
+
}
|
|
174
|
+
if (typeof p.modular === "number" && Number.isFinite(p.modular)) {
|
|
175
|
+
// Each display rung is the anchor times the ratio to its step, emitted as an
|
|
176
|
+
// explicit property. An INLINE custom property on :root outranks the
|
|
177
|
+
// stylesheet, so these simply replace the authored rungs — nothing has to be
|
|
178
|
+
// unset first, and the interface rungs the sheet declares are untouched
|
|
179
|
+
// because none is named here. It multiplies out with `--type-scale` for free,
|
|
180
|
+
// since every rung is still read through that ramp's own calc.
|
|
181
|
+
const r = clamp(p.modular, exports.MODULAR_MIN, exports.MODULAR_MAX);
|
|
182
|
+
DISPLAY_RUNGS.forEach((rung, i) => {
|
|
183
|
+
const rem = MODULAR_ANCHOR_REM * Math.pow(r, i + 1);
|
|
184
|
+
// THE SAME clamp the authored ramp carries, read from the same two names.
|
|
185
|
+
// A regenerated rung and an authored one must be bounded identically or
|
|
186
|
+
// the bound is a property of which code path produced the value, which is
|
|
187
|
+
// exactly the kind of "it depends" a design system exists to delete. The
|
|
188
|
+
// bounds are declared once, in tokens/typography.css, and referenced here
|
|
189
|
+
// — never restated, so moving one moves both.
|
|
190
|
+
out[`--text-${rung}`] =
|
|
191
|
+
`clamp(var(--text-floor), calc(${round(rem)}rem * var(--type-scale, 1)), var(--text-ceiling))`;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (p.density && p.density in DENSITY) {
|
|
195
|
+
out["--density"] = round(DENSITY[p.density]);
|
|
196
|
+
}
|
|
197
|
+
// Indexed, not `in`-checked: the maps omit `default`, so a lookup answers
|
|
198
|
+
// undefined for it and for anything stored that is not a face at all. One
|
|
199
|
+
// read, no cast, and an unknown value is refused by the same line that
|
|
200
|
+
// resolves a known one.
|
|
201
|
+
const face = p.font ? FACE[p.font] : undefined;
|
|
202
|
+
if (face)
|
|
203
|
+
out["--font-sans"] = face;
|
|
204
|
+
const measure = p.width ? MEASURE[p.width] : undefined;
|
|
205
|
+
if (measure) {
|
|
206
|
+
out["--container-max"] = measure.max;
|
|
207
|
+
out["--container-prose"] = measure.prose;
|
|
208
|
+
out["--container-wide"] = measure.wide;
|
|
209
|
+
}
|
|
210
|
+
if (p.accent && isColor(p.accent)) {
|
|
211
|
+
// Both names, because the ramp uses --primary for action surfaces and
|
|
212
|
+
// --accent for selection. One hue, stated once, landing on both.
|
|
213
|
+
out["--primary"] = p.accent.trim();
|
|
214
|
+
out["--accent"] = p.accent.trim();
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
/** `vars()` as a declaration block, for a <style> tag or an SSR inline. */
|
|
219
|
+
function css(p, selector = "html:root") {
|
|
220
|
+
const v = vars(p);
|
|
221
|
+
const keys = Object.keys(v);
|
|
222
|
+
if (!keys.length)
|
|
223
|
+
return "";
|
|
224
|
+
return `${selector}{${keys.map((k) => `${k}:${v[k]}`).join(";")}}`;
|
|
225
|
+
}
|