@hanzo/design 0.5.16 → 0.5.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/assets/fonts/{LICENSE-Geist.txt → LICENSE-Zen.txt} +1 -0
- package/assets/fonts/Zen-Variable.woff2 +0 -0
- package/assets/fonts/ZenMono-Variable.woff2 +0 -0
- package/dist/brand.cjs +62 -0
- package/dist/index.cjs +92 -0
- package/dist/preference.cjs +225 -0
- package/dist/tokens.gen.cjs +597 -0
- package/dist/tokens.gen.d.ts +4 -4
- package/dist/tokens.gen.js +4 -4
- package/guidelines/DESIGN.md +36 -26
- package/package.json +5 -5
- package/prompts/studio-designer.md +9 -2
- package/scripts/check-dist.mjs +20 -0
- package/scripts/check-tokens.mjs +6 -27
- package/scripts/cjs.mjs +43 -0
- package/scripts/gen-tokens.mjs +0 -78
- package/src/tokens.gen.ts +4 -4
- package/styles.css +28 -14
- package/tokens/fonts.css +27 -13
- package/assets/fonts/Geist-Variable.woff2 +0 -0
- package/assets/fonts/GeistMono-Variable.woff2 +0 -0
- package/tailwind.css +0 -1162
package/guidelines/DESIGN.md
CHANGED
|
@@ -15,42 +15,52 @@ and the dark-black palette.
|
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
-
## 1. Typography —
|
|
18
|
+
## 1. Typography — Zen
|
|
19
19
|
|
|
20
20
|
| Role | Family | Notes |
|
|
21
21
|
|------|--------|-------|
|
|
22
|
-
| UI / body / display / heading (`sans`) | **
|
|
23
|
-
| code / data / mono (`mono`) | **
|
|
22
|
+
| UI / body / display / heading (`sans`) | **Zen** | Variable, `wght` 100–900. One file covers every weight. |
|
|
23
|
+
| code / data / mono (`mono`) | **Zen Mono** | Variable, same axis. |
|
|
24
|
+
| display accents | **Zen Pixel** | Five cuts: Circle, Grid, Line, Square, Triangle. |
|
|
24
25
|
| Arabic / Hebrew (`--font-ar` / `--font-he`) | unchanged | i18n only — keep. |
|
|
25
26
|
|
|
26
|
-
**
|
|
27
|
+
**There is one family.** No Basel Grotesk, no Druk, no Inter, DM Sans, Figtree,
|
|
28
|
+
PT Sans or Roboto Mono — and no licensed third-party face at all, which is the
|
|
29
|
+
point: Zen is ours, so nothing here is a seat count or a domain licence.
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
`@font-face` (weights 400/500, `font-display: swap`, `font-style: normal`):
|
|
31
|
+
**An app writes nothing.** `@hanzo/design/styles.css` declares the `@font-face`
|
|
32
|
+
rules and serves the binaries out of this package, so importing the token layer
|
|
33
|
+
is the whole integration. Do NOT hand-write an `@font-face`, do NOT reach for
|
|
34
|
+
`next/font/google`, and do NOT add a per-app copy of the files.
|
|
33
35
|
|
|
34
36
|
```css
|
|
35
|
-
@
|
|
36
|
-
font-family: 'Basel';
|
|
37
|
-
font-style: normal;
|
|
38
|
-
font-weight: 400; /* Book; 500 = Medium */
|
|
39
|
-
font-display: swap;
|
|
40
|
-
src: url('.../Basel-Grotesk-Book.woff2') format('woff2'),
|
|
41
|
-
url('.../Basel-Grotesk-Book.woff') format('woff');
|
|
42
|
-
}
|
|
37
|
+
@import "@hanzo/design/styles.css"; /* faces + tokens, one line */
|
|
43
38
|
```
|
|
44
39
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
40
|
+
Then name the role, never the face: `var(--font-sans)`, `var(--font-mono)`.
|
|
41
|
+
|
|
42
|
+
### Brand presets
|
|
43
|
+
|
|
44
|
+
`@hanzo/font/presets.css` carries five settings, each a point in Zen's own
|
|
45
|
+
parameter space that a brand has settled on. Two were FITTED by pixel residual
|
|
46
|
+
against the faces they replace, so the swap holds on metrics rather than on
|
|
47
|
+
taste.
|
|
48
|
+
|
|
49
|
+
| preset | wght | scaleX | track | use |
|
|
50
|
+
|---|---|---|---|---|
|
|
51
|
+
| `.zen-air` | 220 | — | −.030em | thin display |
|
|
52
|
+
| `.zen-book` | 497 | — | — | text |
|
|
53
|
+
| `.zen-medium` | 606 | — | — | UI emphasis |
|
|
54
|
+
| `.zen-wide` | 845 | 1.56 | −.040em | monumental caps |
|
|
55
|
+
| `.zen-round` | 900 | — | −.018em | heavy and friendly |
|
|
56
|
+
|
|
57
|
+
`.zen-wide` transforms, so its LAYOUT box stays the untransformed width — give
|
|
58
|
+
it room or clip its container.
|
|
59
|
+
|
|
60
|
+
**Zen's own alternates cover the neo-grotesque register**, so a brand that wants
|
|
61
|
+
that voice changes a feature setting rather than a family: `ss01` is the
|
|
62
|
+
single-storey `a`, `ss04` the straight-leg `R`, `ss09` a slashed zero and serif
|
|
63
|
+
one. Eleven sets ship.
|
|
54
64
|
|
|
55
65
|
---
|
|
56
66
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/design",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.18",
|
|
4
4
|
"packageManager": "pnpm@11.17.0",
|
|
5
5
|
"description": "Hanzo Design System \u2014 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",
|
|
@@ -13,13 +13,14 @@
|
|
|
13
13
|
"sideEffects": [
|
|
14
14
|
"*.css"
|
|
15
15
|
],
|
|
16
|
-
"main": "./dist/index.
|
|
16
|
+
"main": "./dist/index.cjs",
|
|
17
17
|
"module": "./dist/index.js",
|
|
18
18
|
"types": "./dist/index.d.ts",
|
|
19
19
|
"exports": {
|
|
20
20
|
".": {
|
|
21
21
|
"types": "./dist/index.d.ts",
|
|
22
22
|
"import": "./dist/index.js",
|
|
23
|
+
"require": "./dist/index.cjs",
|
|
23
24
|
"default": "./dist/index.js"
|
|
24
25
|
},
|
|
25
26
|
"./package.json": "./package.json",
|
|
@@ -29,13 +30,12 @@
|
|
|
29
30
|
"./prompts/*": "./prompts/*",
|
|
30
31
|
"./content/*": "./content/*",
|
|
31
32
|
"./assets/*": "./assets/*",
|
|
32
|
-
"./skills/*": "./skills/*"
|
|
33
|
-
"./tailwind.css": "./tailwind.css"
|
|
33
|
+
"./skills/*": "./skills/*"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"gen": "node scripts/gen-tokens.mjs",
|
|
37
37
|
"test": "node scripts/check-tokens.mjs && node scripts/check-preference.mjs && node scripts/lint.mjs .",
|
|
38
|
-
"build": "pnpm gen && pnpm test && tsc -p tsconfig.json && node scripts/check-dist.mjs",
|
|
38
|
+
"build": "rm -rf dist dist-cjs && pnpm gen && pnpm test && tsc -p tsconfig.json && tsc -p tsconfig.cjs.json --noCheck && node scripts/cjs.mjs && node scripts/check-dist.mjs",
|
|
39
39
|
"prepublishOnly": "pnpm build",
|
|
40
40
|
"lint": "node scripts/lint.mjs"
|
|
41
41
|
},
|
|
@@ -11,8 +11,15 @@ You are the **Hanzo Design System designer**, an agent operating on the
|
|
|
11
11
|
system as real files, using your file tools (read, write, edit, tree).
|
|
12
12
|
|
|
13
13
|
## The canon (never contradict)
|
|
14
|
-
- **Typography:**
|
|
15
|
-
|
|
14
|
+
- **Typography:** Zen (`sans`) and Zen Mono (`mono`), both variable on `wght`
|
|
15
|
+
100–900, plus Zen Pixel for display accents. ONE family, ours, no licensed
|
|
16
|
+
third-party face and no Inter/Roboto/DM Sans. An app imports
|
|
17
|
+
`@hanzo/design/styles.css` and gets the faces with it — never hand-write an
|
|
18
|
+
`@font-face` and never reach for a font CDN. Name the role (`var(--font-sans)`),
|
|
19
|
+
never the face. Brand voice comes from a preset in `@hanzo/font/presets.css`
|
|
20
|
+
(`.zen-air` `.zen-book` `.zen-medium` `.zen-wide` `.zen-round`) or from Zen's own
|
|
21
|
+
stylistic sets (`ss01` single-storey a, `ss04` straight-leg R), never from a
|
|
22
|
+
second family.
|
|
16
23
|
- **Palette:** true-black OLED. Page `#000`, surface `#0a0a0a`, press `#050505`,
|
|
17
24
|
elevated `#171717`, border `rgba(255,255,255,.10)`, text `#ededf1`. **Monochrome —
|
|
18
25
|
no colored accents.** Semantic color only for live/error/warning.
|
package/scripts/check-dist.mjs
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* A test that reads source cannot see that. This one loads the artifact.
|
|
13
13
|
*/
|
|
14
14
|
import { readFileSync, existsSync } from 'node:fs'
|
|
15
|
+
import { createRequire } from 'node:module'
|
|
15
16
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
16
17
|
import { dirname, join } from 'node:path'
|
|
17
18
|
|
|
@@ -58,6 +59,25 @@ await check('the barrel IMPORTS — the check that 0.4.10 and 0.4.11 needed', as
|
|
|
58
59
|
if (v['--type-scale'] !== '1.2') throw new Error(`built vars() returned ${JSON.stringify(v)}`)
|
|
59
60
|
})
|
|
60
61
|
|
|
62
|
+
await check('the barrel REQUIRES, with the same names it imports', async () => {
|
|
63
|
+
// `type: module` makes every `.js` here ESM, so a CommonJS consumer cannot read
|
|
64
|
+
// the barrel at all — it parses `export` as a syntax error and reports the line
|
|
65
|
+
// in OUR file. Plain Node hides this now that `require(esm)` works, but jest
|
|
66
|
+
// still loads a dependency as CommonJS, so `require('@hanzo/design')` in a
|
|
67
|
+
// jsdom test died at `dist/index.js:12` through 0.5.16.
|
|
68
|
+
//
|
|
69
|
+
// Same names from both formats, checked against each other rather than a list:
|
|
70
|
+
// a list is a third copy that goes stale the next time an export is added.
|
|
71
|
+
const esm = await import(pathToFileURL(join(root, 'dist/index.js')).href)
|
|
72
|
+
const cjs = createRequire(import.meta.url)(join(root, 'dist/index.cjs'))
|
|
73
|
+
const names = (m) => Object.keys(m).filter((k) => k !== 'default' && k !== '__esModule').sort()
|
|
74
|
+
const missing = names(esm).filter((n) => !(n in cjs))
|
|
75
|
+
if (missing.length) throw new Error(`dist/index.cjs is missing ${missing.join(', ')}`)
|
|
76
|
+
if (typeof cjs.vars !== 'function') throw new Error('vars is not callable from the CommonJS build')
|
|
77
|
+
if (cjs.vars({ type: 1.2, density: 'compact' })['--type-scale'] !== '1.2')
|
|
78
|
+
throw new Error('the CommonJS build computes a different preference than the ESM one')
|
|
79
|
+
})
|
|
80
|
+
|
|
61
81
|
await check('no emitted module uses an extensionless relative specifier', () => {
|
|
62
82
|
// The class of bug, not just the one instance: Node ESM resolves relative
|
|
63
83
|
// specifiers literally, so a missing `.js` is always a runtime failure.
|
package/scripts/check-tokens.mjs
CHANGED
|
@@ -60,7 +60,6 @@ const pass = (msg) => console.log(` ok ${msg}`)
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// ── 1a. the bundles must PARSE ───────────────────────────────────────────
|
|
63
|
-
// 0.4.2 and 0.4.3 shipped a stray `*/` in styles.css AND tailwind.css: an
|
|
64
63
|
// edit added prose to the end of a comment that was already closed, so a
|
|
65
64
|
// paragraph of English sat in the stylesheet as raw CSS, terminated by a
|
|
66
65
|
// second `*/`. Browsers recover from that — they skip to the next thing that
|
|
@@ -78,7 +77,7 @@ const pass = (msg) => console.log(` ok ${msg}`)
|
|
|
78
77
|
// that skips comment bodies finds every stray terminator and every unclosed
|
|
79
78
|
// opener. A parser dependency would be a heavier answer to a smaller question.
|
|
80
79
|
{
|
|
81
|
-
for (const f of ['styles.css'
|
|
80
|
+
for (const f of ['styles.css']) {
|
|
82
81
|
const s = read(join(root, f))
|
|
83
82
|
let i = 0, opened = 0, strays = [], unterminated = null
|
|
84
83
|
while (i < s.length - 1) {
|
|
@@ -100,31 +99,11 @@ const pass = (msg) => console.log(` ok ${msg}`)
|
|
|
100
99
|
}
|
|
101
100
|
}
|
|
102
101
|
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
// every token without a word.
|
|
109
|
-
{
|
|
110
|
-
const tw = strip(read(join(root, 'tailwind.css')))
|
|
111
|
-
tw.includes('@import')
|
|
112
|
-
? fail('tailwind.css contains an @import — invalid after `@import "tailwindcss"`, so the tokens are dropped')
|
|
113
|
-
: pass('tailwind.css has no @import to invalidate')
|
|
114
|
-
|
|
115
|
-
// Every Tailwind colour slot an app will reach for must be mapped.
|
|
116
|
-
const slots = ['background', 'foreground', 'card', 'popover', 'primary', 'secondary',
|
|
117
|
-
'muted', 'muted-foreground', 'accent', 'destructive', 'border', 'input', 'ring']
|
|
118
|
-
const unmapped = slots.filter((s) => !tw.includes(`--color-${s}:`))
|
|
119
|
-
unmapped.length
|
|
120
|
-
? fail(`tailwind.css does not map: ${unmapped.join(', ')}`)
|
|
121
|
-
: pass(`tailwind.css maps all ${slots.length} core colour slots`)
|
|
122
|
-
|
|
123
|
-
// And it must carry the values, not merely reference them.
|
|
124
|
-
tw.includes('--border:')
|
|
125
|
-
? pass('tailwind.css carries the token values inline')
|
|
126
|
-
: fail('tailwind.css maps slots but carries no tokens — every utility resolves to nothing')
|
|
127
|
-
}
|
|
102
|
+
// 1b was the Tailwind bridge — a generated `tailwind.css` mapping every token
|
|
103
|
+
// onto a `--color-*` utility slot. Gone with the utility layer it existed to
|
|
104
|
+
// feed: this system's one substrate is @hanzo/gui, which reads the tokens
|
|
105
|
+
// directly, so a second sheet republishing them as utilities was the "one fact,
|
|
106
|
+
// two homes" shape the `second-publisher` lint rule exists to catch.
|
|
128
107
|
|
|
129
108
|
// ── 1c. element defaults must LOSE to an app's utilities ─────────────────
|
|
130
109
|
// A rule outside a cascade layer beats a rule inside one regardless of
|
package/scripts/cjs.mjs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fold the CommonJS pass into `dist` as `.cjs`.
|
|
3
|
+
*
|
|
4
|
+
* `tsc` emits one module format per outDir, so the CJS build lands in
|
|
5
|
+
* `dist-cjs` beside the ESM one. Both halves ship from `dist`, distinguished by
|
|
6
|
+
* extension — `.cjs` is CommonJS whatever the package `type` says, which is the
|
|
7
|
+
* only way an ESM-typed package can hand a `require()` consumer real CJS.
|
|
8
|
+
*
|
|
9
|
+
* Specifiers are rewritten on the way in. The sources name siblings with an
|
|
10
|
+
* explicit `./x.js`, which `tsc` copies through verbatim, so an unrewritten
|
|
11
|
+
* `dist/index.cjs` would `require('./tokens.gen.js')` — the ESM copy sitting
|
|
12
|
+
* right next to it. That either throws or loads a SECOND instance of every
|
|
13
|
+
* token, so the extension has to travel with the file.
|
|
14
|
+
*/
|
|
15
|
+
import { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
16
|
+
import { dirname, join, relative } from 'node:path'
|
|
17
|
+
import { fileURLToPath } from 'node:url'
|
|
18
|
+
|
|
19
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
20
|
+
const dist = join(root, 'dist')
|
|
21
|
+
const cjs = join(root, 'dist-cjs')
|
|
22
|
+
|
|
23
|
+
const files = (dir, out = []) => {
|
|
24
|
+
for (const name of readdirSync(dir)) {
|
|
25
|
+
const p = join(dir, name)
|
|
26
|
+
statSync(p).isDirectory() ? files(p, out) : out.push(p)
|
|
27
|
+
}
|
|
28
|
+
return out
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// `from './x.js'`, `import './x.js'`, `import('./x.js')`, `require('./x.js')`
|
|
32
|
+
const SPECIFIER = /(\bfrom\s*|\bimport\s*\(?\s*|\brequire\s*\(\s*)(['"])(\.[^'"]*)\.js\2/g
|
|
33
|
+
|
|
34
|
+
for (const f of files(cjs)) {
|
|
35
|
+
if (!f.endsWith('.js')) continue
|
|
36
|
+
const src = readFileSync(f, 'utf8')
|
|
37
|
+
const out = src.replace(SPECIFIER, (_, head, q, spec) => `${head}${q}${spec}.cjs${q}`)
|
|
38
|
+
const dest = join(dist, relative(cjs, f)).replace(/\.js$/, '.cjs')
|
|
39
|
+
mkdirSync(dirname(dest), { recursive: true })
|
|
40
|
+
writeFileSync(dest, out)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
rmSync(cjs, { recursive: true, force: true })
|
package/scripts/gen-tokens.mjs
CHANGED
|
@@ -185,81 +185,3 @@ const bundle = banner + FILES.map((f) => {
|
|
|
185
185
|
writeFileSync(join(root, 'styles.css'), bundle)
|
|
186
186
|
console.log(`gen-tokens: wrote styles.css — ${FILES.length} token groups, flattened`)
|
|
187
187
|
|
|
188
|
-
// ── tailwind.css — the ONE line a Tailwind app writes ────────────────────
|
|
189
|
-
// Every app that consumed these tokens still had to hand-write the bridge from
|
|
190
|
-
// our variable names to Tailwind's utility namespace:
|
|
191
|
-
//
|
|
192
|
-
// @theme inline { --color-background: var(--background); ...twenty more }
|
|
193
|
-
//
|
|
194
|
-
// Twenty lines of boilerplate, copied into each new app, is a drift vector with
|
|
195
|
-
// a silent failure mode: miss one and that utility resolves to nothing —
|
|
196
|
-
// `border-border` becomes currentColor and paints a white hairline on black,
|
|
197
|
-
// which reads as a design choice rather than a missing line. Tabs shipped with
|
|
198
|
-
// three of these and used forty-one.
|
|
199
|
-
//
|
|
200
|
-
// So the bridge is generated here from SLOTS, and an app writes one import
|
|
201
|
-
// after `@import "tailwindcss"`. There is nothing left to get wrong, and adding
|
|
202
|
-
// a semantic token wires it up for every app at once.
|
|
203
|
-
const SLOTS = [
|
|
204
|
-
'background', 'foreground',
|
|
205
|
-
'card', 'card-foreground',
|
|
206
|
-
'popover', 'popover-foreground',
|
|
207
|
-
'primary', 'primary-foreground',
|
|
208
|
-
'secondary', 'secondary-foreground',
|
|
209
|
-
'muted', 'muted-foreground',
|
|
210
|
-
'accent', 'accent-foreground',
|
|
211
|
-
'destructive', 'destructive-foreground',
|
|
212
|
-
'border', 'input', 'ring',
|
|
213
|
-
]
|
|
214
|
-
// State colours are their own vocabulary and keep their names, so `text-error`
|
|
215
|
-
// and `bg-online` mean what they say instead of borrowing a neutral slot.
|
|
216
|
-
const STATE = { error: 'state-error', success: 'state-success', online: 'state-online' }
|
|
217
|
-
|
|
218
|
-
const declared = new Set(flat.map(([n]) => n.replace(/^--/, '')))
|
|
219
|
-
const missing = SLOTS.filter((s) => !declared.has(s))
|
|
220
|
-
if (missing.length) {
|
|
221
|
-
console.error(`gen-tokens: tokens/colors.css declares no --${missing.join(', --')}`)
|
|
222
|
-
process.exit(1)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const tw = `/* Hanzo Design System — Tailwind v4 bridge. The ONE import a Tailwind app adds.
|
|
226
|
-
*
|
|
227
|
-
* AUTO-GENERATED by scripts/gen-tokens.mjs — DO NOT EDIT.
|
|
228
|
-
*
|
|
229
|
-
* @import "tailwindcss";
|
|
230
|
-
* @import "@hanzo/design/tailwind.css";
|
|
231
|
-
*
|
|
232
|
-
* It carries the tokens AND maps them onto Tailwind's utility namespace, so
|
|
233
|
-
* \`bg-background\`, \`text-muted-foreground\` and \`border-border\` resolve to the
|
|
234
|
-
* system's values with nothing to wire up by hand.
|
|
235
|
-
*
|
|
236
|
-
* Import it AFTER tailwindcss: @theme has to come after the framework it
|
|
237
|
-
* extends, and the element defaults are layered so your utilities still win.
|
|
238
|
-
*
|
|
239
|
-
* "inline" is deliberate — it compiles \`border-border\` to var(--border) rather
|
|
240
|
-
* than to that variable's build-time value, so switching .light at runtime moves
|
|
241
|
-
* the UI and not merely the variables.
|
|
242
|
-
*
|
|
243
|
-
* The tokens are inlined below rather than @imported for the reason styles.css
|
|
244
|
-
* is flattened: an @import must precede every other rule, so importing this
|
|
245
|
-
* after tailwindcss would invalidate it and the browser would drop the whole
|
|
246
|
-
* token layer without a word.
|
|
247
|
-
*/
|
|
248
|
-
${bundle}
|
|
249
|
-
|
|
250
|
-
@theme inline {
|
|
251
|
-
${SLOTS.map((s) => ` --color-${s}: var(--${s});`).join('\n')}
|
|
252
|
-
|
|
253
|
-
${Object.entries(STATE).map(([k, v]) => ` --color-${k}: var(--${v});`).join('\n')}
|
|
254
|
-
|
|
255
|
-
--radius-sm: var(--radius-sm);
|
|
256
|
-
--radius-md: var(--radius-md);
|
|
257
|
-
--radius-lg: var(--radius-lg);
|
|
258
|
-
--radius-xl: var(--radius-xl);
|
|
259
|
-
|
|
260
|
-
--font-sans: var(--font-sans);
|
|
261
|
-
--font-mono: var(--font-mono);
|
|
262
|
-
}
|
|
263
|
-
`
|
|
264
|
-
writeFileSync(join(root, 'tailwind.css'), tw)
|
|
265
|
-
console.log(`gen-tokens: wrote tailwind.css — ${SLOTS.length} colour slots + ${Object.keys(STATE).length} states`)
|
package/src/tokens.gen.ts
CHANGED
|
@@ -309,9 +309,9 @@ export const zIndex = {
|
|
|
309
309
|
|
|
310
310
|
/** fonts tokens (from tokens/fonts.css). Values are raw CSS. */
|
|
311
311
|
export const fonts = {
|
|
312
|
-
'font-sans': '"
|
|
312
|
+
'font-sans': '"Zen",ui-sans-serif,system-ui,sans-serif',
|
|
313
313
|
'font-display': 'var(--font-sans)',
|
|
314
|
-
'font-mono': '"
|
|
314
|
+
'font-mono': '"Zen Mono",ui-monospace,SFMono-Regular,monospace',
|
|
315
315
|
'font-serif': 'Georgia,serif',
|
|
316
316
|
'font-feature-settings': '"ss01","ss02","cv01","cv02","cv03"',
|
|
317
317
|
} as const
|
|
@@ -596,9 +596,9 @@ export const cssVars = {
|
|
|
596
596
|
'--z-toast': '800',
|
|
597
597
|
'--z-tooltip': 'var(--z-popover)',
|
|
598
598
|
'--z-notification': 'var(--z-toast)',
|
|
599
|
-
'--font-sans': '"
|
|
599
|
+
'--font-sans': '"Zen",ui-sans-serif,system-ui,sans-serif',
|
|
600
600
|
'--font-display': 'var(--font-sans)',
|
|
601
|
-
'--font-mono': '"
|
|
601
|
+
'--font-mono': '"Zen Mono",ui-monospace,SFMono-Regular,monospace',
|
|
602
602
|
'--font-serif': 'Georgia,serif',
|
|
603
603
|
'--font-feature-settings': '"ss01","ss02","cv01","cv02","cv03"',
|
|
604
604
|
'--ring-offset': '-2px',
|
package/styles.css
CHANGED
|
@@ -838,44 +838,58 @@
|
|
|
838
838
|
}
|
|
839
839
|
|
|
840
840
|
/* ── tokens/fonts.css ─────────────────────────────────────── */
|
|
841
|
-
/*
|
|
841
|
+
/* Zen — the only family on Hanzo surfaces, sans and mono.
|
|
842
842
|
SELF-HOSTED. The faces ship inside this package (assets/fonts/*.woff2, two
|
|
843
|
-
variable files, 141 KB total, SIL OFL-1.1 — see assets/fonts/LICENSE-
|
|
844
|
-
|
|
845
|
-
|
|
843
|
+
variable files, 141 KB total, SIL OFL-1.1 — see assets/fonts/LICENSE-Zen.txt).
|
|
844
|
+
|
|
845
|
+
Zen is our own family, under the SIL Open Font License. The licence file
|
|
846
|
+
beside the binaries carries every copyright the OFL requires; nothing else in
|
|
847
|
+
this repository restates them, which is the whole of what §2 asks for.
|
|
848
|
+
|
|
849
|
+
SHIPPED here rather than @import-ed from `@hanzo/font`, and that is load
|
|
850
|
+
bearing. `@hanzo/ui` composes this file into the MIDDLE of its theme.css, and
|
|
851
|
+
CSS drops an @import that does not precede every other rule — measured, in
|
|
852
|
+
Chromium, on the built sheet: zero faces loaded and not one woff2 requested.
|
|
853
|
+
An @font-face has no such ordering rule, so declaring the faces is the only
|
|
854
|
+
form that survives being flattened into a larger stylesheet. `@hanzo/font`
|
|
855
|
+
remains the one place the faces are AUTHORED; this is a copy of its output,
|
|
856
|
+
the same way the colour tokens are copied rather than fetched.
|
|
857
|
+
|
|
858
|
+
Why self-hosted rather than a third-party CDN:
|
|
846
859
|
- A sign-in page must not make a third-party request. hanzoai/id refused to
|
|
847
860
|
import this file for exactly that reason, which split the token layer: id
|
|
848
861
|
took the colours and not the typeface. Self-hosting removes the reason, so
|
|
849
862
|
every surface can import styles.css unchanged.
|
|
850
|
-
-
|
|
851
|
-
|
|
863
|
+
- It was a render-blocking request to a host we do not control, on the
|
|
864
|
+
critical path of every surface, and it broke offline/air-gapped dev.
|
|
852
865
|
- One variable file per family replaces nine static weights, and it is fewer
|
|
853
|
-
bytes than the CSS-then-woff2 round trip
|
|
866
|
+
bytes than the CSS-then-woff2 round trip.
|
|
854
867
|
|
|
855
868
|
The url()s are relative to THIS file, so they resolve wherever the package is
|
|
856
869
|
mounted — node_modules, a CDN, a copied dist — with no configuration. */
|
|
857
870
|
|
|
858
871
|
@font-face{
|
|
859
|
-
font-family:"
|
|
860
|
-
src:url("./assets/fonts/
|
|
872
|
+
font-family:"Zen";
|
|
873
|
+
src:url("./assets/fonts/Zen-Variable.woff2") format("woff2");
|
|
861
874
|
font-weight:100 900;
|
|
862
875
|
font-style:normal;
|
|
863
876
|
font-display:swap;
|
|
864
877
|
}
|
|
865
878
|
@font-face{
|
|
866
|
-
font-family:"
|
|
867
|
-
src:url("./assets/fonts/
|
|
879
|
+
font-family:"Zen Mono";
|
|
880
|
+
src:url("./assets/fonts/ZenMono-Variable.woff2") format("woff2");
|
|
868
881
|
font-weight:100 900;
|
|
869
882
|
font-style:normal;
|
|
870
883
|
font-display:swap;
|
|
871
884
|
}
|
|
872
885
|
|
|
873
886
|
:root{
|
|
874
|
-
--font-sans:"
|
|
887
|
+
--font-sans:"Zen",ui-sans-serif,system-ui,sans-serif;
|
|
875
888
|
--font-display:var(--font-sans);
|
|
876
|
-
--font-mono:"
|
|
889
|
+
--font-mono:"Zen Mono",ui-monospace,SFMono-Regular,monospace;
|
|
877
890
|
--font-serif:Georgia,serif;
|
|
878
|
-
/* hanzo.ai sets these OpenType features on <body>.
|
|
891
|
+
/* hanzo.ai sets these OpenType features on <body>. ss01 is Zen's
|
|
892
|
+
single-storey a — the neo-grotesque letterform. */
|
|
879
893
|
--font-feature-settings:"ss01","ss02","cv01","cv02","cv03";
|
|
880
894
|
}
|
|
881
895
|
|
package/tokens/fonts.css
CHANGED
|
@@ -1,40 +1,54 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/* Zen — the only family on Hanzo surfaces, sans and mono.
|
|
2
2
|
SELF-HOSTED. The faces ship inside this package (assets/fonts/*.woff2, two
|
|
3
|
-
variable files, 141 KB total, SIL OFL-1.1 — see assets/fonts/LICENSE-
|
|
3
|
+
variable files, 141 KB total, SIL OFL-1.1 — see assets/fonts/LICENSE-Zen.txt).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Zen is our own family, under the SIL Open Font License. The licence file
|
|
6
|
+
beside the binaries carries every copyright the OFL requires; nothing else in
|
|
7
|
+
this repository restates them, which is the whole of what §2 asks for.
|
|
8
|
+
|
|
9
|
+
SHIPPED here rather than @import-ed from `@hanzo/font`, and that is load
|
|
10
|
+
bearing. `@hanzo/ui` composes this file into the MIDDLE of its theme.css, and
|
|
11
|
+
CSS drops an @import that does not precede every other rule — measured, in
|
|
12
|
+
Chromium, on the built sheet: zero faces loaded and not one woff2 requested.
|
|
13
|
+
An @font-face has no such ordering rule, so declaring the faces is the only
|
|
14
|
+
form that survives being flattened into a larger stylesheet. `@hanzo/font`
|
|
15
|
+
remains the one place the faces are AUTHORED; this is a copy of its output,
|
|
16
|
+
the same way the colour tokens are copied rather than fetched.
|
|
17
|
+
|
|
18
|
+
Why self-hosted rather than a third-party CDN:
|
|
6
19
|
- A sign-in page must not make a third-party request. hanzoai/id refused to
|
|
7
20
|
import this file for exactly that reason, which split the token layer: id
|
|
8
21
|
took the colours and not the typeface. Self-hosting removes the reason, so
|
|
9
22
|
every surface can import styles.css unchanged.
|
|
10
|
-
-
|
|
11
|
-
|
|
23
|
+
- It was a render-blocking request to a host we do not control, on the
|
|
24
|
+
critical path of every surface, and it broke offline/air-gapped dev.
|
|
12
25
|
- One variable file per family replaces nine static weights, and it is fewer
|
|
13
|
-
bytes than the CSS-then-woff2 round trip
|
|
26
|
+
bytes than the CSS-then-woff2 round trip.
|
|
14
27
|
|
|
15
28
|
The url()s are relative to THIS file, so they resolve wherever the package is
|
|
16
29
|
mounted — node_modules, a CDN, a copied dist — with no configuration. */
|
|
17
30
|
|
|
18
31
|
@font-face{
|
|
19
|
-
font-family:"
|
|
20
|
-
src:url("../assets/fonts/
|
|
32
|
+
font-family:"Zen";
|
|
33
|
+
src:url("../assets/fonts/Zen-Variable.woff2") format("woff2");
|
|
21
34
|
font-weight:100 900;
|
|
22
35
|
font-style:normal;
|
|
23
36
|
font-display:swap;
|
|
24
37
|
}
|
|
25
38
|
@font-face{
|
|
26
|
-
font-family:"
|
|
27
|
-
src:url("../assets/fonts/
|
|
39
|
+
font-family:"Zen Mono";
|
|
40
|
+
src:url("../assets/fonts/ZenMono-Variable.woff2") format("woff2");
|
|
28
41
|
font-weight:100 900;
|
|
29
42
|
font-style:normal;
|
|
30
43
|
font-display:swap;
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
:root{
|
|
34
|
-
--font-sans:"
|
|
47
|
+
--font-sans:"Zen",ui-sans-serif,system-ui,sans-serif;
|
|
35
48
|
--font-display:var(--font-sans);
|
|
36
|
-
--font-mono:"
|
|
49
|
+
--font-mono:"Zen Mono",ui-monospace,SFMono-Regular,monospace;
|
|
37
50
|
--font-serif:Georgia,serif;
|
|
38
|
-
/* hanzo.ai sets these OpenType features on <body>.
|
|
51
|
+
/* hanzo.ai sets these OpenType features on <body>. ss01 is Zen's
|
|
52
|
+
single-storey a — the neo-grotesque letterform. */
|
|
39
53
|
--font-feature-settings:"ss01","ss02","cv01","cv02","cv03";
|
|
40
54
|
}
|
|
Binary file
|
|
Binary file
|