@hanzo/design 0.4.0 → 0.4.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/tokens/colors.css CHANGED
@@ -1,9 +1,25 @@
1
1
  /* Hanzo is monochrome. One hue rendered through an opacity ladder.
2
2
  Base ladder = Tailwind neutral (tailwind.config.ts). Semantic names match
3
3
  hanzo.ai's CSS variables exactly, so code copies over 1:1.
4
- DARK IS THE DEFAULT THEME (hanzo.ai mounts ThemeProvider defaultTheme="dark"). */
4
+ DARK IS THE DEFAULT THEME (hanzo.ai mounts ThemeProvider defaultTheme="dark").
5
+
6
+ The dark palette is derived from the chat surface in hanzoai/extension
7
+ (packages/browser/src/sidebar.css), which is the reference aesthetic: a
8
+ #0a0a0a ground, #fafafa ink, and boundaries drawn as LOW-ALPHA HAIRLINES.
9
+ Two properties of that reference are load-bearing and easy to lose:
10
+
11
+ 1. Borders are alpha, not hex. rgb(255 255 255 / .10) composites correctly on
12
+ #0a0a0a AND on #262626; a solid #1f1f1f reads on the page and vanishes the
13
+ moment it lands on a lifted surface — which is why anyone who needed a
14
+ visible edge reached past it for the CONTROL rung and got a wireframe.
15
+ 2. Surfaces lift by tiny steps (0a -> 0f -> 17 -> 26), and ink is a graded
16
+ ramp (#fafafa -> .78 -> .55 -> #a3a3a3), not one flat white. Pure #ffffff
17
+ on pure #000000 is 21:1 — the maximum-contrast pair that exists, and it
18
+ halates. #fafafa on #0a0a0a is 18.1:1: still far past AAA, visibly softer. */
5
19
 
6
20
  :root{
21
+ color-scheme:dark;
22
+
7
23
  /* ——— base neutral ladder ——— */
8
24
  --neutral-50:#FAFAFA;
9
25
  --neutral-100:#F5F5F5;
@@ -23,45 +39,97 @@
23
39
  --hanzo-white:#FFFFFF;
24
40
 
25
41
  /* ——— the opacity ladder: the real palette ——— */
42
+ /* Every rung below .30 is a HAIRLINE or a LIFT — the two things this system
43
+ draws with. The rungs are the reference's own values, not a rounded ramp. */
26
44
  --white-05:rgb(255 255 255 / .05);
45
+ --white-06:rgb(255 255 255 / .06);
46
+ --white-08:rgb(255 255 255 / .08);
27
47
  --white-10:rgb(255 255 255 / .10);
28
48
  --white-15:rgb(255 255 255 / .15);
49
+ --white-16:rgb(255 255 255 / .16);
29
50
  --white-20:rgb(255 255 255 / .20);
51
+ --white-22:rgb(255 255 255 / .22);
30
52
  --white-30:rgb(255 255 255 / .30);
31
53
  --white-40:rgb(255 255 255 / .40);
54
+ --white-55:rgb(255 255 255 / .55);
32
55
  --white-60:rgb(255 255 255 / .60);
56
+ --white-78:rgb(255 255 255 / .78);
33
57
  --white-80:rgb(255 255 255 / .80);
34
58
 
35
59
  /* ——— semantic aliases (dark, the default) ——— */
36
- --background:#000000;
37
- --foreground:#ededed;
38
- --card:#0a0a0a;
39
- --card-foreground:#f5f5f5;
40
- --popover:#0a0a0a;
41
- --popover-foreground:#f5f5f5;
42
- --primary:#ffffff;
43
- --primary-foreground:#000000;
44
- --secondary:#1a1a1a;
45
- --secondary-foreground:#f5f5f5;
46
- --muted:#101010;
47
- --muted-foreground:#888888;
48
- --accent:#1a1a1a;
49
- --accent-foreground:#f5f5f5;
60
+ --background:#0a0a0a;
61
+ --foreground:#fafafa;
62
+ --card:#0f0f0f;
63
+ --card-foreground:#fafafa;
64
+ --popover:#0f0f0f;
65
+ --popover-foreground:#fafafa;
66
+ --primary:#fafafa;
67
+ --primary-hover:#d4d4d4;
68
+ --primary-foreground:#0a0a0a;
69
+ --secondary:#262626;
70
+ --secondary-foreground:#fafafa;
71
+ --muted:#171717;
72
+ --muted-foreground:#a3a3a3;
73
+ --accent:#262626;
74
+ --accent-foreground:#fafafa;
50
75
  --destructive:#666666;
51
- --destructive-foreground:#f5f5f5;
52
- --border:#1f1f1f;
53
- --input:#1f1f1f;
76
+ --destructive-foreground:#fafafa;
77
+
78
+ /* ——— boundaries ———
79
+ THREE tiers, and the split is the whole point. The first two are DECORATIVE:
80
+ they separate content, WCAG imposes no ratio on them, and they must stay
81
+ quiet. The third is PERCEIVABLE: it is the edge that IS the affordance.
82
+
83
+ --border the hairline. Every card, panel, divider, list row.
84
+ --border-strong the SAME hairline, one step up. Hover, active, emphasis.
85
+ --border-control a control's edge — input, select, textarea, checkbox,
86
+ switch. Must clear 3:1 on every surface in both themes
87
+ (WCAG 1.4.11); check-tokens.mjs pins it.
88
+
89
+ Before this split there were only two rungs and they were an octave apart:
90
+ --border at #1f1f1f (1.27:1, invisible on anything lifted) and
91
+ --border-strong at --neutral-500 (4.43:1). Nothing sat between, so every
92
+ surface that wanted a border it could actually SEE reached for the control
93
+ rung — and a 1px mid-grey box around a card on near-black is a wireframe.
94
+ That is the "garish outlines" complaint, and it is fixed by having a middle.
95
+
96
+ A boundary that is WCAG-perceivable on a near-black ground IS a mid-grey;
97
+ that cannot be tuned away. So it is not tuned away — it is NAMED, so it can
98
+ only be spent where it is owed. "strong" is a degree word and will always be
99
+ read as one; "control" is a duty.
100
+
101
+ Which ladder a boundary is cut from follows from that, and is the whole
102
+ rule: DECORATIVE edges come off the ALPHA ladder, because they owe quiet
103
+ and they must composite over whatever surface they land on. CONTROL edges
104
+ come off the NEUTRAL ladder, because they owe a contrast ratio, and a ratio
105
+ you can only meet at a fixed value is not something to express as alpha.
106
+ --border-focus is therefore one NEUTRAL rung above --border-control, not an
107
+ alpha step: a focused field has to read brighter than a resting one, and
108
+ the resting one already sits at the WCAG floor. */
109
+ --border:var(--white-10);
110
+ --border-strong:var(--white-16);
111
+ --border-control:var(--neutral-500); /* 4.43:1 worst case — see --ring */
112
+ --border-focus:var(--neutral-400); /* one rung up: the field is focused */
113
+ --input:var(--border-control); /* shadcn `border-input` — a control */
54
114
  /* A focus indicator is a NON-TEXT CONTRAST target: WCAG 2.4.11/1.4.11 require
55
115
  3:1 against every surface it can land on. --neutral-500 is the only rung on
56
- this ladder that clears 3:1 on all of them — #000000, #0a0a0a, #101010,
57
- #1a1a1a, AND the light theme's #ffffff/#f5f5f5 — so one value serves both
116
+ this ladder that clears 3:1 on all of them — #0a0a0a, #0f0f0f, #171717,
117
+ #262626, AND the light theme's #ffffff/#fafafa — so one value serves both
58
118
  themes. (Was #333333 = 1.66:1 on --background: not a focus indicator.) */
59
119
  --ring:var(--neutral-500);
60
120
  --brand:#e4e4e7;
61
121
  --brand-foreground:#09090b;
62
122
  --brand-muted:#a3a3a3;
63
123
  --black:#000000;
64
- --white:#f5f5f5;
124
+ --white:#fafafa;
125
+ --selection:var(--white-20);
126
+
127
+ /* ——— glass: the lift ———
128
+ How a surface rises WITHOUT a block fill. An alpha-white wash composites
129
+ over whatever it lands on, so one value works on the page, on a card and
130
+ inside a popover. This is the mechanism behind the reference's depth. */
131
+ --glass:var(--white-05);
132
+ --glass-strong:var(--white-08);
65
133
 
66
134
  /* ——— surface recipes (card fills used across hanzo.ai) ——— */
67
135
  --surface-page:var(--background);
@@ -71,29 +139,22 @@
71
139
  --surface-overlay:rgb(10 10 10 / .95); /* dropdown / popover panels */
72
140
  --surface-header:rgb(0 0 0 / .7); /* fixed nav, with backdrop blur */
73
141
  --surface-scrim:rgb(0 0 0 / .8); /* the dialog / sheet backdrop */
74
- /* Boundaries come in two kinds and they are NOT interchangeable.
75
- DECORATIVE (--border, --border-hairline, --border-card): separates content;
76
- WCAG imposes no ratio. Keep them quiet.
77
- PERCEIVABLE (--border-strong): identifies a CONTROL — an input edge, a
78
- switch, a checkbox — and must clear 3:1 (WCAG 1.4.11) on every surface.
79
- Reach for --border-strong whenever the boundary IS the affordance. */
80
- --border-hairline:var(--neutral-800);
81
- --border-card:var(--white-10);
82
- --border-strong:var(--neutral-500); /* 3.59:1 worst case — see --ring */
83
142
 
84
143
  /* ——— the numeric surface ladder ——— */
85
144
  /* Aliases onto the semantic canvases above, so a brand fork that retunes
86
145
  --card/--muted/--secondary retunes the ladder with it and the light theme
87
- inverts for free. Ascending lift: 0 is the page, 3 is a hovered control. */
146
+ inverts for free. Ascending lift: 0 is the page, 3 is a hovered control.
147
+ Dark resolves to the reference's own ladder — 0a / 0f / 17 / 26. */
88
148
  --surface-0:var(--background);
89
149
  --surface-1:var(--card);
90
150
  --surface-2:var(--muted);
91
151
  --surface-3:var(--secondary);
92
152
 
93
- /* ——— text ranks ——— */
94
- --text-primary:var(--pure-white);
95
- --text-secondary:var(--white-80);
96
- --text-tertiary:var(--white-60);
153
+ /* ——— text ranks ———
154
+ A graded ramp, not one flat white. The steps are the reference's. */
155
+ --text-primary:#fafafa;
156
+ --text-secondary:var(--white-78);
157
+ --text-tertiary:var(--white-55);
97
158
  --text-helper:var(--muted-foreground);
98
159
  --text-disabled:var(--white-30);
99
160
 
@@ -108,46 +169,54 @@
108
169
  --chrome-dot-green:rgb(34 197 94 / .6);
109
170
  }
110
171
 
111
- /* Light theme — the same tokens, inverted. Rare: only /brand-style docs pages. */
172
+ /* Light theme — the same tokens, inverted.
173
+ The white-opacity ladder does NOT invert: --white-16 is white-on-white here.
174
+ So EVERY token whose dark value is a --white-* rung must be restated below,
175
+ and check-tokens.mjs fails the build if one is missed — that class of bug is
176
+ silent (the border simply stops existing) and has shipped before. */
112
177
  .light{
178
+ color-scheme:light;
113
179
  --background:#ffffff;
114
180
  --foreground:#0a0a0a;
115
- --card:#f5f5f5;
181
+ /* A ladder, not four names for #f5f5f5. Light lifts by the same tiny steps
182
+ dark does — ~2% per rung — so --surface-0..3 mean something in both themes. */
183
+ --card:#fafafa;
116
184
  --card-foreground:#0a0a0a;
117
185
  --popover:#ffffff;
118
186
  --popover-foreground:#0a0a0a;
119
187
  --primary:#0a0a0a;
120
- --primary-foreground:#ffffff;
121
- --secondary:#f5f5f5;
188
+ --primary-hover:#262626;
189
+ --primary-foreground:#fafafa;
190
+ --secondary:#ededed;
122
191
  --secondary-foreground:#0a0a0a;
123
192
  --muted:#f5f5f5;
124
193
  --muted-foreground:#525252;
125
- --accent:#f5f5f5;
194
+ --accent:#ededed;
126
195
  --accent-foreground:#0a0a0a;
127
196
  --destructive:#999999;
128
197
  --destructive-foreground:#ffffff;
129
- --border:#e5e5e5;
130
- --input:#e5e5e5;
198
+ --border:rgb(0 0 0 / .10);
199
+ --border-strong:rgb(0 0 0 / .16);
200
+ --border-control:var(--neutral-500); /* 4.74:1 on #ffffff — conformant here too */
201
+ --border-focus:var(--neutral-600); /* one rung DOWN — darker reads as brighter here */
202
+ --input:var(--border-control);
131
203
  /* Same rung as dark: #d4d4d4 measured 1.48:1 on white and could not carry a
132
204
  focus indicator either. --neutral-500 is 4.74:1 on #ffffff / 4.38:1 on
133
205
  #f5f5f5, so ONE value is conformant in both themes. */
134
206
  --ring:var(--neutral-500);
135
207
  --black:#0a0a0a;
136
208
  --white:#ffffff;
137
- --surface-card:#f5f5f5;
209
+ --selection:rgb(0 0 0 / .16);
210
+ --glass:rgb(0 0 0 / .04);
211
+ --glass-strong:rgb(0 0 0 / .07);
212
+ --surface-card:#fafafa;
138
213
  --surface-card-emphasis:#ffffff;
139
- --surface-card-quiet:#fafafa;
214
+ --surface-card-quiet:#fcfcfc;
140
215
  --surface-overlay:rgb(255 255 255 / .95);
141
216
  --surface-header:rgb(255 255 255 / .8);
142
217
  --surface-scrim:rgb(0 0 0 / .5);
143
- --border-hairline:var(--neutral-200);
144
- --border-card:rgb(0 0 0 / .1);
145
- --border-strong:var(--neutral-500); /* was --neutral-300 = 1.48:1 on white */
146
- /* The white-opacity ladder does NOT invert, so --white-40 is white-on-white
147
- here (1.00:1). Anything that needs a visible edge in BOTH themes must use
148
- --border-strong, never a --white-* rung. */
149
218
  --text-primary:var(--neutral-950);
150
- --text-secondary:rgb(10 10 10 / .8);
151
- --text-tertiary:rgb(10 10 10 / .6);
219
+ --text-secondary:rgb(10 10 10 / .78);
220
+ --text-tertiary:rgb(10 10 10 / .55);
152
221
  --text-disabled:rgb(10 10 10 / .3);
153
222
  }
@@ -30,16 +30,55 @@
30
30
  --golden-9:11.749rem;
31
31
  --golden-split:38.2% 61.8%; /* @kind other */
32
32
 
33
- /* layout rules (DESIGN.md §1.3) */
33
+ /* layout rules (DESIGN.md §1.3)
34
+ MOBILE-FIRST: the values authored here are the PHONE values, and the
35
+ min-width block at the bottom of this file scales them up. A surface that
36
+ uses the raw token therefore gets a layout that already breathes correctly
37
+ at 390px, with no media query of its own. (This is also the value the
38
+ generator captures for tokens.gen.ts — first occurrence wins, and the
39
+ authored default is the small one.) */
34
40
  --container-max:80rem; /* max-w-7xl — grids */
35
41
  --container-prose:48rem; /* max-w-3xl — centered text */
36
42
  --container-wide:72rem; /* max-w-6xl — landing sections */
37
- --gutter:1rem; /* px-4 */
43
+ /* The page gutter also clears a notch. env() is 0px on every device without
44
+ one, so this is exactly `1rem` in the ordinary case and the safe inset when
45
+ there is something to avoid — no per-app work, no landscape clipping.
46
+ Requires `<meta name="viewport" content="…,viewport-fit=cover">` on the
47
+ host page; without it the UA reports 0 and the max() is inert, not wrong. */
48
+ --gutter:max(1rem,var(--safe-left),var(--safe-right));
38
49
  --gutter-sm:1.5rem; /* sm:px-6 */
39
50
  --gutter-lg:2rem; /* lg:px-8 */
40
- --section-y:4rem; /* py-16 content sections */
41
- --section-y-lg:6rem; /* py-24 landing sections */
42
- --hero-y:5rem; /* py-20 */
43
- --hero-y-lg:8rem; /* lg:py-32 */
44
- --header-height:4rem;
51
+ --section-y:2.5rem; /* py-10 on a phone → py-16 at md */
52
+ --section-y-lg:3.5rem; /* py-14 on a phone → py-24 at md */
53
+ --hero-y:3rem; /* py-12 on a phone → py-20 at md */
54
+ --hero-y-lg:4.5rem; /* py-18 on a phone → py-32 at md */
55
+ --header-height:3.5rem; /* 56px is the phone bar; 64px from md */
56
+
57
+ /* ——— touch ——— */
58
+ /* 44px is the floor a pointer-coarse target may render at (Apple HIG 44pt /
59
+ WCAG 2.5.5 AAA / 2.5.8 AA's 24px, taken at the higher bar). base.css spends
60
+ it automatically under `@media (pointer:coarse)`, so a button does not have
61
+ to opt in. It was previously a bare `min-height:44px` literal in
62
+ hanzoai/id's stylesheet — one app knowing something the system did not. */
63
+ --tap-target:44px;
64
+
65
+ /* ——— safe areas ——— */
66
+ /* The notch/home-indicator insets, named once. Anything pinned to a viewport
67
+ edge — a fixed header, a bottom bar, a sheet — adds the matching one. */
68
+ --safe-top:env(safe-area-inset-top,0px);
69
+ --safe-right:env(safe-area-inset-right,0px);
70
+ --safe-bottom:env(safe-area-inset-bottom,0px);
71
+ --safe-left:env(safe-area-inset-left,0px);
72
+ }
73
+
74
+ /* Scale up from the phone. `md` (48rem) is the same breakpoint grid.css
75
+ declares and Tailwind compiles against — one value, not two that agree. */
76
+ @media (min-width:48rem){
77
+ :root{
78
+ --section-y:4rem; /* py-16 — content sections */
79
+ --section-y-lg:6rem; /* py-24 — landing sections */
80
+ --hero-y:5rem; /* py-20 … */
81
+ --hero-y-lg:8rem; /* … lg:py-32 */
82
+ --header-height:4rem;
83
+ }
45
84
  }
@@ -36,6 +36,19 @@
36
36
  --font-size-8xl:var(--text-8xl);
37
37
  --font-size-9xl:var(--text-9xl);
38
38
 
39
+ /* The size a FORM CONTROL renders at — and the ONE step in this scale that is
40
+ not a constant. iOS Safari zooms the viewport whenever a focused input
41
+ computes below 16px, and this scale's base is 14px, so every surface that
42
+ sized a field from --text-sm or --text-base bought the zoom.
43
+
44
+ It has to be a TOKEN rather than a rule because a control's size is almost
45
+ always set inline (every component in components/forms does), and an inline
46
+ style outranks any stylesheet — including a media query. A var() resolves
47
+ per-device inside that inline style, so this is the only construction that
48
+ actually reaches the control. Fields ask for --text-control; nothing else
49
+ should. */
50
+ --text-control:var(--text-sm);
51
+
39
52
  --weight-normal:400;
40
53
  --weight-medium:500;
41
54
  --weight-semibold:600;
@@ -63,3 +76,11 @@
63
76
  --type-code:400 var(--text-sm)/var(--leading-relaxed) var(--font-mono);
64
77
  --type-eyebrow:600 0.625rem/1 var(--font-sans);
65
78
  }
79
+
80
+ /* On touch, a control goes to 16px. Below that iOS Safari zooms the viewport on
81
+ focus and never zooms back out, which is the single most common way a mobile
82
+ form feels broken. pointer:coarse is the real signal — a desktop mouse keeps
83
+ the compact 13px field. */
84
+ @media (pointer:coarse){
85
+ :root{--text-control:1rem}
86
+ }