@hanzo/design 0.4.10 → 0.4.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/preference.d.ts +20 -14
- package/dist/preference.d.ts.map +1 -1
- package/dist/preference.js +35 -37
- package/dist/tokens.gen.d.ts +74 -70
- package/dist/tokens.gen.d.ts.map +1 -1
- package/dist/tokens.gen.js +74 -70
- package/package.json +2 -2
- package/scripts/check-dist.mjs +81 -0
- package/scripts/check-preference.mjs +66 -33
- package/src/index.ts +2 -2
- package/src/preference.ts +35 -37
- package/src/tokens.gen.ts +74 -70
- package/styles.css +40 -28
- package/tailwind.css +40 -28
- package/tokens/spacing.css +20 -14
- package/tokens/typography.css +20 -14
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import the BUILT package the way a consumer does.
|
|
3
|
+
*
|
|
4
|
+
* Everything else in this repo checks the sources: the token files, the
|
|
5
|
+
* generator's output, the preference contract. All of it passed while
|
|
6
|
+
* `dist/index.js` was unimportable — 0.4.10 and 0.4.11 both shipped a barrel
|
|
7
|
+
* that re-exported `'./preference'` with no extension, which TypeScript emits
|
|
8
|
+
* verbatim and Node ESM refuses to resolve. `pnpm build` was green, `pnpm test`
|
|
9
|
+
* was green, and `import '@hanzo/design'` threw ERR_MODULE_NOT_FOUND for every
|
|
10
|
+
* consumer.
|
|
11
|
+
*
|
|
12
|
+
* A test that reads source cannot see that. This one loads the artifact.
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
16
|
+
import { dirname, join } from 'node:path'
|
|
17
|
+
|
|
18
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
19
|
+
const root = join(here, '..')
|
|
20
|
+
|
|
21
|
+
let failed = 0
|
|
22
|
+
const check = async (name, fn) => {
|
|
23
|
+
try {
|
|
24
|
+
await fn()
|
|
25
|
+
console.log(` ok ${name}`)
|
|
26
|
+
} catch (e) {
|
|
27
|
+
failed++
|
|
28
|
+
console.error(` FAIL ${name}\n ${e.message}`)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log('dist:')
|
|
33
|
+
|
|
34
|
+
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
|
35
|
+
|
|
36
|
+
await check('every export the package advertises actually exists on disk', () => {
|
|
37
|
+
const missing = []
|
|
38
|
+
for (const [name, entry] of Object.entries(pkg.exports ?? {})) {
|
|
39
|
+
for (const target of typeof entry === 'string' ? [entry] : Object.values(entry)) {
|
|
40
|
+
if (typeof target !== 'string' || target.includes('*')) continue
|
|
41
|
+
if (!existsSync(join(root, target))) missing.push(`${name} -> ${target}`)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (missing.length) throw new Error(`exports pointing at nothing:\n ${missing.join('\n ')}`)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
await check('the barrel IMPORTS — the check that 0.4.10 and 0.4.11 needed', async () => {
|
|
48
|
+
const entry = join(root, 'dist/index.js')
|
|
49
|
+
if (!existsSync(entry)) throw new Error('dist/index.js is missing — run the build first')
|
|
50
|
+
const mod = await import(pathToFileURL(entry).href)
|
|
51
|
+
// The preference API is the part that was unreachable; naming it here means a
|
|
52
|
+
// future re-export losing its extension fails by name rather than by silence.
|
|
53
|
+
for (const name of ['vars', 'css', 'isColor', 'TYPE_MIN', 'TYPE_MAX', 'cssVar', 'tokenValue']) {
|
|
54
|
+
if (mod[name] === undefined) throw new Error(`dist/index.js does not export ${name}`)
|
|
55
|
+
}
|
|
56
|
+
if (typeof mod.vars !== 'function') throw new Error('vars is not callable from the built package')
|
|
57
|
+
const v = mod.vars({ type: 1.2, density: 'compact' })
|
|
58
|
+
if (v['--type-scale'] !== '1.2') throw new Error(`built vars() returned ${JSON.stringify(v)}`)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
await check('no emitted module uses an extensionless relative specifier', () => {
|
|
62
|
+
// The class of bug, not just the one instance: Node ESM resolves relative
|
|
63
|
+
// specifiers literally, so a missing `.js` is always a runtime failure.
|
|
64
|
+
const bad = []
|
|
65
|
+
for (const [, entry] of Object.entries(pkg.exports ?? {})) {
|
|
66
|
+
const target = typeof entry === 'string' ? entry : entry.import
|
|
67
|
+
if (typeof target !== 'string' || target.includes('*') || !target.endsWith('.js')) continue
|
|
68
|
+
const file = join(root, target)
|
|
69
|
+
if (!existsSync(file)) continue
|
|
70
|
+
for (const m of readFileSync(file, 'utf8').matchAll(/from\s+['"](\.[^'"]*)['"]/g)) {
|
|
71
|
+
if (!/\.(js|mjs|cjs|json|css)$/.test(m[1])) bad.push(`${target}: ${m[1]}`)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (bad.length) throw new Error(`extensionless relative imports:\n ${bad.join('\n ')}`)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
if (failed) {
|
|
78
|
+
console.error(`\n${failed} dist check(s) failed`)
|
|
79
|
+
process.exit(1)
|
|
80
|
+
}
|
|
81
|
+
console.log('dist: all checks passed')
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
* Preference contract, checked the way this package already checks tokens:
|
|
3
3
|
* a plain node script, no framework, run by `npm test`.
|
|
4
4
|
*
|
|
5
|
-
* The
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* The load-bearing check is the last one. The first version of preference.ts
|
|
6
|
+
* kept its own copy of the type ramp and the copy was WRONG (lg/xl were 16/18
|
|
7
|
+
* against the tokens' 15/17), so a preference of 1 — "leave it alone" — would
|
|
8
|
+
* have resized two rungs of the published design. Nothing caught it, because
|
|
9
|
+
* nothing compared the copy to the source. Now there is no copy: the knobs are
|
|
10
|
+
* multipliers and the ramps live only in tokens/*.css, and check 5 fails if a
|
|
11
|
+
* rung ever stops carrying its multiplier.
|
|
9
12
|
*/
|
|
10
13
|
import { readFileSync } from "node:fs";
|
|
11
14
|
import { fileURLToPath } from "node:url";
|
|
@@ -55,30 +58,60 @@ check("an unset preference changes nothing", () => {
|
|
|
55
58
|
eq(css({}), "");
|
|
56
59
|
});
|
|
57
60
|
|
|
58
|
-
check("type
|
|
59
|
-
const v = vars({ type: 2 });
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// 0.875 / 0.6875 must survive the transform
|
|
63
|
-
const ratio = base / xs;
|
|
64
|
-
ok(Math.abs(ratio - 0.875 / 0.6875) < 1e-9, `ratio drifted: ${ratio}`);
|
|
61
|
+
check("type is ONE knob, not a restated ramp", () => {
|
|
62
|
+
const v = vars({ type: 1.2 });
|
|
63
|
+
eq(Object.keys(v), ["--type-scale"], "type must emit exactly one name:");
|
|
64
|
+
eq(v["--type-scale"], "1.2");
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
check("type is CLAMPED — a preference cannot make the UI illegible", () => {
|
|
68
|
-
|
|
69
|
-
eq(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// the smallest rung must stay readable at the floor
|
|
73
|
-
ok(parseFloat(tiny["--text-xs"]) * 16 >= 9, "xs fell below 9px");
|
|
68
|
+
eq(vars({ type: 0.1 })["--type-scale"], String(TYPE_MIN), "min");
|
|
69
|
+
eq(vars({ type: 99 })["--type-scale"], String(TYPE_MAX), "max");
|
|
70
|
+
// the smallest rung must stay readable at the floor: 11px * TYPE_MIN
|
|
71
|
+
ok(11 * TYPE_MIN >= 9, "xs falls below 9px at the floor");
|
|
74
72
|
});
|
|
75
73
|
|
|
76
|
-
check("density moves
|
|
74
|
+
check("density moves spacing only, and never touches type", () => {
|
|
77
75
|
const v = vars({ density: "compact" });
|
|
78
|
-
|
|
79
|
-
ok(
|
|
80
|
-
ok(
|
|
81
|
-
ok(
|
|
76
|
+
eq(Object.keys(v), ["--density"]);
|
|
77
|
+
ok(Number(v["--density"]) < 1, "compact should tighten");
|
|
78
|
+
ok(Number(vars({ density: "comfortable" })["--density"]) > 1, "comfortable should loosen");
|
|
79
|
+
ok(!("--type-scale" in v), "density must not touch type");
|
|
80
|
+
// Spacing compounds through nesting, so the range stays tight enough that a
|
|
81
|
+
// 44px coarse-pointer target does not fall under the floor.
|
|
82
|
+
ok(Number(vars({ density: "compact" })["--density"]) >= 0.8, "compact is too tight to stay tappable");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
check("EVERY ramp rung carries its multiplier — no rung can opt out", () => {
|
|
86
|
+
// This is the check that the drifting copy would have failed.
|
|
87
|
+
const type = readFileSync(join(root, "tokens/typography.css"), "utf8");
|
|
88
|
+
const space = readFileSync(join(root, "tokens/spacing.css"), "utf8");
|
|
89
|
+
|
|
90
|
+
const bare = [];
|
|
91
|
+
for (const [file, text, knob, re] of [
|
|
92
|
+
["typography.css", type, "--type-scale", /--text-[a-z0-9]+:\s*([^;]+);/g],
|
|
93
|
+
["spacing.css", space, "--density", /--space-\d+:\s*([^;]+);/g],
|
|
94
|
+
]) {
|
|
95
|
+
for (const m of text.matchAll(re)) {
|
|
96
|
+
const value = m[1].trim();
|
|
97
|
+
if (value === "0") continue; // zero times anything is zero
|
|
98
|
+
if (value.startsWith("var(")) continue; // an alias inherits its target's calc
|
|
99
|
+
if (!value.includes(`var(${knob}`)) bare.push(`${file}: ${m[0].trim()}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
eq(bare, [], "rungs that do NOT scale with their knob:");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
check("a unitless leading stays a RATIO — scaling it would double-apply", () => {
|
|
106
|
+
const type = readFileSync(join(root, "tokens/typography.css"), "utf8");
|
|
107
|
+
const bad = [];
|
|
108
|
+
for (const m of type.matchAll(/--leading-[a-z0-9]+:\s*([^;]+);/g)) {
|
|
109
|
+
const v = m[1].trim();
|
|
110
|
+
// A ratio (`1.05`) already scales with the font size it multiplies.
|
|
111
|
+
if (/^[0-9.]+$/.test(v) === false) continue;
|
|
112
|
+
if (v.includes("var(")) bad.push(m[0].trim());
|
|
113
|
+
}
|
|
114
|
+
eq(bad, [], "unitless leadings must not carry a multiplier:");
|
|
82
115
|
});
|
|
83
116
|
|
|
84
117
|
check("a colour lands on both --primary and --accent", () => {
|
|
@@ -111,21 +144,21 @@ check("real colour notations are accepted", () => {
|
|
|
111
144
|
check("css() emits ONE block for the selector it is given", () => {
|
|
112
145
|
const out = css({ type: 1.1, density: "compact", accent: "#fff" }, ":root");
|
|
113
146
|
ok(out.startsWith(":root{") && out.endsWith("}"), out.slice(0, 40));
|
|
114
|
-
ok(!out.includes("}"+"{"), "emitted more than one block");
|
|
147
|
+
ok(!out.includes("}" + "{"), "emitted more than one block");
|
|
115
148
|
});
|
|
116
149
|
|
|
117
|
-
check("every emitted name is one the token files actually
|
|
150
|
+
check("every emitted name is one the token files actually read", () => {
|
|
118
151
|
// A variable nothing reads is a write into another document — the exact
|
|
119
|
-
// mistake this package exists to prevent.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
152
|
+
// mistake this package exists to prevent. The knobs are READ by the ramps
|
|
153
|
+
// (as var(--knob, 1)); the colours are DECLARED by colors.css.
|
|
154
|
+
const files = ["tokens/typography.css", "tokens/spacing.css", "tokens/grid.css", "tokens/colors.css"];
|
|
155
|
+
const text = files.map((f) => readFileSync(join(root, f), "utf8")).join("\n");
|
|
156
|
+
const declared = new Set([...text.matchAll(/^\s*(--[a-z0-9-]+)\s*:/gm)].map((m) => m[1]));
|
|
157
|
+
const read = new Set([...text.matchAll(/var\((--[a-z0-9-]+)/g)].map((m) => m[1]));
|
|
158
|
+
|
|
126
159
|
const emitted = Object.keys(vars({ type: 1.1, density: "compact", accent: "#fff" }));
|
|
127
|
-
const orphans = emitted.filter((k) => !declared.has(k));
|
|
128
|
-
eq(orphans, [], "emitted names no token file declares:");
|
|
160
|
+
const orphans = emitted.filter((k) => !declared.has(k) && !read.has(k));
|
|
161
|
+
eq(orphans, [], "emitted names no token file declares or reads:");
|
|
129
162
|
});
|
|
130
163
|
|
|
131
164
|
if (failed) {
|
package/src/index.ts
CHANGED
|
@@ -66,5 +66,5 @@ export function injectDesignCss(href: string): void {
|
|
|
66
66
|
// A person's own reading of the system — type size, density, accent — as CSS
|
|
67
67
|
// custom properties. Pure: it maps a preference to variables and returns them,
|
|
68
68
|
// so an app, an embedded preview and a server render all apply it the same way.
|
|
69
|
-
export { vars, css, isColor, TYPE_MIN, TYPE_MAX } from './preference';
|
|
70
|
-
export type { Preference, Density } from './preference';
|
|
69
|
+
export { vars, css, isColor, TYPE_MIN, TYPE_MAX } from './preference.js';
|
|
70
|
+
export type { Preference, Density } from './preference.js';
|
package/src/preference.ts
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A person's own reading of the system: type size, density, accent.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* design.
|
|
4
|
+
* Three knobs, and each is ONE multiplier on a whole axis — never a restated
|
|
5
|
+
* ramp. The ramps live in `tokens/*.css`, authored once, and each rung carries
|
|
6
|
+
* its own `calc(<base> * var(--type-scale, 1))`. So a preference sets three
|
|
7
|
+
* numbers and every rung follows, including rungs added later and rungs this
|
|
8
|
+
* file has never heard of.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* That is not a style choice; it is the fix for a real bug. The first version of
|
|
11
|
+
* this module kept its own copy of the type ramp so it could recompute each
|
|
12
|
+
* rung, and the copy was WRONG — it had `lg: 1rem` and `xl: 1.125rem` (16px and
|
|
13
|
+
* 18px) while `tokens/typography.css` says `0.9375rem` and `1.0625rem` (15px and
|
|
14
|
+
* 17px). Setting a preference of 1 — "leave it alone" — would have silently
|
|
15
|
+
* resized two rungs of the published design. A second copy of a value is a
|
|
16
|
+
* second source of truth, and it drifted before anyone used it.
|
|
15
17
|
*
|
|
16
|
-
*
|
|
18
|
+
* Because the knobs are plain multipliers, any OTHER ramp can opt in the same
|
|
19
|
+
* way. @hanzo/gui compiles its own `--f-size-*` scale for the 1600-odd
|
|
20
|
+
* `fontSize="$n"` call sites in the apps; an app that redeclares those as
|
|
21
|
+
* `calc(<its px> * var(--type-scale, 1))` gets the same control with no change
|
|
22
|
+
* at scale 1.
|
|
17
23
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
24
|
+
* It is a pure function on purpose: it maps a preference to custom properties
|
|
25
|
+
* and returns them, touching no document. That is what lets an app, an embedded
|
|
26
|
+
* preview and a server render apply it identically.
|
|
21
27
|
*/
|
|
22
28
|
|
|
23
29
|
export type Density = "compact" | "default" | "comfortable";
|
|
@@ -42,30 +48,24 @@ export interface Preference {
|
|
|
42
48
|
export const TYPE_MIN = 0.85;
|
|
43
49
|
export const TYPE_MAX = 1.4;
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Density moves SPACING only, and its range is much tighter than type's.
|
|
53
|
+
*
|
|
54
|
+
* Spacing compounds: a page nests padding inside gap inside margin, so a 0.75
|
|
55
|
+
* multiplier is already three-quarters of every one of those in sequence. Below
|
|
56
|
+
* that, touch targets fall under the 44px floor `base.css` sets for coarse
|
|
57
|
+
* pointers, and the control that promised comfort takes it away.
|
|
58
|
+
*/
|
|
59
|
+
const DENSITY: Record<Density, number> = {
|
|
60
|
+
compact: 0.85,
|
|
47
61
|
default: 1,
|
|
48
|
-
comfortable: 1.
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
/** The type ramp, by name, in rem at a 16px root — mirrors tokens/typography.css. */
|
|
52
|
-
const TEXT: Record<string, number> = {
|
|
53
|
-
xs: 0.6875, sm: 0.8125, base: 0.875, lg: 1, xl: 1.125,
|
|
54
|
-
"2xl": 1.3125, "3xl": 1.625, "4xl": 2, "5xl": 2.5,
|
|
55
|
-
"6xl": 3.25, "7xl": 4, "8xl": 5.25, "9xl": 7,
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
/** The gap ramp, in rem — mirrors tokens/grid.css. */
|
|
59
|
-
const GAP: Record<string, number> = {
|
|
60
|
-
"grid-gap-tight": 0.5,
|
|
61
|
-
"grid-gap": 1,
|
|
62
|
-
"grid-gap-loose": 1.5,
|
|
62
|
+
comfortable: 1.15,
|
|
63
63
|
};
|
|
64
64
|
|
|
65
65
|
const clamp = (n: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, n));
|
|
66
66
|
|
|
67
67
|
/** Trim to 4dp so a multiplier cannot emit a 17-digit float into a stylesheet. */
|
|
68
|
-
const
|
|
68
|
+
const round = (n: number) => String(Math.round(n * 10000) / 10000);
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* Is this a colour, or is it something being smuggled into a style attribute?
|
|
@@ -96,13 +96,11 @@ export function vars(p: Preference): Record<string, string> {
|
|
|
96
96
|
const out: Record<string, string> = {};
|
|
97
97
|
|
|
98
98
|
if (typeof p.type === "number" && Number.isFinite(p.type)) {
|
|
99
|
-
|
|
100
|
-
for (const [name, size] of Object.entries(TEXT)) out[`--text-${name}`] = rem(size * k);
|
|
99
|
+
out["--type-scale"] = round(clamp(p.type, TYPE_MIN, TYPE_MAX));
|
|
101
100
|
}
|
|
102
101
|
|
|
103
|
-
if (p.density && p.density in
|
|
104
|
-
|
|
105
|
-
for (const [name, size] of Object.entries(GAP)) out[`--${name}`] = rem(size * k);
|
|
102
|
+
if (p.density && p.density in DENSITY) {
|
|
103
|
+
out["--density"] = round(DENSITY[p.density]);
|
|
106
104
|
}
|
|
107
105
|
|
|
108
106
|
if (p.accent && isColor(p.accent)) {
|
package/src/tokens.gen.ts
CHANGED
|
@@ -96,31 +96,32 @@ export const colors = {
|
|
|
96
96
|
|
|
97
97
|
/** typography tokens (from tokens/typography.css). Values are raw CSS. */
|
|
98
98
|
export const typography = {
|
|
99
|
-
'
|
|
100
|
-
'
|
|
101
|
-
'
|
|
102
|
-
'
|
|
103
|
-
'
|
|
104
|
-
'
|
|
105
|
-
'
|
|
106
|
-
'
|
|
107
|
-
'
|
|
108
|
-
'
|
|
109
|
-
'
|
|
110
|
-
'
|
|
111
|
-
'
|
|
112
|
-
'
|
|
113
|
-
'
|
|
114
|
-
'
|
|
115
|
-
'
|
|
99
|
+
'type-scale': '1',
|
|
100
|
+
'text-xs': 'calc(0.6875rem * var(--type-scale, 1))',
|
|
101
|
+
'leading-xs': 'calc(1rem * var(--type-scale, 1))',
|
|
102
|
+
'text-sm': 'calc(0.8125rem * var(--type-scale, 1))',
|
|
103
|
+
'leading-sm': 'calc(1.15rem * var(--type-scale, 1))',
|
|
104
|
+
'text-base': 'calc(0.875rem * var(--type-scale, 1))',
|
|
105
|
+
'leading-base': 'calc(1.35rem * var(--type-scale, 1))',
|
|
106
|
+
'text-lg': 'calc(0.9375rem * var(--type-scale, 1))',
|
|
107
|
+
'leading-lg': 'calc(1.4rem * var(--type-scale, 1))',
|
|
108
|
+
'text-xl': 'calc(1.0625rem * var(--type-scale, 1))',
|
|
109
|
+
'leading-xl': 'calc(1.55rem * var(--type-scale, 1))',
|
|
110
|
+
'text-2xl': 'calc(1.3125rem * var(--type-scale, 1))',
|
|
111
|
+
'leading-2xl': 'calc(1.7rem * var(--type-scale, 1))',
|
|
112
|
+
'text-3xl': 'calc(1.625rem * var(--type-scale, 1))',
|
|
113
|
+
'leading-3xl': 'calc(1.95rem * var(--type-scale, 1))',
|
|
114
|
+
'text-4xl': 'calc(2rem * var(--type-scale, 1))',
|
|
115
|
+
'leading-4xl': 'calc(2.25rem * var(--type-scale, 1))',
|
|
116
|
+
'text-5xl': 'calc(2.5rem * var(--type-scale, 1))',
|
|
116
117
|
'leading-5xl': '1.05',
|
|
117
|
-
'text-6xl': '3.25rem',
|
|
118
|
+
'text-6xl': 'calc(3.25rem * var(--type-scale, 1))',
|
|
118
119
|
'leading-6xl': '1',
|
|
119
|
-
'text-7xl': '4rem',
|
|
120
|
+
'text-7xl': 'calc(4rem * var(--type-scale, 1))',
|
|
120
121
|
'leading-7xl': '1',
|
|
121
|
-
'text-8xl': '5.25rem',
|
|
122
|
+
'text-8xl': 'calc(5.25rem * var(--type-scale, 1))',
|
|
122
123
|
'leading-8xl': '1',
|
|
123
|
-
'text-9xl': '7rem',
|
|
124
|
+
'text-9xl': 'calc(7rem * var(--type-scale, 1))',
|
|
124
125
|
'leading-9xl': '1',
|
|
125
126
|
'font-size-xs': 'var(--text-xs)',
|
|
126
127
|
'font-size-sm': 'var(--text-sm)',
|
|
@@ -162,21 +163,22 @@ export const typography = {
|
|
|
162
163
|
|
|
163
164
|
/** spacing tokens (from tokens/spacing.css). Values are raw CSS. */
|
|
164
165
|
export const spacing = {
|
|
166
|
+
'density': '1',
|
|
165
167
|
'space-0': '0',
|
|
166
|
-
'space-1': '0.25rem',
|
|
167
|
-
'space-2': '0.5rem',
|
|
168
|
-
'space-3': '0.75rem',
|
|
169
|
-
'space-4': '1rem',
|
|
170
|
-
'space-5': '1.25rem',
|
|
171
|
-
'space-6': '1.5rem',
|
|
172
|
-
'space-8': '2rem',
|
|
173
|
-
'space-10': '2.5rem',
|
|
174
|
-
'space-12': '3rem',
|
|
175
|
-
'space-14': '3.5rem',
|
|
176
|
-
'space-16': '4rem',
|
|
177
|
-
'space-20': '5rem',
|
|
178
|
-
'space-24': '6rem',
|
|
179
|
-
'space-32': '8rem',
|
|
168
|
+
'space-1': 'calc(0.25rem * var(--density, 1))',
|
|
169
|
+
'space-2': 'calc(0.5rem * var(--density, 1))',
|
|
170
|
+
'space-3': 'calc(0.75rem * var(--density, 1))',
|
|
171
|
+
'space-4': 'calc(1rem * var(--density, 1))',
|
|
172
|
+
'space-5': 'calc(1.25rem * var(--density, 1))',
|
|
173
|
+
'space-6': 'calc(1.5rem * var(--density, 1))',
|
|
174
|
+
'space-8': 'calc(2rem * var(--density, 1))',
|
|
175
|
+
'space-10': 'calc(2.5rem * var(--density, 1))',
|
|
176
|
+
'space-12': 'calc(3rem * var(--density, 1))',
|
|
177
|
+
'space-14': 'calc(3.5rem * var(--density, 1))',
|
|
178
|
+
'space-16': 'calc(4rem * var(--density, 1))',
|
|
179
|
+
'space-20': 'calc(5rem * var(--density, 1))',
|
|
180
|
+
'space-24': 'calc(6rem * var(--density, 1))',
|
|
181
|
+
'space-32': 'calc(8rem * var(--density, 1))',
|
|
180
182
|
'golden-1': '0.25rem',
|
|
181
183
|
'golden-2': '0.405rem',
|
|
182
184
|
'golden-3': '0.654rem',
|
|
@@ -390,31 +392,32 @@ export const cssVars = {
|
|
|
390
392
|
'--chrome-dot-red': 'rgb(239 68 68 / .6)',
|
|
391
393
|
'--chrome-dot-yellow': 'rgb(234 179 8 / .6)',
|
|
392
394
|
'--chrome-dot-green': 'rgb(34 197 94 / .6)',
|
|
393
|
-
'--
|
|
394
|
-
'--
|
|
395
|
-
'--
|
|
396
|
-
'--
|
|
397
|
-
'--
|
|
398
|
-
'--
|
|
399
|
-
'--
|
|
400
|
-
'--
|
|
401
|
-
'--
|
|
402
|
-
'--
|
|
403
|
-
'--
|
|
404
|
-
'--
|
|
405
|
-
'--
|
|
406
|
-
'--
|
|
407
|
-
'--
|
|
408
|
-
'--
|
|
409
|
-
'--
|
|
395
|
+
'--type-scale': '1',
|
|
396
|
+
'--text-xs': 'calc(0.6875rem * var(--type-scale, 1))',
|
|
397
|
+
'--leading-xs': 'calc(1rem * var(--type-scale, 1))',
|
|
398
|
+
'--text-sm': 'calc(0.8125rem * var(--type-scale, 1))',
|
|
399
|
+
'--leading-sm': 'calc(1.15rem * var(--type-scale, 1))',
|
|
400
|
+
'--text-base': 'calc(0.875rem * var(--type-scale, 1))',
|
|
401
|
+
'--leading-base': 'calc(1.35rem * var(--type-scale, 1))',
|
|
402
|
+
'--text-lg': 'calc(0.9375rem * var(--type-scale, 1))',
|
|
403
|
+
'--leading-lg': 'calc(1.4rem * var(--type-scale, 1))',
|
|
404
|
+
'--text-xl': 'calc(1.0625rem * var(--type-scale, 1))',
|
|
405
|
+
'--leading-xl': 'calc(1.55rem * var(--type-scale, 1))',
|
|
406
|
+
'--text-2xl': 'calc(1.3125rem * var(--type-scale, 1))',
|
|
407
|
+
'--leading-2xl': 'calc(1.7rem * var(--type-scale, 1))',
|
|
408
|
+
'--text-3xl': 'calc(1.625rem * var(--type-scale, 1))',
|
|
409
|
+
'--leading-3xl': 'calc(1.95rem * var(--type-scale, 1))',
|
|
410
|
+
'--text-4xl': 'calc(2rem * var(--type-scale, 1))',
|
|
411
|
+
'--leading-4xl': 'calc(2.25rem * var(--type-scale, 1))',
|
|
412
|
+
'--text-5xl': 'calc(2.5rem * var(--type-scale, 1))',
|
|
410
413
|
'--leading-5xl': '1.05',
|
|
411
|
-
'--text-6xl': '3.25rem',
|
|
414
|
+
'--text-6xl': 'calc(3.25rem * var(--type-scale, 1))',
|
|
412
415
|
'--leading-6xl': '1',
|
|
413
|
-
'--text-7xl': '4rem',
|
|
416
|
+
'--text-7xl': 'calc(4rem * var(--type-scale, 1))',
|
|
414
417
|
'--leading-7xl': '1',
|
|
415
|
-
'--text-8xl': '5.25rem',
|
|
418
|
+
'--text-8xl': 'calc(5.25rem * var(--type-scale, 1))',
|
|
416
419
|
'--leading-8xl': '1',
|
|
417
|
-
'--text-9xl': '7rem',
|
|
420
|
+
'--text-9xl': 'calc(7rem * var(--type-scale, 1))',
|
|
418
421
|
'--leading-9xl': '1',
|
|
419
422
|
'--font-size-xs': 'var(--text-xs)',
|
|
420
423
|
'--font-size-sm': 'var(--text-sm)',
|
|
@@ -452,21 +455,22 @@ export const cssVars = {
|
|
|
452
455
|
'--type-caption': '400 var(--text-xs)/var(--leading-xs) var(--font-sans)',
|
|
453
456
|
'--type-code': '400 var(--text-sm)/var(--leading-relaxed) var(--font-mono)',
|
|
454
457
|
'--type-eyebrow': '600 0.625rem/1 var(--font-sans)',
|
|
458
|
+
'--density': '1',
|
|
455
459
|
'--space-0': '0',
|
|
456
|
-
'--space-1': '0.25rem',
|
|
457
|
-
'--space-2': '0.5rem',
|
|
458
|
-
'--space-3': '0.75rem',
|
|
459
|
-
'--space-4': '1rem',
|
|
460
|
-
'--space-5': '1.25rem',
|
|
461
|
-
'--space-6': '1.5rem',
|
|
462
|
-
'--space-8': '2rem',
|
|
463
|
-
'--space-10': '2.5rem',
|
|
464
|
-
'--space-12': '3rem',
|
|
465
|
-
'--space-14': '3.5rem',
|
|
466
|
-
'--space-16': '4rem',
|
|
467
|
-
'--space-20': '5rem',
|
|
468
|
-
'--space-24': '6rem',
|
|
469
|
-
'--space-32': '8rem',
|
|
460
|
+
'--space-1': 'calc(0.25rem * var(--density, 1))',
|
|
461
|
+
'--space-2': 'calc(0.5rem * var(--density, 1))',
|
|
462
|
+
'--space-3': 'calc(0.75rem * var(--density, 1))',
|
|
463
|
+
'--space-4': 'calc(1rem * var(--density, 1))',
|
|
464
|
+
'--space-5': 'calc(1.25rem * var(--density, 1))',
|
|
465
|
+
'--space-6': 'calc(1.5rem * var(--density, 1))',
|
|
466
|
+
'--space-8': 'calc(2rem * var(--density, 1))',
|
|
467
|
+
'--space-10': 'calc(2.5rem * var(--density, 1))',
|
|
468
|
+
'--space-12': 'calc(3rem * var(--density, 1))',
|
|
469
|
+
'--space-14': 'calc(3.5rem * var(--density, 1))',
|
|
470
|
+
'--space-16': 'calc(4rem * var(--density, 1))',
|
|
471
|
+
'--space-20': 'calc(5rem * var(--density, 1))',
|
|
472
|
+
'--space-24': 'calc(6rem * var(--density, 1))',
|
|
473
|
+
'--space-32': 'calc(8rem * var(--density, 1))',
|
|
470
474
|
'--golden-1': '0.25rem',
|
|
471
475
|
'--golden-2': '0.405rem',
|
|
472
476
|
'--golden-3': '0.654rem',
|
package/styles.css
CHANGED
|
@@ -290,19 +290,25 @@
|
|
|
290
290
|
— the two are the SAME scale, mirrored. A surface/tenant overrides any --text-*
|
|
291
291
|
on :root to retune density on demand. */
|
|
292
292
|
:root{
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
--
|
|
298
|
-
|
|
299
|
-
--text-
|
|
300
|
-
--text-
|
|
301
|
-
--text-
|
|
302
|
-
--text-
|
|
303
|
-
--text-
|
|
304
|
-
--text-
|
|
305
|
-
--text-
|
|
293
|
+
/* The type knob. 1 is the published scale; every rung below multiplies by
|
|
294
|
+
it, so a surface or a person retunes the WHOLE ramp by setting one
|
|
295
|
+
number and no rung can drift out of relation with the others.
|
|
296
|
+
@hanzo/design's `vars({type})` emits exactly this name. */
|
|
297
|
+
--type-scale:1;
|
|
298
|
+
|
|
299
|
+
--text-xs:calc(0.6875rem * var(--type-scale, 1)); --leading-xs:calc(1rem * var(--type-scale, 1)); /* 11px — eyebrows / section labels */
|
|
300
|
+
--text-sm:calc(0.8125rem * var(--type-scale, 1)); --leading-sm:calc(1.15rem * var(--type-scale, 1)); /* 13px — nav labels, dense body */
|
|
301
|
+
--text-base:calc(0.875rem * var(--type-scale, 1)); --leading-base:calc(1.35rem * var(--type-scale, 1)); /* 14px — base app text (was 16px) */
|
|
302
|
+
--text-lg:calc(0.9375rem * var(--type-scale, 1)); --leading-lg:calc(1.4rem * var(--type-scale, 1)); /* 15px */
|
|
303
|
+
--text-xl:calc(1.0625rem * var(--type-scale, 1)); --leading-xl:calc(1.55rem * var(--type-scale, 1)); /* 17px */
|
|
304
|
+
--text-2xl:calc(1.3125rem * var(--type-scale, 1)); --leading-2xl:calc(1.7rem * var(--type-scale, 1)); /* 21px */
|
|
305
|
+
--text-3xl:calc(1.625rem * var(--type-scale, 1)); --leading-3xl:calc(1.95rem * var(--type-scale, 1)); /* 26px */
|
|
306
|
+
--text-4xl:calc(2rem * var(--type-scale, 1)); --leading-4xl:calc(2.25rem * var(--type-scale, 1)); /* 32px */
|
|
307
|
+
--text-5xl:calc(2.5rem * var(--type-scale, 1)); --leading-5xl:1.05; /* 40px */
|
|
308
|
+
--text-6xl:calc(3.25rem * var(--type-scale, 1)); --leading-6xl:1; /* 52px */
|
|
309
|
+
--text-7xl:calc(4rem * var(--type-scale, 1)); --leading-7xl:1; /* 64px */
|
|
310
|
+
--text-8xl:calc(5.25rem * var(--type-scale, 1)); --leading-8xl:1; /* 84px */
|
|
311
|
+
--text-9xl:calc(7rem * var(--type-scale, 1)); --leading-9xl:1; /* 112px */
|
|
306
312
|
|
|
307
313
|
/* The SAME scale under @hanzo/brand's spelling. @hanzo/gui's shell theme and
|
|
308
314
|
Hanzo Studio address the ramp as --font-size-*; both names are one value, so
|
|
@@ -367,7 +373,7 @@
|
|
|
367
373
|
form feels broken. pointer:coarse is the real signal — a desktop mouse keeps
|
|
368
374
|
the compact 13px field. */
|
|
369
375
|
@media (pointer:coarse){
|
|
370
|
-
:root{--text-control:1rem}
|
|
376
|
+
:root{--text-control:calc(1rem * var(--type-scale, 1))}
|
|
371
377
|
}
|
|
372
378
|
|
|
373
379
|
/* ── tokens/spacing.css ─────────────────────────────────────── */
|
|
@@ -375,21 +381,27 @@
|
|
|
375
381
|
declared in hanzo.ai's tailwind.config.ts (legacy v3 config, kept for
|
|
376
382
|
reference) — use it for editorial layouts, not for component padding. */
|
|
377
383
|
:root{
|
|
384
|
+
/* The density knob. 1 is the published spacing; every --space-* rung below
|
|
385
|
+
multiplies by it, and --grid-gap-* reference those rungs rather than
|
|
386
|
+
restating them, so gaps, padding and section rhythm move together.
|
|
387
|
+
@hanzo/design's `vars({density})` emits exactly this name. */
|
|
388
|
+
--density:1;
|
|
389
|
+
|
|
378
390
|
--space-0:0;
|
|
379
|
-
--space-1:0.25rem;
|
|
380
|
-
--space-2:0.5rem;
|
|
381
|
-
--space-3:0.75rem;
|
|
382
|
-
--space-4:1rem;
|
|
383
|
-
--space-5:1.25rem;
|
|
384
|
-
--space-6:1.5rem;
|
|
385
|
-
--space-8:2rem;
|
|
386
|
-
--space-10:2.5rem;
|
|
387
|
-
--space-12:3rem;
|
|
388
|
-
--space-14:3.5rem;
|
|
389
|
-
--space-16:4rem;
|
|
390
|
-
--space-20:5rem;
|
|
391
|
-
--space-24:6rem;
|
|
392
|
-
--space-32:8rem;
|
|
391
|
+
--space-1:calc(0.25rem * var(--density, 1));
|
|
392
|
+
--space-2:calc(0.5rem * var(--density, 1));
|
|
393
|
+
--space-3:calc(0.75rem * var(--density, 1));
|
|
394
|
+
--space-4:calc(1rem * var(--density, 1));
|
|
395
|
+
--space-5:calc(1.25rem * var(--density, 1));
|
|
396
|
+
--space-6:calc(1.5rem * var(--density, 1));
|
|
397
|
+
--space-8:calc(2rem * var(--density, 1));
|
|
398
|
+
--space-10:calc(2.5rem * var(--density, 1));
|
|
399
|
+
--space-12:calc(3rem * var(--density, 1));
|
|
400
|
+
--space-14:calc(3.5rem * var(--density, 1));
|
|
401
|
+
--space-16:calc(4rem * var(--density, 1));
|
|
402
|
+
--space-20:calc(5rem * var(--density, 1));
|
|
403
|
+
--space-24:calc(6rem * var(--density, 1));
|
|
404
|
+
--space-32:calc(8rem * var(--density, 1));
|
|
393
405
|
|
|
394
406
|
/* golden ramp (φ) — hanzo.ai tailwind.config.ts */
|
|
395
407
|
--golden-1:0.25rem;
|