@hanzo/design 0.2.0 → 0.3.1
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/LICENSE +23 -16
- package/README.md +54 -5
- package/assets/fonts/Geist-Variable.woff2 +0 -0
- package/assets/fonts/GeistMono-Variable.woff2 +0 -0
- package/assets/fonts/LICENSE-Geist.txt +92 -0
- package/components/core/HanzoLogo.jsx +1 -1
- package/components/overlays/Dialog.jsx +1 -1
- package/components/overlays/DropdownMenu.jsx +1 -1
- package/components/overlays/Sheet.jsx +1 -1
- package/components/overlays/Toaster.jsx +1 -1
- package/dist/index.d.ts +22 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -8
- package/dist/tokens.gen.d.ts +64 -4
- package/dist/tokens.gen.d.ts.map +1 -1
- package/dist/tokens.gen.js +64 -4
- package/docs/README.md +1 -1
- package/docs/integrate.md +4 -4
- package/guidelines/DESIGN.md +106 -0
- package/package.json +15 -7
- package/prompts/README.md +13 -1
- package/prompts/studio-designer.md +41 -0
- package/prompts/system.md +1 -1
- package/scripts/check-tokens.mjs +93 -0
- package/scripts/gen-tokens.mjs +78 -0
- package/scripts/lint.mjs +179 -0
- package/skills/design-system/SKILL.md +103 -0
- package/src/index.ts +26 -9
- package/src/tokens.gen.ts +64 -4
- package/styles.css +4 -1
- package/tokens/base.css +7 -3
- package/tokens/colors.css +32 -4
- package/tokens/elevation.css +12 -0
- package/tokens/fonts.css +30 -3
- package/tokens/typography.css +19 -0
- package/tokens/z.css +18 -9
- package/ui_kits/.gitkeep +0 -0
package/dist/tokens.gen.js
CHANGED
|
@@ -44,7 +44,7 @@ export const colors = {
|
|
|
44
44
|
'destructive-foreground': '#f5f5f5',
|
|
45
45
|
'border': '#1f1f1f',
|
|
46
46
|
'input': '#1f1f1f',
|
|
47
|
-
'ring': '
|
|
47
|
+
'ring': 'var(--neutral-500)',
|
|
48
48
|
'brand': '#e4e4e7',
|
|
49
49
|
'brand-foreground': '#09090b',
|
|
50
50
|
'brand-muted': '#a3a3a3',
|
|
@@ -56,9 +56,14 @@ export const colors = {
|
|
|
56
56
|
'surface-card-quiet': 'rgb(23 23 23 / .4)',
|
|
57
57
|
'surface-overlay': 'rgb(10 10 10 / .95)',
|
|
58
58
|
'surface-header': 'rgb(0 0 0 / .7)',
|
|
59
|
+
'surface-scrim': 'rgb(0 0 0 / .8)',
|
|
59
60
|
'border-hairline': 'var(--neutral-800)',
|
|
60
61
|
'border-card': 'var(--white-10)',
|
|
61
|
-
'border-strong': 'var(--neutral-
|
|
62
|
+
'border-strong': 'var(--neutral-500)',
|
|
63
|
+
'surface-0': 'var(--background)',
|
|
64
|
+
'surface-1': 'var(--card)',
|
|
65
|
+
'surface-2': 'var(--muted)',
|
|
66
|
+
'surface-3': 'var(--secondary)',
|
|
62
67
|
'text-primary': 'var(--pure-white)',
|
|
63
68
|
'text-secondary': 'var(--white-80)',
|
|
64
69
|
'text-tertiary': 'var(--white-60)',
|
|
@@ -97,6 +102,23 @@ export const typography = {
|
|
|
97
102
|
'leading-6xl': '1',
|
|
98
103
|
'text-7xl': '4rem',
|
|
99
104
|
'leading-7xl': '1',
|
|
105
|
+
'text-8xl': '5.25rem',
|
|
106
|
+
'leading-8xl': '1',
|
|
107
|
+
'text-9xl': '7rem',
|
|
108
|
+
'leading-9xl': '1',
|
|
109
|
+
'font-size-xs': 'var(--text-xs)',
|
|
110
|
+
'font-size-sm': 'var(--text-sm)',
|
|
111
|
+
'font-size-base': 'var(--text-base)',
|
|
112
|
+
'font-size-lg': 'var(--text-lg)',
|
|
113
|
+
'font-size-xl': 'var(--text-xl)',
|
|
114
|
+
'font-size-2xl': 'var(--text-2xl)',
|
|
115
|
+
'font-size-3xl': 'var(--text-3xl)',
|
|
116
|
+
'font-size-4xl': 'var(--text-4xl)',
|
|
117
|
+
'font-size-5xl': 'var(--text-5xl)',
|
|
118
|
+
'font-size-6xl': 'var(--text-6xl)',
|
|
119
|
+
'font-size-7xl': 'var(--text-7xl)',
|
|
120
|
+
'font-size-8xl': 'var(--text-8xl)',
|
|
121
|
+
'font-size-9xl': 'var(--text-9xl)',
|
|
100
122
|
'weight-normal': '400',
|
|
101
123
|
'weight-medium': '500',
|
|
102
124
|
'weight-semibold': '600',
|
|
@@ -176,6 +198,12 @@ export const elevation = {
|
|
|
176
198
|
'shadow-floating': '0 25px 50px -12px rgb(0 0 0 / .25)',
|
|
177
199
|
'shadow-inset-hairline': 'inset 0 0 0 1px var(--white-10)',
|
|
178
200
|
'ring-focus': '0 0 0 2px var(--ring)',
|
|
201
|
+
'shadow-sm': '0 1px 2px 0 rgb(0 0 0 / .40)',
|
|
202
|
+
'shadow': '0 1px 3px 0 rgb(0 0 0 / .45), 0 1px 2px -1px rgb(0 0 0 / .45)',
|
|
203
|
+
'shadow-md': '0 4px 6px -1px rgb(0 0 0 / .50), 0 2px 4px -2px rgb(0 0 0 / .50)',
|
|
204
|
+
'shadow-lg': '0 10px 15px -3px rgb(0 0 0 / .55), 0 4px 6px -4px rgb(0 0 0 / .55)',
|
|
205
|
+
'shadow-xl': '0 20px 25px -5px rgb(0 0 0 / .60), 0 8px 10px -6px rgb(0 0 0 / .60)',
|
|
206
|
+
'shadow-2xl': 'var(--shadow-floating)',
|
|
179
207
|
'glow-hero': 'radial-gradient(circle,rgb(255 255 255 / .12) 0%,transparent 68%)',
|
|
180
208
|
'glow-hero-blur': '120px',
|
|
181
209
|
'sheen-card': 'radial-gradient(120% 120% at 80% 0%,rgb(255 255 255 / .08) 0%,transparent 55%)',
|
|
@@ -207,6 +235,8 @@ export const zIndex = {
|
|
|
207
235
|
'z-modal': '600',
|
|
208
236
|
'z-popover': '700',
|
|
209
237
|
'z-toast': '800',
|
|
238
|
+
'z-tooltip': 'var(--z-popover)',
|
|
239
|
+
'z-notification': 'var(--z-toast)',
|
|
210
240
|
};
|
|
211
241
|
/** fonts tokens (from tokens/fonts.css). Values are raw CSS. */
|
|
212
242
|
export const fonts = {
|
|
@@ -261,7 +291,7 @@ export const cssVars = {
|
|
|
261
291
|
'--destructive-foreground': '#f5f5f5',
|
|
262
292
|
'--border': '#1f1f1f',
|
|
263
293
|
'--input': '#1f1f1f',
|
|
264
|
-
'--ring': '
|
|
294
|
+
'--ring': 'var(--neutral-500)',
|
|
265
295
|
'--brand': '#e4e4e7',
|
|
266
296
|
'--brand-foreground': '#09090b',
|
|
267
297
|
'--brand-muted': '#a3a3a3',
|
|
@@ -273,9 +303,14 @@ export const cssVars = {
|
|
|
273
303
|
'--surface-card-quiet': 'rgb(23 23 23 / .4)',
|
|
274
304
|
'--surface-overlay': 'rgb(10 10 10 / .95)',
|
|
275
305
|
'--surface-header': 'rgb(0 0 0 / .7)',
|
|
306
|
+
'--surface-scrim': 'rgb(0 0 0 / .8)',
|
|
276
307
|
'--border-hairline': 'var(--neutral-800)',
|
|
277
308
|
'--border-card': 'var(--white-10)',
|
|
278
|
-
'--border-strong': 'var(--neutral-
|
|
309
|
+
'--border-strong': 'var(--neutral-500)',
|
|
310
|
+
'--surface-0': 'var(--background)',
|
|
311
|
+
'--surface-1': 'var(--card)',
|
|
312
|
+
'--surface-2': 'var(--muted)',
|
|
313
|
+
'--surface-3': 'var(--secondary)',
|
|
279
314
|
'--text-primary': 'var(--pure-white)',
|
|
280
315
|
'--text-secondary': 'var(--white-80)',
|
|
281
316
|
'--text-tertiary': 'var(--white-60)',
|
|
@@ -311,6 +346,23 @@ export const cssVars = {
|
|
|
311
346
|
'--leading-6xl': '1',
|
|
312
347
|
'--text-7xl': '4rem',
|
|
313
348
|
'--leading-7xl': '1',
|
|
349
|
+
'--text-8xl': '5.25rem',
|
|
350
|
+
'--leading-8xl': '1',
|
|
351
|
+
'--text-9xl': '7rem',
|
|
352
|
+
'--leading-9xl': '1',
|
|
353
|
+
'--font-size-xs': 'var(--text-xs)',
|
|
354
|
+
'--font-size-sm': 'var(--text-sm)',
|
|
355
|
+
'--font-size-base': 'var(--text-base)',
|
|
356
|
+
'--font-size-lg': 'var(--text-lg)',
|
|
357
|
+
'--font-size-xl': 'var(--text-xl)',
|
|
358
|
+
'--font-size-2xl': 'var(--text-2xl)',
|
|
359
|
+
'--font-size-3xl': 'var(--text-3xl)',
|
|
360
|
+
'--font-size-4xl': 'var(--text-4xl)',
|
|
361
|
+
'--font-size-5xl': 'var(--text-5xl)',
|
|
362
|
+
'--font-size-6xl': 'var(--text-6xl)',
|
|
363
|
+
'--font-size-7xl': 'var(--text-7xl)',
|
|
364
|
+
'--font-size-8xl': 'var(--text-8xl)',
|
|
365
|
+
'--font-size-9xl': 'var(--text-9xl)',
|
|
314
366
|
'--weight-normal': '400',
|
|
315
367
|
'--weight-medium': '500',
|
|
316
368
|
'--weight-semibold': '600',
|
|
@@ -381,6 +433,12 @@ export const cssVars = {
|
|
|
381
433
|
'--shadow-floating': '0 25px 50px -12px rgb(0 0 0 / .25)',
|
|
382
434
|
'--shadow-inset-hairline': 'inset 0 0 0 1px var(--white-10)',
|
|
383
435
|
'--ring-focus': '0 0 0 2px var(--ring)',
|
|
436
|
+
'--shadow-sm': '0 1px 2px 0 rgb(0 0 0 / .40)',
|
|
437
|
+
'--shadow': '0 1px 3px 0 rgb(0 0 0 / .45), 0 1px 2px -1px rgb(0 0 0 / .45)',
|
|
438
|
+
'--shadow-md': '0 4px 6px -1px rgb(0 0 0 / .50), 0 2px 4px -2px rgb(0 0 0 / .50)',
|
|
439
|
+
'--shadow-lg': '0 10px 15px -3px rgb(0 0 0 / .55), 0 4px 6px -4px rgb(0 0 0 / .55)',
|
|
440
|
+
'--shadow-xl': '0 20px 25px -5px rgb(0 0 0 / .60), 0 8px 10px -6px rgb(0 0 0 / .60)',
|
|
441
|
+
'--shadow-2xl': 'var(--shadow-floating)',
|
|
384
442
|
'--glow-hero': 'radial-gradient(circle,rgb(255 255 255 / .12) 0%,transparent 68%)',
|
|
385
443
|
'--glow-hero-blur': '120px',
|
|
386
444
|
'--sheen-card': 'radial-gradient(120% 120% at 80% 0%,rgb(255 255 255 / .08) 0%,transparent 55%)',
|
|
@@ -406,6 +464,8 @@ export const cssVars = {
|
|
|
406
464
|
'--z-modal': '600',
|
|
407
465
|
'--z-popover': '700',
|
|
408
466
|
'--z-toast': '800',
|
|
467
|
+
'--z-tooltip': 'var(--z-popover)',
|
|
468
|
+
'--z-notification': 'var(--z-toast)',
|
|
409
469
|
'--font-sans': '"Geist","Geist Sans",ui-sans-serif,system-ui,sans-serif',
|
|
410
470
|
'--font-display': 'var(--font-sans)',
|
|
411
471
|
'--font-mono': '"Geist Mono",ui-monospace,SFMono-Regular,monospace',
|
package/docs/README.md
CHANGED
package/docs/integrate.md
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
## 1. Install
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
pnpm add @
|
|
6
|
+
pnpm add @hanzo/design
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
## 2. Import the tokens — once
|
|
10
10
|
|
|
11
11
|
```css
|
|
12
12
|
/* app/globals.css, or the root of your styles */
|
|
13
|
-
@import "@
|
|
13
|
+
@import "@hanzo/design/styles.css";
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
That one file pulls in, in order: fonts (Geist + Geist Mono), color, typography,
|
|
@@ -54,8 +54,8 @@ Each part ships as `.jsx` + `.d.ts` + a `.prompt.md` usage guide. They are
|
|
|
54
54
|
self-contained (inline styles), so they need no framework:
|
|
55
55
|
|
|
56
56
|
```jsx
|
|
57
|
-
import { Button } from "@
|
|
58
|
-
import { HanzoLogo } from "@
|
|
57
|
+
import { Button } from "@hanzo/design/components/core/Button.jsx";
|
|
58
|
+
import { HanzoLogo } from "@hanzo/design/components/core/HanzoLogo.jsx";
|
|
59
59
|
|
|
60
60
|
<HanzoLogo />
|
|
61
61
|
<Button pill>Get started</Button>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Hanzo Design System — Canonical Tokens
|
|
2
|
+
|
|
3
|
+
`@hanzo/ui` is the single source of truth for the shared Hanzo product look across
|
|
4
|
+
the **Tailwind** apps (hanzo.chat, hanzo.app, hanzo console, commerce, hanzo-desktop).
|
|
5
|
+
Change a value here; apps converge on it. This file is that source of truth for the
|
|
6
|
+
three things that must read as **one product**: typography, the sidebar/panel system,
|
|
7
|
+
and the dark-black palette.
|
|
8
|
+
|
|
9
|
+
> One library: **`@hanzo/ui@8`** (`pkg/ui`, on **`@hanzo/gui`**) IS the component
|
|
10
|
+
> library — the cross-platform product/record layer every surface consumes.
|
|
11
|
+
> **`@hanzo/ui-shadcn`** (`pkgs/ui`) is the legacy shadcn/Tailwind/Radix kit, kept
|
|
12
|
+
> only for existing v5 consumers (pin `@hanzo/ui-shadcn@^5`; no new adoptions).
|
|
13
|
+
> This file stays the source of truth for the *token values* (fonts, dark palette,
|
|
14
|
+
> the sidebar glyph) both render.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. Typography — Basel Grotesk + Geist Mono
|
|
19
|
+
|
|
20
|
+
| Role | Family | Notes |
|
|
21
|
+
|------|--------|-------|
|
|
22
|
+
| UI / body / display / heading (`sans`) | **Basel Grotesk** | Self-hosted. Book = weight **400**, Medium = weight **500**. |
|
|
23
|
+
| code / data / mono (`mono`) | **Geist Mono** | `next/font/google` (`Geist_Mono`) or the geist CDN. |
|
|
24
|
+
| Arabic / Hebrew (`--font-ar` / `--font-he`) | unchanged | i18n only — keep. |
|
|
25
|
+
|
|
26
|
+
**Dropped as defaults:** Geist Sans, DM Sans, Figtree, Inter, PT Sans, Roboto Mono.
|
|
27
|
+
|
|
28
|
+
Basel is a **licensed, non-Google** face — **self-host** the woff2/woff, do NOT use
|
|
29
|
+
`next/font/google` for it. Canonical files (mirror lux.exchange):
|
|
30
|
+
`Basel-Grotesk-Book.woff2/.woff` (400), `Basel-Grotesk-Medium.woff2/.woff` (500).
|
|
31
|
+
|
|
32
|
+
`@font-face` (weights 400/500, `font-display: swap`, `font-style: normal`):
|
|
33
|
+
|
|
34
|
+
```css
|
|
35
|
+
@font-face {
|
|
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
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Per-app adoption (converge the value, keep each app's own mechanism):
|
|
46
|
+
- **@hanzo/ui / Next apps** → `next/font/local` for Basel (`--font-basel-sans`) +
|
|
47
|
+
`next/font/google` `Geist_Mono` (`--font-geist-mono`). See `app/lib/fonts.ts`;
|
|
48
|
+
tailwind `sans → var(--font-basel-sans)`, `mono → var(--font-geist-mono)`.
|
|
49
|
+
- **Vite + Tailwind apps** (chat, launcher, desktop) → self-host Basel `@font-face`
|
|
50
|
+
+ geist-mono CDN import; tailwind `fontFamily.sans = ['Basel', …]`,
|
|
51
|
+
`mono = ['Geist Mono', …]`.
|
|
52
|
+
- **Tamagui (console)** → Basel `@font-face` in globals + override the Tamagui
|
|
53
|
+
`body`/`heading` font `family` to Basel; Geist Mono for `code`/`pre`.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 2. Sidebar toggle icon — lucide `PanelLeft`
|
|
58
|
+
|
|
59
|
+
One glyph everywhere: lucide **`PanelLeft`** (the shadcn `SidebarTrigger` default).
|
|
60
|
+
Where a directional open/close affordance is wanted, use the pair
|
|
61
|
+
**`PanelLeftClose`** (expanded) / **`PanelLeft`** (collapsed). Never a hamburger,
|
|
62
|
+
a magnifier, a directional arrow, or a bespoke panel SVG for the sidebar toggle.
|
|
63
|
+
|
|
64
|
+
- Icon: stroke-2, ~16–20px, `currentColor`.
|
|
65
|
+
- Button: ghost/outline, square (`size-7`/`h-6 w-6`), subtle hover.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 3. Sidebar + panels
|
|
70
|
+
|
|
71
|
+
| Spec | Value |
|
|
72
|
+
|------|-------|
|
|
73
|
+
| Sidebar width (expanded) | **16rem / 256px** (`SIDEBAR_WIDTH`) |
|
|
74
|
+
| Sidebar width (collapsed / icon rail) | **3rem / 48px** (apps vary 48–70px) |
|
|
75
|
+
| Sidebar / panel surface | resting **#0a0a0a** over the true-black page |
|
|
76
|
+
| Border / separation | `border-border` — subtle white-alpha ~10% in dark (`border-r` / `border-l`) |
|
|
77
|
+
| Item hover | subtle `white/5` |
|
|
78
|
+
| Item active | monochrome `white/10` — **no colored accent** (the house style is monochrome) |
|
|
79
|
+
| Right panel rail | `border-l border-border`, same surface, collapsible |
|
|
80
|
+
|
|
81
|
+
Prefer the `@hanzo/ui` `Sidebar` primitive (`pkgs/ui/primitives/sidebar.tsx`,
|
|
82
|
+
`SidebarTrigger` → `PanelLeft`) where the app can consume it; otherwise match these
|
|
83
|
+
classes/tokens.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 4. Dark-black palette (true-black OLED)
|
|
88
|
+
|
|
89
|
+
The house dark theme is a **true-black** canvas (matches hanzo.ai marketing +
|
|
90
|
+
hanzo.chat OLED), with a shallow surface-depth ladder for cards/panels and quiet
|
|
91
|
+
hairline borders — never harsh pure-white on pure-black.
|
|
92
|
+
|
|
93
|
+
| Token | Value | Use |
|
|
94
|
+
|-------|-------|-----|
|
|
95
|
+
| Page background | **#000000** (`oklch(0 0 0)`) | body / canvas |
|
|
96
|
+
| Surface / sidebar / panel (resting) | **#0a0a0a** | sidebars, panels, cards |
|
|
97
|
+
| Press | **#050505** | pressed surface |
|
|
98
|
+
| Elevated / hover | **#171717** | hover, raised card |
|
|
99
|
+
| Border / divider | **rgba(255,255,255,0.10)** (≈ `#171717` opaque on black) | hairlines |
|
|
100
|
+
| Foreground (primary text) | near-white **#ededf1** (`oklch(0.985)`) — not pure `#fff` | body text |
|
|
101
|
+
| Muted / secondary text | `white/70` (≈ `#a1a1aa`) | secondary |
|
|
102
|
+
|
|
103
|
+
Keep each app's theme-token engine (CSS vars / Tailwind tokens / Tamagui `$color*`);
|
|
104
|
+
converge the **values/usage** to the table above, don't rip the engine.
|
|
105
|
+
|
|
106
|
+
Reference: `pkgs/ui/style/hanzo-default-colors.css` (`.dark` / `.hanzo-ui-dark-theme`).
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/design",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Hanzo Design System
|
|
5
|
-
"license": "
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Hanzo Design System — monochrome, dark-default tokens + components + brand assets, the single source of truth for every Hanzo surface. CSS + typed programmatic tokens.",
|
|
5
|
+
"license": "BSD-3-Clause",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/hanzoai/design.git"
|
|
@@ -25,12 +25,15 @@
|
|
|
25
25
|
"./components/*": "./components/*",
|
|
26
26
|
"./prompts/*": "./prompts/*",
|
|
27
27
|
"./content/*": "./content/*",
|
|
28
|
-
"./assets/*": "./assets/*"
|
|
28
|
+
"./assets/*": "./assets/*",
|
|
29
|
+
"./skills/*": "./skills/*"
|
|
29
30
|
},
|
|
30
31
|
"scripts": {
|
|
31
32
|
"gen": "node scripts/gen-tokens.mjs",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
33
|
+
"test": "node scripts/check-tokens.mjs && node scripts/lint.mjs components",
|
|
34
|
+
"build": "npm run gen && npm test && tsc -p tsconfig.json",
|
|
35
|
+
"prepublishOnly": "npm run build",
|
|
36
|
+
"lint": "node scripts/lint.mjs"
|
|
34
37
|
},
|
|
35
38
|
"devDependencies": {
|
|
36
39
|
"typescript": "^5.9.3"
|
|
@@ -42,6 +45,8 @@
|
|
|
42
45
|
"tokens",
|
|
43
46
|
"components",
|
|
44
47
|
"prompts",
|
|
48
|
+
"skills",
|
|
49
|
+
"scripts",
|
|
45
50
|
"content",
|
|
46
51
|
"docs",
|
|
47
52
|
"assets",
|
|
@@ -58,5 +63,8 @@
|
|
|
58
63
|
"publishConfig": {
|
|
59
64
|
"access": "public",
|
|
60
65
|
"registry": "https://registry.npmjs.org/"
|
|
66
|
+
},
|
|
67
|
+
"bin": {
|
|
68
|
+
"hanzo-design-lint": "./scripts/lint.mjs"
|
|
61
69
|
}
|
|
62
|
-
}
|
|
70
|
+
}
|
package/prompts/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# prompts — make an AI build it Hanzo
|
|
2
2
|
|
|
3
|
+
> **Entry point is `skills/design-system/SKILL.md`**, not this folder. The skill
|
|
4
|
+
> is what chat, hanzo.app and Claude Code load; it points here for the long form
|
|
5
|
+
> and finishes by running `hanzo-design-lint`. Everything below is the reference
|
|
6
|
+
> the skill cites.
|
|
7
|
+
|
|
3
8
|
Guidance an agent reads *before* generating a Hanzo surface. Two layers, no
|
|
4
9
|
overlap:
|
|
5
10
|
|
|
@@ -20,7 +25,14 @@ place.
|
|
|
20
25
|
Everything here assumes one import gives you the tokens:
|
|
21
26
|
|
|
22
27
|
```css
|
|
23
|
-
@import "@
|
|
28
|
+
@import "@hanzo/design/styles.css";
|
|
24
29
|
```
|
|
25
30
|
|
|
26
31
|
So every rule below can be obeyed with a CSS variable, never a hardcoded hex.
|
|
32
|
+
|
|
33
|
+
## The two prompts (one and one way)
|
|
34
|
+
- **`system.md`** — the design LANGUAGE prompt: paste as a system prompt to make any
|
|
35
|
+
model generate Hanzo-looking output (monochrome, true-black, token-only).
|
|
36
|
+
- **`studio-designer.md`** — the AGENTIC prompt Hanzo Studio's Design System designer
|
|
37
|
+
hands the Hanzo agent stack to GENERATE/EDIT this repo's file tree with its file
|
|
38
|
+
tools, grounded in `guidelines/DESIGN.md`. Uses `system.md`'s language rules.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Hanzo Studio — Design System Designer (agentic prompt)
|
|
2
|
+
|
|
3
|
+
The system prompt the Studio "Design System" designer hands to the Hanzo agent
|
|
4
|
+
stack. The agent generates/edits the design-system file tree in **hanzoai/design**
|
|
5
|
+
using its file tools (write/edit/tree), grounded in the canon.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
You are the **Hanzo Design System designer**, an agent operating on the
|
|
10
|
+
`hanzoai/design` repository. You produce and maintain a complete, coherent design
|
|
11
|
+
system as real files, using your file tools (read, write, edit, tree).
|
|
12
|
+
|
|
13
|
+
## The canon (never contradict)
|
|
14
|
+
- **Typography:** Basel Grotesk (`sans`, weights 400 Book / 500 Medium, self-hosted
|
|
15
|
+
woff2) + Geist Mono (`mono`). No Inter/Roboto/DM Sans as defaults.
|
|
16
|
+
- **Palette:** true-black OLED. Page `#000`, surface `#0a0a0a`, press `#050505`,
|
|
17
|
+
elevated `#171717`, border `rgba(255,255,255,.10)`, text `#ededf1`. **Monochrome —
|
|
18
|
+
no colored accents.** Semantic color only for live/error/warning.
|
|
19
|
+
- **Sidebar:** 256px expanded / 48px rail; lucide `PanelLeft` toggle; `white/5` hover,
|
|
20
|
+
`white/10` active.
|
|
21
|
+
- Full source: `guidelines/DESIGN.md`.
|
|
22
|
+
|
|
23
|
+
## Structure you own
|
|
24
|
+
`tokens/` (base·colors·fonts·spacing·radius·elevation·motion.css + TS modules) ·
|
|
25
|
+
`guidelines/` · `assets/{brand,logos,providers,fonts}` · `components/` · `ui_kits/`.
|
|
26
|
+
|
|
27
|
+
## How you work
|
|
28
|
+
1. **Read before writing.** `tree` the repo + read `guidelines/DESIGN.md` and the
|
|
29
|
+
relevant `tokens/*` so every new value stays consistent with what exists.
|
|
30
|
+
2. **Tokens are the source.** A component reads token vars (`var(--hz-*)`), never a
|
|
31
|
+
hardcoded hex/px. If a needed token is missing, add it to `tokens/` first, then use it.
|
|
32
|
+
3. **One and one way.** One place per value. Don't duplicate a color/space/font.
|
|
33
|
+
4. **Generate real, coherent files** — a request like "add a Card component" writes
|
|
34
|
+
`components/card.{css,tsx,md}` referencing the tokens + a usage note, not a mock.
|
|
35
|
+
5. **Honor the request scope.** "Generate a design system" → the full tree from the
|
|
36
|
+
canon. "Add elevation tokens" → just `tokens/elevation.css` + wire into `base.css`.
|
|
37
|
+
6. **Never fabricate brand assets.** Reference real files in `assets/`; if a logo/font
|
|
38
|
+
is missing, note it as a TODO, don't invent bytes.
|
|
39
|
+
7. **Explain what you wrote** (files + why) so the review is legible.
|
|
40
|
+
|
|
41
|
+
Output: the files, written to the repo, plus a one-paragraph summary of the change.
|
package/prompts/system.md
CHANGED
|
@@ -12,7 +12,7 @@ and frontier models. Every surface you produce obeys one atom:
|
|
|
12
12
|
|
|
13
13
|
## Foundation
|
|
14
14
|
|
|
15
|
-
- **Import once.** `@import "@
|
|
15
|
+
- **Import once.** `@import "@hanzo/design/styles.css";` gives you every token
|
|
16
16
|
as a CSS custom property. Use the variables. Never write a raw hex, rgb, px
|
|
17
17
|
font-size, or magic z-index — if you reach for a literal, there is a token for
|
|
18
18
|
it and you are doing it wrong.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// check-tokens.mjs — the gate. Every defect this file tests for was, at some
|
|
2
|
+
// point, LIVE and SILENT: an undefined custom property paints nothing, an
|
|
3
|
+
// unimported token file resolves nothing, and a 1.66:1 focus ring looks fine to
|
|
4
|
+
// whoever shipped it. None of them can fail loudly on their own, so they fail
|
|
5
|
+
// here. Run via `npm test` (part of `build`).
|
|
6
|
+
import { readFileSync, readdirSync } from 'node:fs'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import { dirname, join } from 'node:path'
|
|
9
|
+
|
|
10
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
11
|
+
const tokensDir = join(root, 'tokens')
|
|
12
|
+
const strip = (s) => s.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
13
|
+
const read = (p) => readFileSync(p, 'utf8')
|
|
14
|
+
|
|
15
|
+
let failures = 0
|
|
16
|
+
const fail = (msg) => { console.error(` FAIL ${msg}`); failures++ }
|
|
17
|
+
const pass = (msg) => console.log(` ok ${msg}`)
|
|
18
|
+
|
|
19
|
+
// ── 1. styles.css must serve every token file ────────────────────────────
|
|
20
|
+
// tokens/z.css was authored, exported and documented — and left out of
|
|
21
|
+
// styles.css, so the whole ladder resolved to nothing on every consumer.
|
|
22
|
+
{
|
|
23
|
+
const entry = read(join(root, 'styles.css'))
|
|
24
|
+
const files = readdirSync(tokensDir).filter((f) => f.endsWith('.css'))
|
|
25
|
+
const missing = files.filter((f) => !entry.includes(`tokens/${f}`))
|
|
26
|
+
missing.length
|
|
27
|
+
? fail(`styles.css does not import: ${missing.join(', ')}`)
|
|
28
|
+
: pass(`styles.css serves all ${files.length} token files`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── 2. every var() used inside the token layer must resolve ──────────────
|
|
32
|
+
{
|
|
33
|
+
const declared = new Set()
|
|
34
|
+
const used = new Map() // name -> file
|
|
35
|
+
for (const f of readdirSync(tokensDir).filter((f) => f.endsWith('.css'))) {
|
|
36
|
+
const css = strip(read(join(tokensDir, f)))
|
|
37
|
+
for (const [, n] of css.matchAll(/--([A-Za-z0-9-]+)\s*:/g)) declared.add(n)
|
|
38
|
+
for (const [, n] of css.matchAll(/var\(\s*--([A-Za-z0-9-]+)/g)) if (!used.has(n)) used.set(n, f)
|
|
39
|
+
}
|
|
40
|
+
const ghosts = [...used].filter(([n]) => !declared.has(n))
|
|
41
|
+
ghosts.length
|
|
42
|
+
? ghosts.forEach(([n, f]) => fail(`tokens/${f} references --${n}, which nothing declares`))
|
|
43
|
+
: pass(`all ${used.size} internal var() references resolve`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── 3. the non-text contrast gate (WCAG 1.4.11 / 2.4.11) ─────────────────
|
|
47
|
+
// A focus indicator and a control boundary must clear 3:1 against EVERY surface
|
|
48
|
+
// they can land on, in BOTH themes. --ring was #333333 = 1.66:1 on --background.
|
|
49
|
+
{
|
|
50
|
+
const css = strip(read(join(tokensDir, 'colors.css')))
|
|
51
|
+
const block = (re) => { const m = css.match(re); const o = {}; if (m) for (const [, n, v] of m[1].matchAll(/--([A-Za-z0-9-]+)\s*:\s*([^;]+);/g)) o[n] = v.trim(); return o }
|
|
52
|
+
const dark = block(/:root\s*\{([\s\S]*?)\n\}/)
|
|
53
|
+
const themes = { dark, light: { ...dark, ...block(/\.light\s*\{([\s\S]*?)\n\}/) } }
|
|
54
|
+
|
|
55
|
+
const deref = (v, s, d = 0) => { const m = d < 10 && v && String(v).match(/^var\(\s*--([A-Za-z0-9-]+)\s*\)$/); return m ? deref(s[m[1]], s, d + 1) : v }
|
|
56
|
+
const rgb = (v) => {
|
|
57
|
+
if (!v) return null
|
|
58
|
+
let m = String(v).trim().match(/^#([0-9a-f]{6})$/i)
|
|
59
|
+
if (m) { const n = parseInt(m[1], 16); return [n >> 16 & 255, n >> 8 & 255, n & 255, 1] }
|
|
60
|
+
m = String(v).trim().match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)\s*(?:[/,]\s*([\d.]+))?\s*\)$/i)
|
|
61
|
+
return m ? [+m[1], +m[2], +m[3], m[4] === undefined ? 1 : +m[4]] : null
|
|
62
|
+
}
|
|
63
|
+
const over = (f, b) => [0, 1, 2].map((i) => f[i] * f[3] + b[i] * (1 - f[3]))
|
|
64
|
+
const lin = (c) => { c /= 255; return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4 }
|
|
65
|
+
const lum = ([r, g, b]) => 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b)
|
|
66
|
+
const ratio = (fg, bg, s) => {
|
|
67
|
+
const f = rgb(deref(fg, s)), b = rgb(deref(bg, s))
|
|
68
|
+
if (!f || !b) return null
|
|
69
|
+
const bo = b[3] < 1 ? over(b, [0, 0, 0, 1]) : b.slice(0, 3)
|
|
70
|
+
const fo = f[3] < 1 ? over(f, bo) : f.slice(0, 3)
|
|
71
|
+
const [A, B] = [lum(fo), lum(bo)].sort((x, y) => y - x)
|
|
72
|
+
return (A + 0.05) / (B + 0.05)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const CANVASES = ['background', 'card', 'popover', 'muted', 'secondary', 'surface-card', 'surface-overlay']
|
|
76
|
+
const GATED = { ring: 3, 'border-strong': 3 }
|
|
77
|
+
for (const [theme, scope] of Object.entries(themes)) {
|
|
78
|
+
for (const [tok, min] of Object.entries(GATED)) {
|
|
79
|
+
let worst = Infinity, where = ''
|
|
80
|
+
for (const c of CANVASES) {
|
|
81
|
+
const r = ratio(scope[tok], scope[c], scope)
|
|
82
|
+
if (r !== null && r < worst) { worst = r; where = `--${c}` }
|
|
83
|
+
}
|
|
84
|
+
if (!isFinite(worst)) { fail(`--${tok} (${theme}) could not be measured`); continue }
|
|
85
|
+
worst < min
|
|
86
|
+
? fail(`--${tok} (${theme}) is ${worst.toFixed(2)}:1 on ${where} — needs ${min}:1`)
|
|
87
|
+
: pass(`--${tok} (${theme}) ${worst.toFixed(2)}:1 worst case (${where})`)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(failures ? `\n${failures} check(s) failed` : '\nall token checks passed')
|
|
93
|
+
process.exit(failures ? 1 : 0)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// gen-tokens.mjs — the ONE source→TS bridge for @hanzo/design.
|
|
2
|
+
// The hand-authored token CSS under tokens/*.css is the source of truth; this
|
|
3
|
+
// script parses every `--name: value;` custom property out of it and emits a
|
|
4
|
+
// typed TypeScript module (src/tokens.gen.ts). CSS and code therefore can never
|
|
5
|
+
// drift — you edit a token in ONE place (the CSS) and both the stylesheet and
|
|
6
|
+
// the programmatic API update. Run via `npm run gen` (part of `build`).
|
|
7
|
+
import { readFileSync, writeFileSync, readdirSync } from 'node:fs'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
import { dirname, join } from 'node:path'
|
|
10
|
+
|
|
11
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
12
|
+
const tokensDir = join(root, 'tokens')
|
|
13
|
+
|
|
14
|
+
// The token files, in the same order styles.css imports them. base.css is the
|
|
15
|
+
// semantic-alias layer (references other vars) — parsed too, so `--background`
|
|
16
|
+
// etc. are available programmatically.
|
|
17
|
+
const FILES = ['colors', 'typography', 'spacing', 'radius', 'elevation', 'motion', 'z', 'fonts', 'base']
|
|
18
|
+
|
|
19
|
+
// group name for each file (the export const); a couple read better renamed.
|
|
20
|
+
const GROUP = { z: 'zIndex' }
|
|
21
|
+
|
|
22
|
+
/** Strip /* */ comments, then pull every `--name: value;` pair (multi-per-line ok). */
|
|
23
|
+
function parse(css) {
|
|
24
|
+
const noComments = css.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
25
|
+
const out = []
|
|
26
|
+
const re = /--([A-Za-z0-9-]+)\s*:\s*([^;]+);/g
|
|
27
|
+
let m
|
|
28
|
+
while ((m = re.exec(noComments))) out.push([m[1].trim(), m[2].trim()])
|
|
29
|
+
return out
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// A token may be authored more than once — the dark `:root{}` default and a
|
|
33
|
+
// light `@media`/`[data-theme]` override carry the SAME name. Keep the FIRST
|
|
34
|
+
// occurrence (dark, the default theme): the static map is the authored default;
|
|
35
|
+
// live theme switching happens through the CSS cascade via cssVar()/var(--x).
|
|
36
|
+
const groupMaps = {} // groupName -> Map(name->value), first-wins
|
|
37
|
+
const flatMap = new Map() // '--name' -> value, first-wins
|
|
38
|
+
for (const f of FILES) {
|
|
39
|
+
let css
|
|
40
|
+
try {
|
|
41
|
+
css = readFileSync(join(tokensDir, `${f}.css`), 'utf8')
|
|
42
|
+
} catch {
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
const g = GROUP[f] ?? f
|
|
46
|
+
const gm = (groupMaps[g] ??= new Map())
|
|
47
|
+
for (const [n, v] of parse(css)) {
|
|
48
|
+
if (!gm.has(n)) gm.set(n, v)
|
|
49
|
+
if (!flatMap.has(`--${n}`)) flatMap.set(`--${n}`, v)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const groups = Object.fromEntries(Object.entries(groupMaps).map(([g, m]) => [g, [...m]]))
|
|
53
|
+
const flat = [...flatMap]
|
|
54
|
+
|
|
55
|
+
const esc = (s) => s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
|
|
56
|
+
const objLit = (pairs) =>
|
|
57
|
+
'{\n' + pairs.map(([n, v]) => ` '${esc(n)}': '${esc(v)}',`).join('\n') + '\n} as const'
|
|
58
|
+
|
|
59
|
+
let ts = `// AUTO-GENERATED by scripts/gen-tokens.mjs from tokens/*.css — DO NOT EDIT.
|
|
60
|
+
// Edit the token in tokens/<group>.css and re-run \`npm run gen\`.
|
|
61
|
+
/* eslint-disable */
|
|
62
|
+
|
|
63
|
+
`
|
|
64
|
+
|
|
65
|
+
// Per-group typed maps (keyed by the var name WITHOUT the leading '--').
|
|
66
|
+
for (const [g, pairs] of Object.entries(groups)) {
|
|
67
|
+
ts += `/** ${g} tokens (from tokens/${Object.keys(GROUP).find((k) => GROUP[k] === g) ?? g}.css). Values are raw CSS. */\n`
|
|
68
|
+
ts += `export const ${g} = ${objLit(pairs)}\n\n`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The complete flat map, keyed by the literal CSS custom-property name.
|
|
72
|
+
ts += `/** Every token, keyed by its literal CSS custom-property name ('--background', …). */\n`
|
|
73
|
+
ts += `export const cssVars = ${objLit(flat)}\n\n`
|
|
74
|
+
|
|
75
|
+
ts += `export type CssVarName = keyof typeof cssVars\n`
|
|
76
|
+
|
|
77
|
+
writeFileSync(join(root, 'src', 'tokens.gen.ts'), ts)
|
|
78
|
+
console.log(`gen-tokens: wrote src/tokens.gen.ts — ${flat.length} tokens across ${Object.keys(groups).length} groups`)
|