@hanzo/design 0.5.8 → 0.5.10
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/preference.d.ts +30 -0
- package/dist/preference.d.ts.map +1 -1
- package/dist/preference.js +60 -0
- package/dist/tokens.gen.d.ts +2 -0
- package/dist/tokens.gen.d.ts.map +1 -1
- package/dist/tokens.gen.js +2 -0
- package/package.json +2 -2
- package/scripts/check-preference.mjs +36 -2
- package/scripts/gen-tokens.mjs +25 -1
- package/src/preference.ts +84 -0
- package/src/tokens.gen.ts +2 -0
- package/styles.css +38 -13
- package/tailwind.css +38 -13
- package/tokens/typography.css +38 -13
package/dist/preference.d.ts
CHANGED
|
@@ -26,10 +26,27 @@
|
|
|
26
26
|
* preview and a server render apply it identically.
|
|
27
27
|
*/
|
|
28
28
|
export type Density = "compact" | "default" | "comfortable";
|
|
29
|
+
/**
|
|
30
|
+
* The face, named by what it IS rather than by a family — the families are the
|
|
31
|
+
* token file's to choose, and it already names four.
|
|
32
|
+
*/
|
|
33
|
+
export type Face = "default" | "system" | "serif" | "mono";
|
|
34
|
+
/** How wide the page runs before it stops. */
|
|
35
|
+
export type Measure = "narrow" | "default" | "wide";
|
|
29
36
|
export interface Preference {
|
|
30
37
|
/** Multiplier on the type ramp. 1 is the published scale. */
|
|
31
38
|
type?: number;
|
|
39
|
+
/**
|
|
40
|
+
* How far APART the rungs sit — the ramp's contrast, where `type` is its size.
|
|
41
|
+
* 1 is the published scale; below 1 flattens it toward a uniform register,
|
|
42
|
+
* above 1 opens the display end and tightens the small one.
|
|
43
|
+
*/
|
|
44
|
+
ratio?: number;
|
|
32
45
|
density?: Density;
|
|
46
|
+
/** Which face the page is set in. */
|
|
47
|
+
font?: Face;
|
|
48
|
+
/** How wide the page runs — the measure, not the window. */
|
|
49
|
+
width?: Measure;
|
|
33
50
|
/** A CSS colour for --primary / --accent. Rejected unless it is one. */
|
|
34
51
|
accent?: string;
|
|
35
52
|
}
|
|
@@ -44,6 +61,19 @@ export interface Preference {
|
|
|
44
61
|
*/
|
|
45
62
|
export declare const TYPE_MIN = 0.85;
|
|
46
63
|
export declare const TYPE_MAX = 1.4;
|
|
64
|
+
/**
|
|
65
|
+
* The ratio's bounds are looser than type's, because the ramp defends its own
|
|
66
|
+
* floor.
|
|
67
|
+
*
|
|
68
|
+
* `tokens/typography.css` floors --text-xs/sm with `max()`, so the failure mode
|
|
69
|
+
* that forces type's tight clamp — a knob quietly rendering 9px labels — cannot
|
|
70
|
+
* happen on this axis however it combines with type. What is left to bound is
|
|
71
|
+
* only whether the ramp still READS as a ramp: at 0.75 the app register is
|
|
72
|
+
* within a hair of uniform, and past 1.5 a page's h2 has left its own body text
|
|
73
|
+
* behind entirely.
|
|
74
|
+
*/
|
|
75
|
+
export declare const RATIO_MIN = 0.75;
|
|
76
|
+
export declare const RATIO_MAX = 1.5;
|
|
47
77
|
/**
|
|
48
78
|
* Is this a colour, or is it something being smuggled into a style attribute?
|
|
49
79
|
*
|
package/dist/preference.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preference.d.ts","sourceRoot":"","sources":["../src/preference.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,MAAM,MAAM,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,aAAa,CAAC;AAE5D,MAAM,WAAW,UAAU;IACzB,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ,OAAO,CAAC;AAC7B,eAAO,MAAM,QAAQ,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"preference.d.ts","sourceRoot":"","sources":["../src/preference.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,MAAM,MAAM,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,aAAa,CAAC;AAE5D;;;GAGG;AACH,MAAM,MAAM,IAAI,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;AAE3D,8CAA8C;AAC9C,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC;AAEpD,MAAM,WAAW,UAAU;IACzB,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qCAAqC;IACrC,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,4DAA4D;IAC5D,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ,OAAO,CAAC;AAC7B,eAAO,MAAM,QAAQ,MAAM,CAAC;AAE5B;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,OAAO,CAAC;AAC9B,eAAO,MAAM,SAAS,MAAM,CAAC;AAsD7B;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAS1C;AAED;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAqC1D;AAED,2EAA2E;AAC3E,wBAAgB,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,QAAQ,SAAc,GAAG,MAAM,CAKjE"}
|
package/dist/preference.js
CHANGED
|
@@ -36,6 +36,19 @@
|
|
|
36
36
|
*/
|
|
37
37
|
export const TYPE_MIN = 0.85;
|
|
38
38
|
export const TYPE_MAX = 1.4;
|
|
39
|
+
/**
|
|
40
|
+
* The ratio's bounds are looser than type's, because the ramp defends its own
|
|
41
|
+
* floor.
|
|
42
|
+
*
|
|
43
|
+
* `tokens/typography.css` floors --text-xs/sm with `max()`, so the failure mode
|
|
44
|
+
* that forces type's tight clamp — a knob quietly rendering 9px labels — cannot
|
|
45
|
+
* happen on this axis however it combines with type. What is left to bound is
|
|
46
|
+
* only whether the ramp still READS as a ramp: at 0.75 the app register is
|
|
47
|
+
* within a hair of uniform, and past 1.5 a page's h2 has left its own body text
|
|
48
|
+
* behind entirely.
|
|
49
|
+
*/
|
|
50
|
+
export const RATIO_MIN = 0.75;
|
|
51
|
+
export const RATIO_MAX = 1.5;
|
|
39
52
|
/**
|
|
40
53
|
* Density moves SPACING only, and its range is much tighter than type's.
|
|
41
54
|
*
|
|
@@ -49,6 +62,37 @@ const DENSITY = {
|
|
|
49
62
|
default: 1,
|
|
50
63
|
comfortable: 1.15,
|
|
51
64
|
};
|
|
65
|
+
/**
|
|
66
|
+
* A face is chosen from the ones `tokens/fonts.css` already declares, and it is
|
|
67
|
+
* set by REFERENCE — `var(--font-serif)`, never a family list restated here.
|
|
68
|
+
*
|
|
69
|
+
* That is what keeps a preference from pinning a face: when the token file
|
|
70
|
+
* changes what "mono" means, or a brand overrides it, a person who chose mono
|
|
71
|
+
* follows. Writing `Georgia, serif` here would freeze this file's idea of serif
|
|
72
|
+
* into every document that ever stored the preference.
|
|
73
|
+
*
|
|
74
|
+
* `default` is deliberately absent from the map rather than mapped to
|
|
75
|
+
* `var(--font-sans)`: the axis writes `--font-sans`, so resolving it to itself
|
|
76
|
+
* is a cycle, and "no opinion" is already how every other axis says default.
|
|
77
|
+
*/
|
|
78
|
+
const FACE = {
|
|
79
|
+
system: "ui-sans-serif, system-ui, -apple-system, sans-serif",
|
|
80
|
+
serif: "var(--font-serif)",
|
|
81
|
+
mono: "var(--font-mono)",
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* The measure moves the CONTAINERS, not the columns.
|
|
85
|
+
*
|
|
86
|
+
* `--grid-columns` is 12 because layouts are authored against 12; changing it
|
|
87
|
+
* re-flows every span a page declares and is a different page, not a wider one.
|
|
88
|
+
* What a reader actually means by "wider" is how far the text runs before it
|
|
89
|
+
* wraps, which is `--container-*` — so that is the knob, and the grid inside it
|
|
90
|
+
* is untouched.
|
|
91
|
+
*/
|
|
92
|
+
const MEASURE = {
|
|
93
|
+
narrow: { max: "64rem", prose: "40rem", wide: "58rem" },
|
|
94
|
+
wide: { max: "96rem", prose: "56rem", wide: "86rem" },
|
|
95
|
+
};
|
|
52
96
|
const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
|
|
53
97
|
/** Trim to 4dp so a multiplier cannot emit a 17-digit float into a stylesheet. */
|
|
54
98
|
const round = (n) => String(Math.round(n * 10000) / 10000);
|
|
@@ -81,9 +125,25 @@ export function vars(p) {
|
|
|
81
125
|
if (typeof p.type === "number" && Number.isFinite(p.type)) {
|
|
82
126
|
out["--type-scale"] = round(clamp(p.type, TYPE_MIN, TYPE_MAX));
|
|
83
127
|
}
|
|
128
|
+
if (typeof p.ratio === "number" && Number.isFinite(p.ratio)) {
|
|
129
|
+
out["--type-ratio"] = round(clamp(p.ratio, RATIO_MIN, RATIO_MAX));
|
|
130
|
+
}
|
|
84
131
|
if (p.density && p.density in DENSITY) {
|
|
85
132
|
out["--density"] = round(DENSITY[p.density]);
|
|
86
133
|
}
|
|
134
|
+
// Indexed, not `in`-checked: the maps omit `default`, so a lookup answers
|
|
135
|
+
// undefined for it and for anything stored that is not a face at all. One
|
|
136
|
+
// read, no cast, and an unknown value is refused by the same line that
|
|
137
|
+
// resolves a known one.
|
|
138
|
+
const face = p.font ? FACE[p.font] : undefined;
|
|
139
|
+
if (face)
|
|
140
|
+
out["--font-sans"] = face;
|
|
141
|
+
const measure = p.width ? MEASURE[p.width] : undefined;
|
|
142
|
+
if (measure) {
|
|
143
|
+
out["--container-max"] = measure.max;
|
|
144
|
+
out["--container-prose"] = measure.prose;
|
|
145
|
+
out["--container-wide"] = measure.wide;
|
|
146
|
+
}
|
|
87
147
|
if (p.accent && isColor(p.accent)) {
|
|
88
148
|
// Both names, because the ramp uses --primary for action surfaces and
|
|
89
149
|
// --accent for selection. One hue, stated once, landing on both.
|
package/dist/tokens.gen.d.ts
CHANGED
|
@@ -92,6 +92,7 @@ export declare const colors: {
|
|
|
92
92
|
/** typography tokens (from tokens/typography.css). Values are raw CSS. */
|
|
93
93
|
export declare const typography: {
|
|
94
94
|
readonly 'type-scale': "1";
|
|
95
|
+
readonly 'type-ratio': "1";
|
|
95
96
|
readonly 'text-xs': "0.6875rem";
|
|
96
97
|
readonly 'leading-xs': "1rem";
|
|
97
98
|
readonly 'text-sm': "0.8125rem";
|
|
@@ -384,6 +385,7 @@ export declare const cssVars: {
|
|
|
384
385
|
readonly '--chrome-dot-yellow': "rgb(234 179 8 / .6)";
|
|
385
386
|
readonly '--chrome-dot-green': "rgb(34 197 94 / .6)";
|
|
386
387
|
readonly '--type-scale': "1";
|
|
388
|
+
readonly '--type-ratio': "1";
|
|
387
389
|
readonly '--text-xs': "0.6875rem";
|
|
388
390
|
readonly '--leading-xs': "1rem";
|
|
389
391
|
readonly '--text-sm': "0.8125rem";
|
package/dist/tokens.gen.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokens.gen.d.ts","sourceRoot":"","sources":["../src/tokens.gen.ts"],"names":[],"mappings":"AAIA,kEAAkE;AAClE,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyFT,CAAA;AAEV,0EAA0E;AAC1E,eAAO,MAAM,UAAU
|
|
1
|
+
{"version":3,"file":"tokens.gen.d.ts","sourceRoot":"","sources":["../src/tokens.gen.ts"],"names":[],"mappings":"AAIA,kEAAkE;AAClE,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyFT,CAAA;AAEV,0EAA0E;AAC1E,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiEb,CAAA;AAEV,oEAAoE;AACpE,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2CV,CAAA;AAEV,8DAA8D;AAC9D,eAAO,MAAM,IAAI;;;;;;;;;;;;CAYP,CAAA;AAEV,kEAAkE;AAClE,eAAO,MAAM,MAAM;;;;;;;;;CAST,CAAA;AAEV,wEAAwE;AACxE,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BZ,CAAA;AAEV,kEAAkE;AAClE,eAAO,MAAM,MAAM;;;;;;;;;;;;;CAaT,CAAA;AAEV,6DAA6D;AAC7D,eAAO,MAAM,MAAM;;;;;;;;;;;;CAYT,CAAA;AAEV,gEAAgE;AAChE,eAAO,MAAM,KAAK;;;;;;CAMR,CAAA;AAEV,8DAA8D;AAC9D,eAAO,MAAM,IAAI,IAEP,CAAA;AAEV,sFAAsF;AACtF,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4QV,CAAA;AAEV,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,OAAO,CAAA"}
|
package/dist/tokens.gen.js
CHANGED
|
@@ -95,6 +95,7 @@ export const colors = {
|
|
|
95
95
|
/** typography tokens (from tokens/typography.css). Values are raw CSS. */
|
|
96
96
|
export const typography = {
|
|
97
97
|
'type-scale': '1',
|
|
98
|
+
'type-ratio': '1',
|
|
98
99
|
'text-xs': '0.6875rem',
|
|
99
100
|
'leading-xs': '1rem',
|
|
100
101
|
'text-sm': '0.8125rem',
|
|
@@ -387,6 +388,7 @@ export const cssVars = {
|
|
|
387
388
|
'--chrome-dot-yellow': 'rgb(234 179 8 / .6)',
|
|
388
389
|
'--chrome-dot-green': 'rgb(34 197 94 / .6)',
|
|
389
390
|
'--type-scale': '1',
|
|
391
|
+
'--type-ratio': '1',
|
|
390
392
|
'--text-xs': '0.6875rem',
|
|
391
393
|
'--leading-xs': '1rem',
|
|
392
394
|
'--text-sm': '0.8125rem',
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/design",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.10",
|
|
4
4
|
"packageManager": "pnpm@11.17.0",
|
|
5
|
-
"description": "Hanzo Design System
|
|
5
|
+
"description": "Hanzo Design System — monochrome, dark-default tokens + components + brand assets, the single source of truth for every Hanzo surface. CSS + typed programmatic tokens.",
|
|
6
6
|
"license": "MIT OR Apache-2.0",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -24,7 +24,13 @@ const js = src
|
|
|
24
24
|
.replace(/^import[^\n]*\n/gm, "")
|
|
25
25
|
.replace(/export (type|interface) [\s\S]*?\n}\n/g, "")
|
|
26
26
|
.replace(/export type [^\n]*\n/g, "")
|
|
27
|
-
|
|
27
|
+
// A generic annotation may nest (`Partial<Record<Face, string>>`), and
|
|
28
|
+
// stopping at the first `>` leaves `, string>` behind, so Function() fails to
|
|
29
|
+
// parse for a reason that has nothing to do with the preference. One level of
|
|
30
|
+
// nesting is matched explicitly — balanced, and it cannot run past the
|
|
31
|
+
// annotation the way a lookahead to the initializer can (that ate a function
|
|
32
|
+
// body, because a return type has no initializer to stop at).
|
|
33
|
+
.replace(/:\s*\w+<(?:[^<>]|<[^<>]*>)*>/g, "")
|
|
28
34
|
.replace(/: Preference/g, "")
|
|
29
35
|
.replace(/: Density/g, "")
|
|
30
36
|
.replace(/: string/g, "")
|
|
@@ -114,6 +120,30 @@ check("a unitless leading stays a RATIO — scaling it would double-apply", () =
|
|
|
114
120
|
eq(bad, [], "unitless leadings must not carry a multiplier:");
|
|
115
121
|
});
|
|
116
122
|
|
|
123
|
+
check("a face is set by REFERENCE, so a brand's own face still wins", () => {
|
|
124
|
+
// Restating "Georgia, serif" here would freeze this file's idea of serif into
|
|
125
|
+
// every document that ever stored the preference.
|
|
126
|
+
eq(vars({ font: "serif" })["--font-sans"], "var(--font-serif)");
|
|
127
|
+
eq(vars({ font: "mono" })["--font-sans"], "var(--font-mono)");
|
|
128
|
+
ok(!("--font-mono" in vars({ font: "serif" })), "code must stay monospaced");
|
|
129
|
+
eq(vars({ font: "default" }), {}, "default is no opinion, not a self-reference:");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
check("the measure moves containers, never the column count", () => {
|
|
133
|
+
const v = vars({ width: "wide" });
|
|
134
|
+
ok(!("--grid-columns" in v), "changing the columns is a different page, not a wider one");
|
|
135
|
+
ok("--container-max" in v && "--container-prose" in v, Object.keys(v).join(","));
|
|
136
|
+
ok(parseFloat(v["--container-prose"]) > parseFloat(vars({ width: "narrow" })["--container-prose"]), "wide must exceed narrow");
|
|
137
|
+
eq(vars({ width: "default" }), {}, "default is no opinion:");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
check("an unknown stored value is refused, not passed through", () => {
|
|
141
|
+
// A preference is user input; these axes are string unions, so a stored value
|
|
142
|
+
// from a future version or a hand-edited store must not reach a stylesheet.
|
|
143
|
+
eq(vars({ font: "comic" }), {});
|
|
144
|
+
eq(vars({ width: "enormous" }), {});
|
|
145
|
+
});
|
|
146
|
+
|
|
117
147
|
check("a colour lands on both --primary and --accent", () => {
|
|
118
148
|
const v = vars({ accent: "#808000" });
|
|
119
149
|
eq(v["--primary"], "#808000");
|
|
@@ -156,7 +186,11 @@ check("every emitted name is one the token files actually read", () => {
|
|
|
156
186
|
const declared = new Set([...text.matchAll(/^\s*(--[a-z0-9-]+)\s*:/gm)].map((m) => m[1]));
|
|
157
187
|
const read = new Set([...text.matchAll(/var\((--[a-z0-9-]+)/g)].map((m) => m[1]));
|
|
158
188
|
|
|
159
|
-
|
|
189
|
+
// EVERY axis, not a sample — an axis missing here is an axis whose names
|
|
190
|
+
// nothing gates, which is how a knob starts writing into another document.
|
|
191
|
+
const emitted = Object.keys(
|
|
192
|
+
vars({ type: 1.1, ratio: 1.2, density: "compact", font: "serif", width: "wide", accent: "#fff" })
|
|
193
|
+
);
|
|
160
194
|
const orphans = emitted.filter((k) => !declared.has(k) && !read.has(k));
|
|
161
195
|
eq(orphans, [], "emitted names no token file declares or reads:");
|
|
162
196
|
});
|
package/scripts/gen-tokens.mjs
CHANGED
|
@@ -52,9 +52,33 @@ function parse(css) {
|
|
|
52
52
|
* The shape is exact (`calc(<value> * var(--<knob>, 1))`) and only that shape is
|
|
53
53
|
* unwrapped; a genuinely composite calc (`calc(var(--fold) - 1px)`) is untouched.
|
|
54
54
|
* check-tokens.mjs holds it: no emitted value may name a knob.
|
|
55
|
+
*
|
|
56
|
+
* The type ramp adds a second knob INSIDE the first — a rung is
|
|
57
|
+
* `calc((<anchor> ± <delta> * var(--type-ratio, 1)) * var(--type-scale, 1))`, and
|
|
58
|
+
* the small rungs wrap that in a `max()` floor. Both are peeled here, in the
|
|
59
|
+
* order they nest, and the arithmetic is then done at the published defaults
|
|
60
|
+
* (both knobs 1) so the table carries exactly the rung the sheet renders — which
|
|
61
|
+
* is the same number it carried before the ratio existed. The floor is DROPPED
|
|
62
|
+
* rather than evaluated: it exists to catch a person's settings compounding, and
|
|
63
|
+
* at the defaults it never binds, so keeping it would put a guard in the table
|
|
64
|
+
* against a knob the table does not have.
|
|
55
65
|
*/
|
|
56
66
|
const KNOB = /^calc\(\s*([^*]+?)\s*\*\s*var\(--(?:type-scale|density),\s*1\)\s*\)$/
|
|
57
|
-
const
|
|
67
|
+
const FLOOR = /^max\(\s*[\d.]+[a-z%]*\s*,\s*(calc\(.+\))\s*\)$/
|
|
68
|
+
const RUNG =
|
|
69
|
+
/^calc\(\(\s*([\d.]+)(r?em|px)\s*([+-])\s*([\d.]+)(?:r?em|px)\s*\*\s*var\(--type-ratio,\s*1\)\s*\)\s*\*\s*var\(--type-scale,\s*1\)\s*\)$/
|
|
70
|
+
|
|
71
|
+
const scale = (v) => {
|
|
72
|
+
const inner = v.match(FLOOR)?.[1] ?? v
|
|
73
|
+
const r = inner.match(RUNG)
|
|
74
|
+
if (r) {
|
|
75
|
+
const [, anchor, unit, sign, delta] = r
|
|
76
|
+
const n = sign === '-' ? Number(anchor) - Number(delta) : Number(anchor) + Number(delta)
|
|
77
|
+
// 4dp, then trim — 0.875 + 0.75 must read `1.625rem`, not `1.6250000000000002rem`.
|
|
78
|
+
return `${Number(n.toFixed(4))}${unit}`
|
|
79
|
+
}
|
|
80
|
+
return inner.match(KNOB)?.[1] ?? inner
|
|
81
|
+
}
|
|
58
82
|
|
|
59
83
|
const groupMaps = {} // groupName -> Map(name->value), first-wins
|
|
60
84
|
const flatMap = new Map() // '--name' -> value, first-wins
|
package/src/preference.ts
CHANGED
|
@@ -28,10 +28,29 @@
|
|
|
28
28
|
|
|
29
29
|
export type Density = "compact" | "default" | "comfortable";
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* The face, named by what it IS rather than by a family — the families are the
|
|
33
|
+
* token file's to choose, and it already names four.
|
|
34
|
+
*/
|
|
35
|
+
export type Face = "default" | "system" | "serif" | "mono";
|
|
36
|
+
|
|
37
|
+
/** How wide the page runs before it stops. */
|
|
38
|
+
export type Measure = "narrow" | "default" | "wide";
|
|
39
|
+
|
|
31
40
|
export interface Preference {
|
|
32
41
|
/** Multiplier on the type ramp. 1 is the published scale. */
|
|
33
42
|
type?: number;
|
|
43
|
+
/**
|
|
44
|
+
* How far APART the rungs sit — the ramp's contrast, where `type` is its size.
|
|
45
|
+
* 1 is the published scale; below 1 flattens it toward a uniform register,
|
|
46
|
+
* above 1 opens the display end and tightens the small one.
|
|
47
|
+
*/
|
|
48
|
+
ratio?: number;
|
|
34
49
|
density?: Density;
|
|
50
|
+
/** Which face the page is set in. */
|
|
51
|
+
font?: Face;
|
|
52
|
+
/** How wide the page runs — the measure, not the window. */
|
|
53
|
+
width?: Measure;
|
|
35
54
|
/** A CSS colour for --primary / --accent. Rejected unless it is one. */
|
|
36
55
|
accent?: string;
|
|
37
56
|
}
|
|
@@ -48,6 +67,20 @@ export interface Preference {
|
|
|
48
67
|
export const TYPE_MIN = 0.85;
|
|
49
68
|
export const TYPE_MAX = 1.4;
|
|
50
69
|
|
|
70
|
+
/**
|
|
71
|
+
* The ratio's bounds are looser than type's, because the ramp defends its own
|
|
72
|
+
* floor.
|
|
73
|
+
*
|
|
74
|
+
* `tokens/typography.css` floors --text-xs/sm with `max()`, so the failure mode
|
|
75
|
+
* that forces type's tight clamp — a knob quietly rendering 9px labels — cannot
|
|
76
|
+
* happen on this axis however it combines with type. What is left to bound is
|
|
77
|
+
* only whether the ramp still READS as a ramp: at 0.75 the app register is
|
|
78
|
+
* within a hair of uniform, and past 1.5 a page's h2 has left its own body text
|
|
79
|
+
* behind entirely.
|
|
80
|
+
*/
|
|
81
|
+
export const RATIO_MIN = 0.75;
|
|
82
|
+
export const RATIO_MAX = 1.5;
|
|
83
|
+
|
|
51
84
|
/**
|
|
52
85
|
* Density moves SPACING only, and its range is much tighter than type's.
|
|
53
86
|
*
|
|
@@ -62,6 +95,39 @@ const DENSITY: Record<Density, number> = {
|
|
|
62
95
|
comfortable: 1.15,
|
|
63
96
|
};
|
|
64
97
|
|
|
98
|
+
/**
|
|
99
|
+
* A face is chosen from the ones `tokens/fonts.css` already declares, and it is
|
|
100
|
+
* set by REFERENCE — `var(--font-serif)`, never a family list restated here.
|
|
101
|
+
*
|
|
102
|
+
* That is what keeps a preference from pinning a face: when the token file
|
|
103
|
+
* changes what "mono" means, or a brand overrides it, a person who chose mono
|
|
104
|
+
* follows. Writing `Georgia, serif` here would freeze this file's idea of serif
|
|
105
|
+
* into every document that ever stored the preference.
|
|
106
|
+
*
|
|
107
|
+
* `default` is deliberately absent from the map rather than mapped to
|
|
108
|
+
* `var(--font-sans)`: the axis writes `--font-sans`, so resolving it to itself
|
|
109
|
+
* is a cycle, and "no opinion" is already how every other axis says default.
|
|
110
|
+
*/
|
|
111
|
+
const FACE: Partial<Record<Face, string>> = {
|
|
112
|
+
system: "ui-sans-serif, system-ui, -apple-system, sans-serif",
|
|
113
|
+
serif: "var(--font-serif)",
|
|
114
|
+
mono: "var(--font-mono)",
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The measure moves the CONTAINERS, not the columns.
|
|
119
|
+
*
|
|
120
|
+
* `--grid-columns` is 12 because layouts are authored against 12; changing it
|
|
121
|
+
* re-flows every span a page declares and is a different page, not a wider one.
|
|
122
|
+
* What a reader actually means by "wider" is how far the text runs before it
|
|
123
|
+
* wraps, which is `--container-*` — so that is the knob, and the grid inside it
|
|
124
|
+
* is untouched.
|
|
125
|
+
*/
|
|
126
|
+
const MEASURE: Partial<Record<Measure, { max: string; prose: string; wide: string }>> = {
|
|
127
|
+
narrow: { max: "64rem", prose: "40rem", wide: "58rem" },
|
|
128
|
+
wide: { max: "96rem", prose: "56rem", wide: "86rem" },
|
|
129
|
+
};
|
|
130
|
+
|
|
65
131
|
const clamp = (n: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, n));
|
|
66
132
|
|
|
67
133
|
/** Trim to 4dp so a multiplier cannot emit a 17-digit float into a stylesheet. */
|
|
@@ -99,10 +165,28 @@ export function vars(p: Preference): Record<string, string> {
|
|
|
99
165
|
out["--type-scale"] = round(clamp(p.type, TYPE_MIN, TYPE_MAX));
|
|
100
166
|
}
|
|
101
167
|
|
|
168
|
+
if (typeof p.ratio === "number" && Number.isFinite(p.ratio)) {
|
|
169
|
+
out["--type-ratio"] = round(clamp(p.ratio, RATIO_MIN, RATIO_MAX));
|
|
170
|
+
}
|
|
171
|
+
|
|
102
172
|
if (p.density && p.density in DENSITY) {
|
|
103
173
|
out["--density"] = round(DENSITY[p.density]);
|
|
104
174
|
}
|
|
105
175
|
|
|
176
|
+
// Indexed, not `in`-checked: the maps omit `default`, so a lookup answers
|
|
177
|
+
// undefined for it and for anything stored that is not a face at all. One
|
|
178
|
+
// read, no cast, and an unknown value is refused by the same line that
|
|
179
|
+
// resolves a known one.
|
|
180
|
+
const face = p.font ? FACE[p.font] : undefined;
|
|
181
|
+
if (face) out["--font-sans"] = face;
|
|
182
|
+
|
|
183
|
+
const measure = p.width ? MEASURE[p.width] : undefined;
|
|
184
|
+
if (measure) {
|
|
185
|
+
out["--container-max"] = measure.max;
|
|
186
|
+
out["--container-prose"] = measure.prose;
|
|
187
|
+
out["--container-wide"] = measure.wide;
|
|
188
|
+
}
|
|
189
|
+
|
|
106
190
|
if (p.accent && isColor(p.accent)) {
|
|
107
191
|
// Both names, because the ramp uses --primary for action surfaces and
|
|
108
192
|
// --accent for selection. One hue, stated once, landing on both.
|
package/src/tokens.gen.ts
CHANGED
|
@@ -97,6 +97,7 @@ export const colors = {
|
|
|
97
97
|
/** typography tokens (from tokens/typography.css). Values are raw CSS. */
|
|
98
98
|
export const typography = {
|
|
99
99
|
'type-scale': '1',
|
|
100
|
+
'type-ratio': '1',
|
|
100
101
|
'text-xs': '0.6875rem',
|
|
101
102
|
'leading-xs': '1rem',
|
|
102
103
|
'text-sm': '0.8125rem',
|
|
@@ -400,6 +401,7 @@ export const cssVars = {
|
|
|
400
401
|
'--chrome-dot-yellow': 'rgb(234 179 8 / .6)',
|
|
401
402
|
'--chrome-dot-green': 'rgb(34 197 94 / .6)',
|
|
402
403
|
'--type-scale': '1',
|
|
404
|
+
'--type-ratio': '1',
|
|
403
405
|
'--text-xs': '0.6875rem',
|
|
404
406
|
'--leading-xs': '1rem',
|
|
405
407
|
'--text-sm': '0.8125rem',
|
package/styles.css
CHANGED
|
@@ -296,19 +296,44 @@
|
|
|
296
296
|
@hanzo/design's `vars({type})` emits exactly this name. */
|
|
297
297
|
--type-scale:1;
|
|
298
298
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
299
|
+
/* The CONTRAST knob — how far apart the rungs sit, where --type-scale is how
|
|
300
|
+
big they all are. Two different questions, and only the first one was
|
|
301
|
+
askable: a surface could take the whole ramp up 15% but could not flatten a
|
|
302
|
+
dense table's steps or open up a marketing page's, so both settled for the
|
|
303
|
+
app register's tight default.
|
|
304
|
+
|
|
305
|
+
It is authored as a TRANSFORM and not as a generated ramp, which is the
|
|
306
|
+
whole point. `base + (authored - base) * ratio` keeps each rung's authored
|
|
307
|
+
value as the INPUT, so ratio 1 reduces to exactly the published number by
|
|
308
|
+
algebra rather than by care — the deltas below are just `authored - 0.875`.
|
|
309
|
+
The alternative, deriving each rung as base * ratio^n, would restate the
|
|
310
|
+
ramp in a second place, and this scale is not geometric anyway: the app
|
|
311
|
+
register (11/13/14/15/17) is near-linear on purpose and a ratio applied to
|
|
312
|
+
it would erase the 1px steps the compact register is FOR. That drift is not
|
|
313
|
+
hypothetical here — see src/preference.ts, where a recomputed copy of this
|
|
314
|
+
ramp shipped `lg: 16px` against this file's 15px.
|
|
315
|
+
|
|
316
|
+
A rung is one place, so a rung added later needs a delta and nothing else. */
|
|
317
|
+
--type-ratio:1;
|
|
318
|
+
|
|
319
|
+
/* --text-xs/sm carry a floor because the knobs COMPOUND: a flat ratio shrinks
|
|
320
|
+
the small end while a small scale shrinks everything, and the two multiply.
|
|
321
|
+
Clamping each knob's range separately cannot fix that (0.85 x 0.9 is inside
|
|
322
|
+
both bounds and lands at 8.5px); flooring the RESULT can, so no combination
|
|
323
|
+
of a person's settings and an org's renders its own labels illegible. */
|
|
324
|
+
--text-xs:max(0.625rem, calc((0.875rem - 0.1875rem * var(--type-ratio, 1)) * var(--type-scale, 1))); --leading-xs:calc((1.35rem - 0.35rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 11px — eyebrows / section labels */
|
|
325
|
+
--text-sm:max(0.6875rem, calc((0.875rem - 0.0625rem * var(--type-ratio, 1)) * var(--type-scale, 1))); --leading-sm:calc((1.35rem - 0.2rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 13px — nav labels, dense body */
|
|
326
|
+
--text-base:calc(0.875rem * var(--type-scale, 1)); --leading-base:calc(1.35rem * var(--type-scale, 1)); /* 14px — the ANCHOR: the rung the ratio pivots on, so it carries no ratio term */
|
|
327
|
+
--text-lg:calc((0.875rem + 0.0625rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-lg:calc((1.35rem + 0.05rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 15px */
|
|
328
|
+
--text-xl:calc((0.875rem + 0.1875rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-xl:calc((1.35rem + 0.2rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 17px */
|
|
329
|
+
--text-2xl:calc((0.875rem + 0.4375rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-2xl:calc((1.35rem + 0.35rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 21px */
|
|
330
|
+
--text-3xl:calc((0.875rem + 0.75rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-3xl:calc((1.35rem + 0.6rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 26px */
|
|
331
|
+
--text-4xl:calc((0.875rem + 1.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-4xl:calc((1.35rem + 0.9rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 32px */
|
|
332
|
+
--text-5xl:calc((0.875rem + 1.625rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-5xl:1.05; /* 40px — display leading is unitless, so it tracks its own size */
|
|
333
|
+
--text-6xl:calc((0.875rem + 2.375rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-6xl:1; /* 52px */
|
|
334
|
+
--text-7xl:calc((0.875rem + 3.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-7xl:1; /* 64px */
|
|
335
|
+
--text-8xl:calc((0.875rem + 4.375rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-8xl:1; /* 84px */
|
|
336
|
+
--text-9xl:calc((0.875rem + 6.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-9xl:1; /* 112px */
|
|
312
337
|
|
|
313
338
|
/* The SAME scale under @hanzo/brand's spelling. @hanzo/gui's shell theme and
|
|
314
339
|
Hanzo Studio address the ramp as --font-size-*; both names are one value, so
|
package/tailwind.css
CHANGED
|
@@ -319,19 +319,44 @@
|
|
|
319
319
|
@hanzo/design's `vars({type})` emits exactly this name. */
|
|
320
320
|
--type-scale:1;
|
|
321
321
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
322
|
+
/* The CONTRAST knob — how far apart the rungs sit, where --type-scale is how
|
|
323
|
+
big they all are. Two different questions, and only the first one was
|
|
324
|
+
askable: a surface could take the whole ramp up 15% but could not flatten a
|
|
325
|
+
dense table's steps or open up a marketing page's, so both settled for the
|
|
326
|
+
app register's tight default.
|
|
327
|
+
|
|
328
|
+
It is authored as a TRANSFORM and not as a generated ramp, which is the
|
|
329
|
+
whole point. `base + (authored - base) * ratio` keeps each rung's authored
|
|
330
|
+
value as the INPUT, so ratio 1 reduces to exactly the published number by
|
|
331
|
+
algebra rather than by care — the deltas below are just `authored - 0.875`.
|
|
332
|
+
The alternative, deriving each rung as base * ratio^n, would restate the
|
|
333
|
+
ramp in a second place, and this scale is not geometric anyway: the app
|
|
334
|
+
register (11/13/14/15/17) is near-linear on purpose and a ratio applied to
|
|
335
|
+
it would erase the 1px steps the compact register is FOR. That drift is not
|
|
336
|
+
hypothetical here — see src/preference.ts, where a recomputed copy of this
|
|
337
|
+
ramp shipped `lg: 16px` against this file's 15px.
|
|
338
|
+
|
|
339
|
+
A rung is one place, so a rung added later needs a delta and nothing else. */
|
|
340
|
+
--type-ratio:1;
|
|
341
|
+
|
|
342
|
+
/* --text-xs/sm carry a floor because the knobs COMPOUND: a flat ratio shrinks
|
|
343
|
+
the small end while a small scale shrinks everything, and the two multiply.
|
|
344
|
+
Clamping each knob's range separately cannot fix that (0.85 x 0.9 is inside
|
|
345
|
+
both bounds and lands at 8.5px); flooring the RESULT can, so no combination
|
|
346
|
+
of a person's settings and an org's renders its own labels illegible. */
|
|
347
|
+
--text-xs:max(0.625rem, calc((0.875rem - 0.1875rem * var(--type-ratio, 1)) * var(--type-scale, 1))); --leading-xs:calc((1.35rem - 0.35rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 11px — eyebrows / section labels */
|
|
348
|
+
--text-sm:max(0.6875rem, calc((0.875rem - 0.0625rem * var(--type-ratio, 1)) * var(--type-scale, 1))); --leading-sm:calc((1.35rem - 0.2rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 13px — nav labels, dense body */
|
|
349
|
+
--text-base:calc(0.875rem * var(--type-scale, 1)); --leading-base:calc(1.35rem * var(--type-scale, 1)); /* 14px — the ANCHOR: the rung the ratio pivots on, so it carries no ratio term */
|
|
350
|
+
--text-lg:calc((0.875rem + 0.0625rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-lg:calc((1.35rem + 0.05rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 15px */
|
|
351
|
+
--text-xl:calc((0.875rem + 0.1875rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-xl:calc((1.35rem + 0.2rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 17px */
|
|
352
|
+
--text-2xl:calc((0.875rem + 0.4375rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-2xl:calc((1.35rem + 0.35rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 21px */
|
|
353
|
+
--text-3xl:calc((0.875rem + 0.75rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-3xl:calc((1.35rem + 0.6rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 26px */
|
|
354
|
+
--text-4xl:calc((0.875rem + 1.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-4xl:calc((1.35rem + 0.9rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 32px */
|
|
355
|
+
--text-5xl:calc((0.875rem + 1.625rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-5xl:1.05; /* 40px — display leading is unitless, so it tracks its own size */
|
|
356
|
+
--text-6xl:calc((0.875rem + 2.375rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-6xl:1; /* 52px */
|
|
357
|
+
--text-7xl:calc((0.875rem + 3.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-7xl:1; /* 64px */
|
|
358
|
+
--text-8xl:calc((0.875rem + 4.375rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-8xl:1; /* 84px */
|
|
359
|
+
--text-9xl:calc((0.875rem + 6.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-9xl:1; /* 112px */
|
|
335
360
|
|
|
336
361
|
/* The SAME scale under @hanzo/brand's spelling. @hanzo/gui's shell theme and
|
|
337
362
|
Hanzo Studio address the ramp as --font-size-*; both names are one value, so
|
package/tokens/typography.css
CHANGED
|
@@ -11,19 +11,44 @@
|
|
|
11
11
|
@hanzo/design's `vars({type})` emits exactly this name. */
|
|
12
12
|
--type-scale:1;
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
14
|
+
/* The CONTRAST knob — how far apart the rungs sit, where --type-scale is how
|
|
15
|
+
big they all are. Two different questions, and only the first one was
|
|
16
|
+
askable: a surface could take the whole ramp up 15% but could not flatten a
|
|
17
|
+
dense table's steps or open up a marketing page's, so both settled for the
|
|
18
|
+
app register's tight default.
|
|
19
|
+
|
|
20
|
+
It is authored as a TRANSFORM and not as a generated ramp, which is the
|
|
21
|
+
whole point. `base + (authored - base) * ratio` keeps each rung's authored
|
|
22
|
+
value as the INPUT, so ratio 1 reduces to exactly the published number by
|
|
23
|
+
algebra rather than by care — the deltas below are just `authored - 0.875`.
|
|
24
|
+
The alternative, deriving each rung as base * ratio^n, would restate the
|
|
25
|
+
ramp in a second place, and this scale is not geometric anyway: the app
|
|
26
|
+
register (11/13/14/15/17) is near-linear on purpose and a ratio applied to
|
|
27
|
+
it would erase the 1px steps the compact register is FOR. That drift is not
|
|
28
|
+
hypothetical here — see src/preference.ts, where a recomputed copy of this
|
|
29
|
+
ramp shipped `lg: 16px` against this file's 15px.
|
|
30
|
+
|
|
31
|
+
A rung is one place, so a rung added later needs a delta and nothing else. */
|
|
32
|
+
--type-ratio:1;
|
|
33
|
+
|
|
34
|
+
/* --text-xs/sm carry a floor because the knobs COMPOUND: a flat ratio shrinks
|
|
35
|
+
the small end while a small scale shrinks everything, and the two multiply.
|
|
36
|
+
Clamping each knob's range separately cannot fix that (0.85 x 0.9 is inside
|
|
37
|
+
both bounds and lands at 8.5px); flooring the RESULT can, so no combination
|
|
38
|
+
of a person's settings and an org's renders its own labels illegible. */
|
|
39
|
+
--text-xs:max(0.625rem, calc((0.875rem - 0.1875rem * var(--type-ratio, 1)) * var(--type-scale, 1))); --leading-xs:calc((1.35rem - 0.35rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 11px — eyebrows / section labels */
|
|
40
|
+
--text-sm:max(0.6875rem, calc((0.875rem - 0.0625rem * var(--type-ratio, 1)) * var(--type-scale, 1))); --leading-sm:calc((1.35rem - 0.2rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 13px — nav labels, dense body */
|
|
41
|
+
--text-base:calc(0.875rem * var(--type-scale, 1)); --leading-base:calc(1.35rem * var(--type-scale, 1)); /* 14px — the ANCHOR: the rung the ratio pivots on, so it carries no ratio term */
|
|
42
|
+
--text-lg:calc((0.875rem + 0.0625rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-lg:calc((1.35rem + 0.05rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 15px */
|
|
43
|
+
--text-xl:calc((0.875rem + 0.1875rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-xl:calc((1.35rem + 0.2rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 17px */
|
|
44
|
+
--text-2xl:calc((0.875rem + 0.4375rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-2xl:calc((1.35rem + 0.35rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 21px */
|
|
45
|
+
--text-3xl:calc((0.875rem + 0.75rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-3xl:calc((1.35rem + 0.6rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 26px */
|
|
46
|
+
--text-4xl:calc((0.875rem + 1.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-4xl:calc((1.35rem + 0.9rem * var(--type-ratio, 1)) * var(--type-scale, 1)); /* 32px */
|
|
47
|
+
--text-5xl:calc((0.875rem + 1.625rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-5xl:1.05; /* 40px — display leading is unitless, so it tracks its own size */
|
|
48
|
+
--text-6xl:calc((0.875rem + 2.375rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-6xl:1; /* 52px */
|
|
49
|
+
--text-7xl:calc((0.875rem + 3.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-7xl:1; /* 64px */
|
|
50
|
+
--text-8xl:calc((0.875rem + 4.375rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-8xl:1; /* 84px */
|
|
51
|
+
--text-9xl:calc((0.875rem + 6.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)); --leading-9xl:1; /* 112px */
|
|
27
52
|
|
|
28
53
|
/* The SAME scale under @hanzo/brand's spelling. @hanzo/gui's shell theme and
|
|
29
54
|
Hanzo Studio address the ramp as --font-size-*; both names are one value, so
|