@hanzo/design 0.5.9 → 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.
@@ -26,6 +26,13 @@
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;
@@ -36,6 +43,10 @@ export interface Preference {
36
43
  */
37
44
  ratio?: number;
38
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;
39
50
  /** A CSS colour for --primary / --accent. Rejected unless it is one. */
40
51
  accent?: string;
41
52
  }
@@ -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;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,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;AAE5B;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,OAAO,CAAC;AAC9B,eAAO,MAAM,SAAS,MAAM,CAAC;AAqB7B;;;;;;;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,CAuB1D;AAED,2EAA2E;AAC3E,wBAAgB,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,QAAQ,SAAc,GAAG,MAAM,CAKjE"}
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"}
@@ -62,6 +62,37 @@ const DENSITY = {
62
62
  default: 1,
63
63
  comfortable: 1.15,
64
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
+ };
65
96
  const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
66
97
  /** Trim to 4dp so a multiplier cannot emit a 17-digit float into a stylesheet. */
67
98
  const round = (n) => String(Math.round(n * 10000) / 10000);
@@ -100,6 +131,19 @@ export function vars(p) {
100
131
  if (p.density && p.density in DENSITY) {
101
132
  out["--density"] = round(DENSITY[p.density]);
102
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
+ }
103
147
  if (p.accent && isColor(p.accent)) {
104
148
  // Both names, because the ramp uses --primary for action surfaces and
105
149
  // --accent for selection. One hue, stated once, landing on both.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/design",
3
- "version": "0.5.9",
3
+ "version": "0.5.10",
4
4
  "packageManager": "pnpm@11.17.0",
5
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",
@@ -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
- .replace(/: Record<[^>]*>/g, "")
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
- const emitted = Object.keys(vars({ type: 1.1, density: "compact", accent: "#fff" }));
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/src/preference.ts CHANGED
@@ -28,6 +28,15 @@
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;
@@ -38,6 +47,10 @@ export interface Preference {
38
47
  */
39
48
  ratio?: number;
40
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;
41
54
  /** A CSS colour for --primary / --accent. Rejected unless it is one. */
42
55
  accent?: string;
43
56
  }
@@ -82,6 +95,39 @@ const DENSITY: Record<Density, number> = {
82
95
  comfortable: 1.15,
83
96
  };
84
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
+
85
131
  const clamp = (n: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, n));
86
132
 
87
133
  /** Trim to 4dp so a multiplier cannot emit a 17-digit float into a stylesheet. */
@@ -127,6 +173,20 @@ export function vars(p: Preference): Record<string, string> {
127
173
  out["--density"] = round(DENSITY[p.density]);
128
174
  }
129
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
+
130
190
  if (p.accent && isColor(p.accent)) {
131
191
  // Both names, because the ramp uses --primary for action surfaces and
132
192
  // --accent for selection. One hue, stated once, landing on both.