@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 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/` | Geist Sans + Geist Mono, self-hosted (two variable `.woff2`, 141 KB, SIL OFL-1.1). No Google Fonts request. |
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. |
@@ -1,4 +1,5 @@
1
1
  Copyright (c) 2023 Vercel, in collaboration with basement.studio
2
+ Copyright (c) 2026 Hanzo AI, Inc.
2
3
 
3
4
  This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
5
  This license is copied below, and is also available with a FAQ at:
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
+ }