@hanzo/design 0.4.3 → 0.4.5

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.
@@ -3,18 +3,24 @@ import React from 'react'
3
3
  /** Native-select build of the @hanzo/ui Select — same chrome, no portal. */
4
4
  export function Select({ options = [], value, onChange, placeholder, style, children, ...rest }) {
5
5
  const [focus, setFocus] = React.useState(false)
6
+ const [hover, setHover] = React.useState(false)
6
7
  return (
7
8
  <span style={{position:'relative',display:'inline-flex',alignItems:'center',width:'100%'}}>
9
+ {/* Same chrome as Input and Textarea — radius, hover lift, focus halo. */}
8
10
  <select
9
11
  value={value}
10
12
  onChange={(e)=>onChange && onChange(e.target.value)}
11
13
  onFocus={()=>setFocus(true)} onBlur={()=>setFocus(false)}
14
+ onMouseEnter={()=>setHover(true)} onMouseLeave={()=>setHover(false)}
12
15
  style={{
13
16
  appearance:'none',width:'100%',height:36,padding:'0 32px 0 12px',
14
17
  fontFamily:'var(--font-sans)',fontSize:'var(--text-control)',color:'var(--foreground)',
15
- background:'var(--surface-2)',border:'1px solid ' + (focus ? 'var(--border-focus)' : 'var(--border-control)'),
16
- borderRadius:'var(--radius-sm)',cursor:'pointer',
17
- transition:'border-color var(--duration-fast) var(--ease-out)',
18
+ background: hover && !focus ? 'var(--surface-3)' : 'var(--surface-2)',
19
+ border:'1px solid ' + (focus ? 'var(--border-focus)' : 'var(--border-control)'),
20
+ borderRadius:'var(--radius-md)',cursor:'pointer',
21
+ outline:'none',
22
+ boxShadow: focus ? 'var(--ring-focus)' : 'none',
23
+ transition:'border-color var(--duration-fast) var(--ease-out), background-color var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast) var(--ease-out)',
18
24
  ...style,
19
25
  }}
20
26
  {...rest}
@@ -1,18 +1,27 @@
1
1
  import React from 'react'
2
2
 
3
+ // Same chrome as Input, to the pixel — radius, hover lift, focus halo. A
4
+ // textarea that rounds one step tighter than the field above it and lights up
5
+ // differently on focus is the kind of drift nobody reports and everybody feels.
3
6
  export function Textarea({ rows = 4, style, ...rest }) {
4
7
  const [focus, setFocus] = React.useState(false)
8
+ const [hover, setHover] = React.useState(false)
5
9
  return (
6
10
  <textarea
7
11
  rows={rows}
8
12
  onFocus={(e)=>{setFocus(true);rest.onFocus&&rest.onFocus(e)}}
9
13
  onBlur={(e)=>{setFocus(false);rest.onBlur&&rest.onBlur(e)}}
14
+ onMouseEnter={()=>setHover(true)}
15
+ onMouseLeave={()=>setHover(false)}
10
16
  style={{
11
17
  width:'100%',padding:'8px 12px',resize:'vertical',
12
18
  fontFamily:'var(--font-sans)',fontSize:'var(--text-control)',lineHeight:'var(--leading-relaxed)',color:'var(--foreground)',
13
- background:'var(--surface-2)',border:'1px solid ' + (focus ? 'var(--border-focus)' : 'var(--border-control)'),
14
- borderRadius:'var(--radius-sm)',
15
- transition:'border-color var(--duration-fast) var(--ease-out)',
19
+ background: hover && !focus ? 'var(--surface-3)' : 'var(--surface-2)',
20
+ border:'1px solid ' + (focus ? 'var(--border-focus)' : 'var(--border-control)'),
21
+ borderRadius:'var(--radius-md)',
22
+ outline:'none',
23
+ boxShadow: focus ? 'var(--ring-focus)' : 'none',
24
+ transition:'border-color var(--duration-fast) var(--ease-out), background-color var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast) var(--ease-out)',
16
25
  ...style,
17
26
  }}
18
27
  {...rest}
package/docs/integrate.md CHANGED
@@ -22,7 +22,7 @@ every token below is a live CSS custom property.
22
22
  ```css
23
23
  .panel{
24
24
  background: var(--surface-card);
25
- border: 1px solid var(--border-card);
25
+ border: 1px solid var(--border);
26
26
  border-radius: var(--radius-lg);
27
27
  padding: var(--space-6);
28
28
  color: var(--text-secondary);
@@ -1,7 +1,7 @@
1
- <!-- @dsCard group="Colors" viewport="700x150" name="Borders" subtitle="One alpha ladder, graded by duty — only the focus ring owes a ratio" -->
1
+ <!-- @dsCard group="Colors" viewport="820x160" name="Borders" subtitle="One alpha ladder, graded by duty — only the focus ring owes a ratio" -->
2
2
  <!doctype html><html class="dark"><head><meta charset="utf-8"><title>Borders</title><link rel="stylesheet" href="../styles.css">
3
3
  <style>body{margin:0;padding:16px 20px;background:#000;font-family:var(--font-sans);color:var(--foreground)}
4
- .g{display:flex;gap:10px;flex-wrap:wrap}.sw{flex:1;min-width:64px}.chip{height:56px;border-radius:8px;border:1px solid var(--white-10)}
4
+ .g{display:flex;gap:10px;flex-wrap:wrap}.sw{flex:1;min-width:110px}.chip{height:56px;border-radius:8px;border:1px solid var(--white-10)}
5
5
  .n{margin-top:6px;font-size:10px;font-weight:600;letter-spacing:.06em;color:var(--neutral-300);font-family:var(--font-mono)}
6
6
  .v{font-size:9px;color:var(--neutral-600);font-family:var(--font-mono)}
7
7
  .row{display:flex;align-items:baseline;gap:14px}.mono{font-family:var(--font-mono)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/design",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
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": "BSD-3-Clause",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "scripts": {
34
34
  "gen": "node scripts/gen-tokens.mjs",
35
- "test": "node scripts/check-tokens.mjs && node scripts/lint.mjs components guidelines",
35
+ "test": "node scripts/check-tokens.mjs && node scripts/lint.mjs .",
36
36
  "build": "pnpm gen && pnpm test && tsc -p tsconfig.json",
37
37
  "prepublishOnly": "pnpm build",
38
38
  "lint": "node scripts/lint.mjs"
package/scripts/lint.mjs CHANGED
@@ -26,7 +26,15 @@ const TOKENS = new Set()
26
26
  }
27
27
 
28
28
  // ── what we read ─────────────────────────────────────────────────────────
29
- const EXT = new Set(['.css', '.scss', '.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.html'])
29
+ const EXT = new Set(['.css', '.scss', '.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.html', '.md'])
30
+ // Markdown is PROSE, and prose legitimately says `var(--x)` while explaining
31
+ // what a var is. Only its FENCED CSS BLOCKS are code, and only rule 1 runs on
32
+ // them. That is not a technicality — `docs/integrate.md` shipped the
33
+ // copy-paste snippet every new consumer starts from, and its `.panel` rule
34
+ // drew `1px solid var(--border-card)`, a token nothing declares. Anyone who
35
+ // followed the integration guide got a white hairline on black, from the
36
+ // document whose job is to prevent exactly that.
37
+ const FENCED_CSS = /```(?:css|scss)\n([\s\S]*?)```/g
30
38
  const SKIP = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'out', 'coverage', 'vendor', '__pycache__'])
31
39
  // Third-party marks keep their own hex by design (DESIGN.md §2.4); so do the
32
40
  // token files themselves, which are where raw values are SUPPOSED to live, and
@@ -41,7 +49,13 @@ const SKIP = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'out', '
41
49
  // An undefined custom property makes the declaration invalid, `border-color`
42
50
  // falls back to `currentColor`, and the specimen cards for this system's own
43
51
  // border scale had been painting near-white hairlines on black.
44
- const EXEMPT = /(^|\/)(tokens|assets|logos|providers|ui_kits|guidelines)(\/|$)|\.card\.html$/
52
+ // The three GENERATED artifacts are on the list for the same reason `tokens/`
53
+ // is: each one is that directory, emitted. `src/tokens.gen.ts` carries every
54
+ // authored literal into TypeScript; `styles.css` and `tailwind.css` are the
55
+ // flattened bundles. Flagging any of them for raw colour is flagging them for
56
+ // working. They are still checked by rule 1 and by check-tokens.mjs, which is
57
+ // what actually governs them.
58
+ const EXEMPT = /(^|\/)(tokens|assets|logos|providers|ui_kits|guidelines)(\/|$)|\.card\.html$|(^|\/)(tokens\.gen\.ts|styles\.css|tailwind\.css)$/
45
59
 
46
60
  const walk = (p, out = []) => {
47
61
  const st = statSync(p)
@@ -95,6 +109,15 @@ function lintUnresolved(rel, src) {
95
109
  function lintFile(abs, root) {
96
110
  const rel = relative(root, abs).split(sep).join('/')
97
111
  const raw = readFileSync(abs, 'utf8')
112
+ if (extname(rel) === '.md') {
113
+ // Line numbers stay true: replace everything outside a css fence with
114
+ // blanks rather than extracting the fences, so a reported line is the line.
115
+ let masked = raw.replace(/[^\n]/g, ' ')
116
+ for (const m of raw.matchAll(FENCED_CSS))
117
+ masked = masked.slice(0, m.index) + m[0] + masked.slice(m.index + m[0].length)
118
+ lintUnresolved(rel, strip(masked))
119
+ return
120
+ }
98
121
  const src = strip(raw)
99
122
  lintUnresolved(rel, src)
100
123
  if (EXEMPT.test('/' + rel)) return
package/styles.css CHANGED
@@ -818,7 +818,9 @@
818
818
  UA's border, which puts two typefaces and a foreign blue focus ring inside
819
819
  Hanzo cards on every surface that has not styled its own fields yet. The
820
820
  rules below are what a control looks like here. --border-control, NOT
821
- --border: the boundary IS the affordance, so it is the rung that owes 3:1. */
821
+ --border: a field's edge is its own duty and its own rung, so a surface can
822
+ retune fields without touching every divider on the page. It owes no
823
+ contrast ratio — the whole budget goes to --ring, below. */
822
824
  :where(input,select,textarea,button){font:inherit;color:inherit}
823
825
  :where(input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=color]),select,textarea){
824
826
  background:var(--surface-2);
@@ -896,7 +898,7 @@
896
898
  every scrolling surface in the product. 6px, no track, thumb on the same
897
899
  hairline ladder as everything else. Both syntaxes ship because they are
898
900
  disjoint — `scrollbar-*` is Firefox and the `::-webkit-*` pseudos are
899
- Chromium/Safari; neither engine reads the other. */
901
+ Chromium/Safari; neither engine reads the other.
900
902
  The thumb is a --white-* rung, so it does not invert; the `.light`
901
903
  restatements sit here rather than in colors.css because a scrollbar is an
902
904
  element default and this file is where element defaults live. Everything
package/tailwind.css CHANGED
@@ -841,7 +841,9 @@
841
841
  UA's border, which puts two typefaces and a foreign blue focus ring inside
842
842
  Hanzo cards on every surface that has not styled its own fields yet. The
843
843
  rules below are what a control looks like here. --border-control, NOT
844
- --border: the boundary IS the affordance, so it is the rung that owes 3:1. */
844
+ --border: a field's edge is its own duty and its own rung, so a surface can
845
+ retune fields without touching every divider on the page. It owes no
846
+ contrast ratio — the whole budget goes to --ring, below. */
845
847
  :where(input,select,textarea,button){font:inherit;color:inherit}
846
848
  :where(input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=color]),select,textarea){
847
849
  background:var(--surface-2);
@@ -919,7 +921,7 @@
919
921
  every scrolling surface in the product. 6px, no track, thumb on the same
920
922
  hairline ladder as everything else. Both syntaxes ship because they are
921
923
  disjoint — `scrollbar-*` is Firefox and the `::-webkit-*` pseudos are
922
- Chromium/Safari; neither engine reads the other. */
924
+ Chromium/Safari; neither engine reads the other.
923
925
  The thumb is a --white-* rung, so it does not invert; the `.light`
924
926
  restatements sit here rather than in colors.css because a scrollbar is an
925
927
  element default and this file is where element defaults live. Everything
package/tokens/base.css CHANGED
@@ -83,7 +83,9 @@
83
83
  UA's border, which puts two typefaces and a foreign blue focus ring inside
84
84
  Hanzo cards on every surface that has not styled its own fields yet. The
85
85
  rules below are what a control looks like here. --border-control, NOT
86
- --border: the boundary IS the affordance, so it is the rung that owes 3:1. */
86
+ --border: a field's edge is its own duty and its own rung, so a surface can
87
+ retune fields without touching every divider on the page. It owes no
88
+ contrast ratio — the whole budget goes to --ring, below. */
87
89
  :where(input,select,textarea,button){font:inherit;color:inherit}
88
90
  :where(input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=color]),select,textarea){
89
91
  background:var(--surface-2);
@@ -161,7 +163,7 @@
161
163
  every scrolling surface in the product. 6px, no track, thumb on the same
162
164
  hairline ladder as everything else. Both syntaxes ship because they are
163
165
  disjoint — `scrollbar-*` is Firefox and the `::-webkit-*` pseudos are
164
- Chromium/Safari; neither engine reads the other. */
166
+ Chromium/Safari; neither engine reads the other.
165
167
  The thumb is a --white-* rung, so it does not invert; the `.light`
166
168
  restatements sit here rather than in colors.css because a scrollbar is an
167
169
  element default and this file is where element defaults live. Everything