@hanzo/design 0.4.6 → 0.4.8

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 CHANGED
@@ -1,28 +1,14 @@
1
- BSD 3-Clause License
1
+ Licensed under either of
2
2
 
3
- Copyright (c) 2026 Hanzo AI, Inc.
3
+ * Apache License, Version 2.0 (LICENSE-APACHE or
4
+ https://www.apache.org/licenses/LICENSE-2.0)
5
+ * MIT license (LICENSE-MIT or https://opensource.org/licenses/MIT)
4
6
 
5
- Redistribution and use in source and binary forms, with or without
6
- modification, are permitted provided that the following conditions are met:
7
+ at your option.
7
8
 
8
- 1. Redistributions of source code must retain the above copyright notice, this
9
- list of conditions and the following disclaimer.
9
+ Unless you explicitly state otherwise, any contribution intentionally
10
+ submitted for inclusion in the work by you, as defined in the Apache-2.0
11
+ license, shall be dual licensed as above, without any additional terms or
12
+ conditions.
10
13
 
11
- 2. Redistributions in binary form must reproduce the above copyright notice,
12
- this list of conditions and the following disclaimer in the documentation
13
- and/or other materials provided with the distribution.
14
-
15
- 3. Neither the name of the copyright holder nor the names of its
16
- contributors may be used to endorse or promote products derived from
17
- this software without specific prior written permission.
18
-
19
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
- CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
+ See HIP-0137 (hanzoai/hips) for the standard this follows.
package/README.md CHANGED
@@ -126,7 +126,7 @@ Tokens track `hanzo.ai` (`app/globals.css`, `tailwind.config.ts`, `DESIGN.md`) a
126
126
 
127
127
  ## License
128
128
 
129
- BSD-3-Clause. Brand marks (the Hanzo logo, partner and provider logos) are the property of their respective owners and are provided for identification.
129
+ MIT OR Apache-2.0, per [HIP-0137](https://github.com/hanzoai/hips/blob/main/HIPs/hip-0137-one-license.md). Brand marks (the Hanzo logo, partner and provider logos) are the property of their respective owners and are provided for identification.
130
130
 
131
131
  ## Using it in a Tailwind app
132
132
 
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@hanzo/design",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
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
- "license": "BSD-3-Clause",
6
+ "license": "MIT OR Apache-2.0",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "https://github.com/hanzoai/design.git"
@@ -70,4 +70,4 @@
70
70
  "bin": {
71
71
  "hanzo-design-lint": "./scripts/lint.mjs"
72
72
  }
73
- }
73
+ }
@@ -140,6 +140,32 @@ const pass = (msg) => console.log(` ok ${msg}`)
140
140
  : fail('base.css is UNLAYERED — its element rules outrank every utility an app writes')
141
141
  }
142
142
 
143
+ // ── 1d. a focused element gets exactly ONE indicator ─────────────────────
144
+ // The field rule and the generic ring rule both compute to (0,1,0) — :where()
145
+ // zeroes its contents and each side keeps one pseudo-class — so the cascade
146
+ // falls through to SOURCE ORDER inside @layer base. The generic rule was
147
+ // written later, so it overrode the `outline:none` that the field rule states
148
+ // expressly to prevent it, and every focused input on every consumer drew the
149
+ // 2px ring AND the edge+halo. Both rules read as correct in isolation; only
150
+ // their order was wrong, which is why nobody saw it in either file.
151
+ //
152
+ // Order is not testable as intent, so this tests the property instead: a rule
153
+ // that paints an outline on focus must not be able to land on a field.
154
+ {
155
+ const base = strip(read(join(tokensDir, 'base.css')))
156
+ const FIELDS = 'input,select,textarea'
157
+ // `outline:none` disarms; anything else paints. `outline-offset` is a
158
+ // different property and never matches — the colon must follow `outline`.
159
+ const paints = (body) => /(?:^|[;{\s])outline\s*:\s*(?!none\b)[^;}]+/.test(body)
160
+ const scoped = (sel) => sel.includes(`:where(${FIELDS})`) || sel.includes(`:not(${FIELDS})`)
161
+ const doubled = [...base.matchAll(/([^{}]+)\{([^{}]*)\}/g)]
162
+ .map(([, sel, body]) => ({ sel: sel.trim().replace(/\s+/g, ' '), body }))
163
+ .filter(({ sel, body }) => sel.includes(':focus-visible') && paints(body) && !scoped(sel))
164
+ doubled.length
165
+ ? doubled.forEach(({ sel }) => fail(`\`${sel}\` paints an outline on a field that already draws its own edge + halo — two focus indicators`))
166
+ : pass('one focus indicator per element: fields brighten their edge, everything else rings')
167
+ }
168
+
143
169
  // ── 2. every var() used inside the token layer must resolve ──────────────
144
170
  {
145
171
  const declared = new Set()
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: design-system
3
3
  description: "Use whenever you generate, edit, or review a Hanzo user interface — a page, component, screen, email, or any code change touching colour, type, spacing, elevation, motion, or stacking order. Use it when PLANNING UI work too, so the plan names tokens rather than values. Teaches the Hanzo token layer (@hanzo/design, derived from @hanzo/brand plus @hanzo/logo) and runs the linter that proves generated code actually reaches it. Triggers — build a page, add a component, style, theme, dark mode, colour, hex, palette, font size, spacing, padding, z-index, modal, dropdown, toast, button, card, dialog, icon, make it look Hanzo, brand, design review."
4
- license: BSD-3-Clause
4
+ license: MIT OR Apache-2.0
5
5
  ---
6
6
 
7
7
  # The Hanzo design system
package/styles.css CHANGED
@@ -229,23 +229,23 @@
229
229
  silent (the border simply stops existing) and has shipped before. */
230
230
  .light{
231
231
  color-scheme:light;
232
- --background:#ffffff;
232
+ --background:#f7f7f7;
233
233
  --foreground:#0a0a0a;
234
234
  /* A ladder, not four names for #f5f5f5. Light lifts by the same tiny steps
235
235
  dark does — ~2% per rung — so --surface-0..3 mean something in both themes. */
236
- --card:#fafafa;
236
+ --card:#f2f2f2;
237
237
  --card-foreground:#0a0a0a;
238
- --popover:#ffffff;
238
+ --popover:#fbfbfb;
239
239
  --popover-foreground:#0a0a0a;
240
240
  --primary:#0a0a0a;
241
241
  --primary-hover:#262626;
242
242
  --primary-foreground:#fafafa;
243
- --secondary:#ededed;
243
+ --secondary:#e4e4e4;
244
244
  --secondary-hover:#e0e0e0;
245
245
  --secondary-foreground:#0a0a0a;
246
- --muted:#f5f5f5;
246
+ --muted:#ededed;
247
247
  --muted-foreground:#525252;
248
- --accent:#ededed;
248
+ --accent:#e4e4e4;
249
249
  --accent-foreground:#0a0a0a;
250
250
  --destructive:var(--state-error);
251
251
  --destructive-hover:#dc2626;
@@ -270,9 +270,9 @@
270
270
  --selection:rgb(0 0 0 / .16);
271
271
  --glass:rgb(0 0 0 / .04);
272
272
  --glass-strong:rgb(0 0 0 / .07);
273
- --surface-card:#fafafa;
274
- --surface-card-emphasis:#ffffff;
275
- --surface-card-quiet:#fcfcfc;
273
+ --surface-card:#f2f2f2;
274
+ --surface-card-emphasis:#fdfdfd;
275
+ --surface-card-quiet:#f5f5f5;
276
276
  --surface-overlay:rgb(255 255 255 / .95);
277
277
  --surface-header:rgb(255 255 255 / .8);
278
278
  --surface-scrim:rgb(0 0 0 / .5);
@@ -838,8 +838,9 @@
838
838
  Not the generic ring. A field already HAS an edge, so focus brightens that
839
839
  edge (.15 -> .22) and adds a soft halo just outside it — which is what the
840
840
  reference does (.composer-box:focus-within) and what separates a focused
841
- field from a browser default. The generic `outline` is suppressed here
842
- precisely because it would draw a second, harder box around this one. */
841
+ field from a browser default. `outline:none` drops the UA's own focus ring,
842
+ which would otherwise draw a second, harder box around this one; the
843
+ generic ring below excludes fields for the same reason. */
843
844
  :where(input,select,textarea):focus-visible{
844
845
  outline:none;
845
846
  border-color:var(--border-focus);
@@ -886,7 +887,17 @@
886
887
  }
887
888
  }
888
889
 
889
- :focus-visible{outline:2px solid var(--ring);outline-offset:2px}
890
+ /* The generic ring — for everything that is NOT a field. The exclusion is
891
+ load-bearing, not tidiness. This rule and the control rule above both
892
+ compute to (0,1,0): :where() zeroes whatever it wraps, leaving one
893
+ pseudo-class on each side. Equal specificity inside one layer falls through
894
+ to source order, this rule sits 46 lines LATER, so it overrode the
895
+ `outline:none` written above to prevent precisely this — and every focused
896
+ input, select and textarea on every consumer drew BOTH the 2px ring and the
897
+ edge+halo. Two indicators, from the two rules that each say there is one.
898
+ Stating the exclusion in the selector fixes it by MEANING rather than by
899
+ position, so reordering either rule cannot bring it back. */
900
+ :where(:not(input,select,textarea)):focus-visible{outline:2px solid var(--ring);outline-offset:2px}
890
901
  /* --white-20 is white-on-white in the light theme, so selection reads through
891
902
  --selection, which BOTH themes define. */
892
903
  ::selection{background:var(--selection);color:var(--text-primary)}
package/tailwind.css CHANGED
@@ -252,23 +252,23 @@
252
252
  silent (the border simply stops existing) and has shipped before. */
253
253
  .light{
254
254
  color-scheme:light;
255
- --background:#ffffff;
255
+ --background:#f7f7f7;
256
256
  --foreground:#0a0a0a;
257
257
  /* A ladder, not four names for #f5f5f5. Light lifts by the same tiny steps
258
258
  dark does — ~2% per rung — so --surface-0..3 mean something in both themes. */
259
- --card:#fafafa;
259
+ --card:#f2f2f2;
260
260
  --card-foreground:#0a0a0a;
261
- --popover:#ffffff;
261
+ --popover:#fbfbfb;
262
262
  --popover-foreground:#0a0a0a;
263
263
  --primary:#0a0a0a;
264
264
  --primary-hover:#262626;
265
265
  --primary-foreground:#fafafa;
266
- --secondary:#ededed;
266
+ --secondary:#e4e4e4;
267
267
  --secondary-hover:#e0e0e0;
268
268
  --secondary-foreground:#0a0a0a;
269
- --muted:#f5f5f5;
269
+ --muted:#ededed;
270
270
  --muted-foreground:#525252;
271
- --accent:#ededed;
271
+ --accent:#e4e4e4;
272
272
  --accent-foreground:#0a0a0a;
273
273
  --destructive:var(--state-error);
274
274
  --destructive-hover:#dc2626;
@@ -293,9 +293,9 @@
293
293
  --selection:rgb(0 0 0 / .16);
294
294
  --glass:rgb(0 0 0 / .04);
295
295
  --glass-strong:rgb(0 0 0 / .07);
296
- --surface-card:#fafafa;
297
- --surface-card-emphasis:#ffffff;
298
- --surface-card-quiet:#fcfcfc;
296
+ --surface-card:#f2f2f2;
297
+ --surface-card-emphasis:#fdfdfd;
298
+ --surface-card-quiet:#f5f5f5;
299
299
  --surface-overlay:rgb(255 255 255 / .95);
300
300
  --surface-header:rgb(255 255 255 / .8);
301
301
  --surface-scrim:rgb(0 0 0 / .5);
@@ -861,8 +861,9 @@
861
861
  Not the generic ring. A field already HAS an edge, so focus brightens that
862
862
  edge (.15 -> .22) and adds a soft halo just outside it — which is what the
863
863
  reference does (.composer-box:focus-within) and what separates a focused
864
- field from a browser default. The generic `outline` is suppressed here
865
- precisely because it would draw a second, harder box around this one. */
864
+ field from a browser default. `outline:none` drops the UA's own focus ring,
865
+ which would otherwise draw a second, harder box around this one; the
866
+ generic ring below excludes fields for the same reason. */
866
867
  :where(input,select,textarea):focus-visible{
867
868
  outline:none;
868
869
  border-color:var(--border-focus);
@@ -909,7 +910,17 @@
909
910
  }
910
911
  }
911
912
 
912
- :focus-visible{outline:2px solid var(--ring);outline-offset:2px}
913
+ /* The generic ring — for everything that is NOT a field. The exclusion is
914
+ load-bearing, not tidiness. This rule and the control rule above both
915
+ compute to (0,1,0): :where() zeroes whatever it wraps, leaving one
916
+ pseudo-class on each side. Equal specificity inside one layer falls through
917
+ to source order, this rule sits 46 lines LATER, so it overrode the
918
+ `outline:none` written above to prevent precisely this — and every focused
919
+ input, select and textarea on every consumer drew BOTH the 2px ring and the
920
+ edge+halo. Two indicators, from the two rules that each say there is one.
921
+ Stating the exclusion in the selector fixes it by MEANING rather than by
922
+ position, so reordering either rule cannot bring it back. */
923
+ :where(:not(input,select,textarea)):focus-visible{outline:2px solid var(--ring);outline-offset:2px}
913
924
  /* --white-20 is white-on-white in the light theme, so selection reads through
914
925
  --selection, which BOTH themes define. */
915
926
  ::selection{background:var(--selection);color:var(--text-primary)}
package/tokens/base.css CHANGED
@@ -103,8 +103,9 @@
103
103
  Not the generic ring. A field already HAS an edge, so focus brightens that
104
104
  edge (.15 -> .22) and adds a soft halo just outside it — which is what the
105
105
  reference does (.composer-box:focus-within) and what separates a focused
106
- field from a browser default. The generic `outline` is suppressed here
107
- precisely because it would draw a second, harder box around this one. */
106
+ field from a browser default. `outline:none` drops the UA's own focus ring,
107
+ which would otherwise draw a second, harder box around this one; the
108
+ generic ring below excludes fields for the same reason. */
108
109
  :where(input,select,textarea):focus-visible{
109
110
  outline:none;
110
111
  border-color:var(--border-focus);
@@ -151,7 +152,17 @@
151
152
  }
152
153
  }
153
154
 
154
- :focus-visible{outline:2px solid var(--ring);outline-offset:2px}
155
+ /* The generic ring — for everything that is NOT a field. The exclusion is
156
+ load-bearing, not tidiness. This rule and the control rule above both
157
+ compute to (0,1,0): :where() zeroes whatever it wraps, leaving one
158
+ pseudo-class on each side. Equal specificity inside one layer falls through
159
+ to source order, this rule sits 46 lines LATER, so it overrode the
160
+ `outline:none` written above to prevent precisely this — and every focused
161
+ input, select and textarea on every consumer drew BOTH the 2px ring and the
162
+ edge+halo. Two indicators, from the two rules that each say there is one.
163
+ Stating the exclusion in the selector fixes it by MEANING rather than by
164
+ position, so reordering either rule cannot bring it back. */
165
+ :where(:not(input,select,textarea)):focus-visible{outline:2px solid var(--ring);outline-offset:2px}
155
166
  /* --white-20 is white-on-white in the light theme, so selection reads through
156
167
  --selection, which BOTH themes define. */
157
168
  ::selection{background:var(--selection);color:var(--text-primary)}
package/tokens/colors.css CHANGED
@@ -214,23 +214,23 @@
214
214
  silent (the border simply stops existing) and has shipped before. */
215
215
  .light{
216
216
  color-scheme:light;
217
- --background:#ffffff;
217
+ --background:#f7f7f7;
218
218
  --foreground:#0a0a0a;
219
219
  /* A ladder, not four names for #f5f5f5. Light lifts by the same tiny steps
220
220
  dark does — ~2% per rung — so --surface-0..3 mean something in both themes. */
221
- --card:#fafafa;
221
+ --card:#f2f2f2;
222
222
  --card-foreground:#0a0a0a;
223
- --popover:#ffffff;
223
+ --popover:#fbfbfb;
224
224
  --popover-foreground:#0a0a0a;
225
225
  --primary:#0a0a0a;
226
226
  --primary-hover:#262626;
227
227
  --primary-foreground:#fafafa;
228
- --secondary:#ededed;
228
+ --secondary:#e4e4e4;
229
229
  --secondary-hover:#e0e0e0;
230
230
  --secondary-foreground:#0a0a0a;
231
- --muted:#f5f5f5;
231
+ --muted:#ededed;
232
232
  --muted-foreground:#525252;
233
- --accent:#ededed;
233
+ --accent:#e4e4e4;
234
234
  --accent-foreground:#0a0a0a;
235
235
  --destructive:var(--state-error);
236
236
  --destructive-hover:#dc2626;
@@ -255,9 +255,9 @@
255
255
  --selection:rgb(0 0 0 / .16);
256
256
  --glass:rgb(0 0 0 / .04);
257
257
  --glass-strong:rgb(0 0 0 / .07);
258
- --surface-card:#fafafa;
259
- --surface-card-emphasis:#ffffff;
260
- --surface-card-quiet:#fcfcfc;
258
+ --surface-card:#f2f2f2;
259
+ --surface-card-emphasis:#fdfdfd;
260
+ --surface-card-quiet:#f5f5f5;
261
261
  --surface-overlay:rgb(255 255 255 / .95);
262
262
  --surface-header:rgb(255 255 255 / .8);
263
263
  --surface-scrim:rgb(0 0 0 / .5);