@hanzo/design 0.4.0 → 0.4.2

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.
@@ -2,7 +2,7 @@ import React from 'react'
2
2
 
3
3
  export function Avatar({ size = 32, style, children, ...rest }) {
4
4
  return (
5
- <span style={{position:'relative',display:'inline-flex',alignItems:'center',justifyContent:'center',width:size,height:size,flexShrink:0,borderRadius:'var(--radius-full)',overflow:'hidden',border:'1px solid var(--border-hairline)',background:'linear-gradient(to bottom right,var(--neutral-700),var(--neutral-900))',...style}} {...rest}>
5
+ <span style={{position:'relative',display:'inline-flex',alignItems:'center',justifyContent:'center',width:size,height:size,flexShrink:0,borderRadius:'var(--radius-full)',overflow:'hidden',border:'1px solid var(--border)',background:'linear-gradient(to bottom right,var(--muted-foreground),var(--surface-3))',...style}} {...rest}>
6
6
  {children}
7
7
  </span>
8
8
  )
@@ -1,13 +1,16 @@
1
1
  import React from 'react'
2
2
 
3
- /** Rank is carried by opacity, never by hue (DESIGN.md §2.3). */
3
+ /** Rank is carried by lift, never by hue (DESIGN.md §2.3).
4
+ The rungs come off the SURFACE ladder and --glass, not off --white-*: the
5
+ opacity ladder is white-on-white in the light theme, so a tier2 pill drawn
6
+ from --white-15 was an invisible chip the moment a surface went light. */
4
7
  const V = {
5
- default:{background:'var(--white-10)',color:'var(--text-primary)',border:'1px solid var(--border)'},
8
+ default:{background:'var(--glass-strong)',color:'var(--text-primary)',border:'1px solid var(--border)'},
6
9
  secondary:{background:'var(--secondary)',color:'var(--secondary-foreground)',border:'1px solid transparent'},
7
- outline:{background:'transparent',color:'var(--text-secondary)',border:'1px solid var(--border-card)'},
8
- tier1:{background:'var(--white-05)',color:'var(--text-tertiary)',border:'1px solid var(--border-card)'},
9
- tier2:{background:'var(--white-15)',color:'var(--text-secondary)',border:'1px solid var(--border-card)'},
10
- tier3:{background:'var(--white-20)',color:'var(--text-primary)',border:'1px solid var(--white-20)'},
10
+ outline:{background:'transparent',color:'var(--text-secondary)',border:'1px solid var(--border)'},
11
+ tier1:{background:'var(--glass)',color:'var(--text-tertiary)',border:'1px solid var(--border)'},
12
+ tier2:{background:'var(--surface-2)',color:'var(--text-secondary)',border:'1px solid var(--border)'},
13
+ tier3:{background:'var(--surface-3)',color:'var(--text-primary)',border:'1px solid var(--border-strong)'},
11
14
  error:{background:'var(--state-error-bg)',color:'var(--state-error-text)',border:'1px solid transparent'},
12
15
  }
13
16
 
@@ -31,7 +34,7 @@ export function Badge({ variant = 'default', pill = true, style, children, ...re
31
34
  /** The eyebrow pill above a hero headline. */
32
35
  export function EyebrowBadge({ children, style, ...rest }) {
33
36
  return (
34
- <span style={{display:'inline-flex',alignItems:'center',gap:8,padding:'8px 16px',borderRadius:'var(--radius-full)',border:'1px solid var(--border-hairline)',background:'var(--white-05)',fontSize:'var(--text-sm)',color:'var(--neutral-200)',...style}} {...rest}>{children}</span>
37
+ <span style={{display:'inline-flex',alignItems:'center',gap:8,padding:'8px 16px',borderRadius:'var(--radius-full)',border:'1px solid var(--border)',background:'var(--glass)',fontSize:'var(--text-sm)',color:'var(--text-secondary)',...style}} {...rest}>{children}</span>
35
38
  )
36
39
  }
37
40
 
@@ -3,7 +3,8 @@ import React from 'react'
3
3
  const V = {
4
4
  primary:{background:'var(--primary)',color:'var(--primary-foreground)',border:'1px solid transparent'},
5
5
  secondary:{background:'var(--secondary)',color:'var(--secondary-foreground)',border:'1px solid transparent'},
6
- outline:{background:'transparent',color:'var(--text-primary)',border:'1px solid var(--border)'},
6
+ // --border-control, not --border: an outline button's edge IS the button.
7
+ outline:{background:'transparent',color:'var(--text-primary)',border:'1px solid var(--border-control)'},
7
8
  ghost:{background:'transparent',color:'var(--text-secondary)',border:'1px solid transparent'},
8
9
  link:{background:'transparent',color:'var(--text-primary)',border:'1px solid transparent',textDecoration:'underline',textUnderlineOffset:4,padding:0,height:'auto'},
9
10
  destructive:{background:'var(--destructive)',color:'var(--destructive-foreground)',border:'1px solid transparent'},
@@ -14,18 +15,34 @@ const SZ = {
14
15
  lg:{height:44,padding:'0 24px',fontSize:'var(--text-sm)'},
15
16
  icon:{height:36,width:36,padding:0},
16
17
  }
17
- const HOVER = { primary:'primary', secondary:'secondary', outline:'accent', ghost:'accent', destructive:'destructive', link:'link' }
18
+ // Hover, per variant. A FILLED button does not brighten on hover — its fill is
19
+ // already the top of the ink ramp — so it steps DOWN that ramp and
20
+ // gains a bloom instead, which is the reference's own move
21
+ // (.primary-btn:hover → --accent-hover + a white glow). This used to be
22
+ // `opacity: 0.9`, which faded the button toward the page: the one thing a
23
+ // hovered control must never do is look more disabled than it did at rest.
24
+ // An UNFILLED button has room to gain, so it takes a surface and an edge.
25
+ const HOVER = {
26
+ primary: { background:'var(--primary-hover)', boxShadow:'var(--bloom)' },
27
+ secondary: { background:'var(--secondary-hover)', boxShadow:'var(--edge-highlight)' },
28
+ // An unfilled button's hover is the SURFACE arriving, not the edge moving:
29
+ // --border-control (.15) to --border-strong (.16) is a one-percent step and
30
+ // reads as nothing, so the fill has to carry it. --glass-strong, not --glass;
31
+ // at .05 the fill was invisible too and the variant had no hover at all.
32
+ outline: { background:'var(--glass-strong)', borderColor:'var(--border-strong)', color:'var(--text-primary)' },
33
+ ghost: { background:'var(--glass-strong)', color:'var(--text-primary)' },
34
+ destructive:{ background:'var(--destructive-hover)', boxShadow:'var(--bloom)' },
35
+ link: { textDecoration:'underline' },
36
+ }
37
+ // The filled variants catch the light along their top edge, the same way every
38
+ // raised surface in this system does.
39
+ const LIT = new Set(['primary', 'destructive'])
18
40
 
19
41
  export function Button({ variant = 'primary', size = 'default', pill = false, disabled, style, children, ...rest }) {
20
42
  const [hover, setHover] = React.useState(false)
21
43
  const v = V[variant] || V.primary
22
44
  const s = SZ[size] || SZ.default
23
- const h = HOVER[variant]
24
- const hoverStyle = !hover || disabled ? null
25
- : h === 'primary' || h === 'destructive' ? { opacity: 0.9 }
26
- : h === 'secondary' ? { background: 'var(--neutral-800)' }
27
- : h === 'accent' ? { background: 'var(--accent)', color: 'var(--text-primary)' }
28
- : { textDecoration: 'underline' }
45
+ const hoverStyle = !hover || disabled ? null : HOVER[variant]
29
46
  return (
30
47
  <button
31
48
  disabled={disabled}
@@ -34,10 +51,18 @@ export function Button({ variant = 'primary', size = 'default', pill = false, di
34
51
  style={{
35
52
  display:'inline-flex',alignItems:'center',justifyContent:'center',gap:8,whiteSpace:'nowrap',
36
53
  fontFamily:'var(--font-sans)',fontWeight:'var(--weight-medium)',lineHeight:1,
37
- borderRadius: pill ? 'var(--radius-full)' : 'var(--radius-sm)',
54
+ // --radius-md (8px), not --radius-sm (6px): on a 36px control that one
55
+ // step is the difference between boxy and considered. --radius-sm keeps
56
+ // its job on genuinely small parts (badges, chips, menu rows).
57
+ borderRadius: pill ? 'var(--radius-full)' : 'var(--radius-md)',
58
+ letterSpacing:'var(--tracking-tight)',
38
59
  cursor: disabled ? 'not-allowed' : 'pointer',
39
60
  opacity: disabled ? 0.5 : 1,
40
- transition:'background-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out), opacity var(--duration-fast) var(--ease-out)',
61
+ boxShadow: LIT.has(variant) ? 'var(--edge-highlight)' : 'none',
62
+ // `transform` is in the list because tokens/base.css scales this button
63
+ // to .98 while it is held down, and a transform with no transition
64
+ // snaps rather than presses.
65
+ transition:'background-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast) var(--ease-out), opacity var(--duration-fast) var(--ease-out), transform var(--duration-press) var(--ease-out)',
41
66
  ...v, ...s, ...hoverStyle, ...style,
42
67
  }}
43
68
  {...rest}
@@ -7,8 +7,9 @@ const FILL = {
7
7
  plain:'transparent',
8
8
  }
9
9
 
10
- export function Card({ variant = 'default', sheen = false, interactive = false, style, children, ...rest }) {
10
+ export function Card({ variant = 'default', sheen = false, interactive = false, selected = false, style, children, ...rest }) {
11
11
  const [hover, setHover] = React.useState(false)
12
+ const edge = selected ? 'var(--border-selected)' : hover ? 'var(--border-strong)' : 'var(--border)'
12
13
  return (
13
14
  <div
14
15
  onMouseEnter={interactive ? () => setHover(true) : undefined}
@@ -16,13 +17,24 @@ export function Card({ variant = 'default', sheen = false, interactive = false,
16
17
  style={{
17
18
  position:'relative',overflow:'hidden',
18
19
  borderRadius:'var(--radius-lg)',
19
- border:'1px solid ' + (hover ? 'var(--border-strong)' : 'var(--border-hairline)'),
20
+ border:'1px solid ' + edge,
20
21
  background:FILL[variant] || FILL.default,
21
- transition:'border-color var(--duration-fast) var(--ease-out)',
22
+ // The card catches the light along its top edge at rest, and picks up a
23
+ // real drop only when it lifts. A shadow on a resting card is noise; a
24
+ // card with NO inset highlight is a coloured rectangle rather than a
25
+ // surface, which is why the highlight is unconditional and the drop is
26
+ // not. See tokens/elevation.css for why this is the part that matters
27
+ // on a near-black ground.
28
+ boxShadow: hover ? 'var(--edge-highlight), var(--shadow-lg)' : 'var(--edge-highlight)',
29
+ transform: hover ? 'translateY(-1px)' : 'none',
30
+ transition:'border-color var(--duration-fast) var(--ease-out), box-shadow var(--duration-base) var(--ease-out), transform var(--duration-base) var(--ease-out)',
22
31
  ...style,
23
32
  }}
24
33
  {...rest}
25
34
  >
35
+ {/* The top hairline: brightest at the centre, dissolved before either
36
+ corner, so the edge never terminates in a hard stop. */}
37
+ <div aria-hidden style={{position:'absolute',top:0,left:0,right:0,height:1,pointerEvents:'none',background:'var(--sheen-edge)'}} />
26
38
  {sheen && <div aria-hidden style={{position:'absolute',inset:0,pointerEvents:'none',background:'var(--sheen-card)',opacity:0.7}} />}
27
39
  {children}
28
40
  </div>
@@ -33,10 +45,13 @@ export function CardHeader({ style, children, ...rest }) {
33
45
  return <div style={{position:'relative',padding:'24px 24px 0',display:'flex',flexDirection:'column',gap:6,...style}} {...rest}>{children}</div>
34
46
  }
35
47
  export function CardTitle({ style, children, ...rest }) {
36
- return <h3 style={{fontSize:'var(--text-base)',fontWeight:'var(--weight-semibold)',color:'var(--text-primary)',...style}} {...rest}>{children}</h3>
48
+ // --text-lg over --text-base: at 14px a title sat one point above its own
49
+ // 13px description, which is not a hierarchy, it is a rounding error. The
50
+ // step up plus the tighter tracking is what makes a card scannable.
51
+ return <h3 style={{fontSize:'var(--text-lg)',fontWeight:'var(--weight-semibold)',letterSpacing:'var(--tracking-tight)',color:'var(--text-primary)',...style}} {...rest}>{children}</h3>
37
52
  }
38
53
  export function CardDescription({ style, children, ...rest }) {
39
- return <p style={{fontSize:'var(--text-sm)',lineHeight:'var(--leading-relaxed)',color:'var(--text-helper)',...style}} {...rest}>{children}</p>
54
+ return <p style={{fontSize:'var(--text-sm)',lineHeight:'var(--leading-relaxed)',color:'var(--text-tertiary)',...style}} {...rest}>{children}</p>
40
55
  }
41
56
  export function CardContent({ style, children, ...rest }) {
42
57
  return <div style={{position:'relative',padding:24,...style}} {...rest}>{children}</div>
@@ -4,7 +4,7 @@ export function Progress({ value = 0, max = 100, height = 6, style, ...rest }) {
4
4
  const pct = Math.max(0, Math.min(100, (value / max) * 100))
5
5
  return (
6
6
  <div role="progressbar" aria-valuenow={value} aria-valuemin={0} aria-valuemax={max}
7
- style={{position:'relative',width:'100%',height,borderRadius:'var(--radius-full)',background:'var(--neutral-800)',overflow:'hidden',...style}} {...rest}>
7
+ style={{position:'relative',width:'100%',height,borderRadius:'var(--radius-full)',background:'var(--glass-strong)',overflow:'hidden',...style}} {...rest}>
8
8
  <div style={{height:'100%',width:pct+'%',background:'var(--primary)',borderRadius:'var(--radius-full)',transition:'width var(--duration-base) var(--ease-out)'}} />
9
9
  </div>
10
10
  )
@@ -4,7 +4,7 @@ export function Table({ style, children, ...rest }) {
4
4
  return <table style={{width:'100%',borderCollapse:'collapse',fontSize:'var(--text-sm)',...style}} {...rest}>{children}</table>
5
5
  }
6
6
  export function TableHeader({ style, children, ...rest }) {
7
- return <thead style={{background:'var(--black)',...style}} {...rest}>{children}</thead>
7
+ return <thead style={{background:'var(--surface-2)',...style}} {...rest}>{children}</thead>
8
8
  }
9
9
  export function TableBody({ style, children, ...rest }) {
10
10
  return <tbody style={style} {...rest}>{children}</tbody>
@@ -15,7 +15,7 @@ export function TableRow({ interactive = false, style, children, ...rest }) {
15
15
  <tr
16
16
  onMouseEnter={interactive ? () => setHover(true) : undefined}
17
17
  onMouseLeave={interactive ? () => setHover(false) : undefined}
18
- style={{borderTop:'1px solid var(--border-hairline)',background: hover ? 'rgb(23 23 23 / .6)' : 'transparent',cursor: interactive ? 'pointer' : 'default',transition:'background-color var(--duration-fast) var(--ease-out)',...style}}
18
+ style={{borderTop:'1px solid var(--border)',background: hover ? 'var(--glass)' : 'transparent',cursor: interactive ? 'pointer' : 'default',transition:'background-color var(--duration-fast) var(--ease-out)',...style}}
19
19
  {...rest}
20
20
  >
21
21
  {children}
@@ -15,7 +15,7 @@ export function Checkbox({ checked, defaultChecked, onChange, disabled, label, s
15
15
  onKeyDown={(e)=>{if(e.key===' '||e.key==='Enter'){e.preventDefault();toggle()}}}
16
16
  style={{
17
17
  display:'inline-flex',alignItems:'center',justifyContent:'center',width:16,height:16,flexShrink:0,
18
- borderRadius:4,border:'1px solid ' + (on ? 'var(--primary)' : 'var(--border-strong)'),
18
+ borderRadius:4,border:'1px solid ' + (on ? 'var(--primary)' : 'var(--border-control)'),
19
19
  background: on ? 'var(--primary)' : 'transparent',
20
20
  cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.5 : 1,
21
21
  transition:'background-color var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out)',
@@ -2,17 +2,27 @@ import React from 'react'
2
2
 
3
3
  export function Input({ invalid = false, style, ...rest }) {
4
4
  const [focus, setFocus] = React.useState(false)
5
+ const [hover, setHover] = React.useState(false)
5
6
  return (
6
7
  <input
7
8
  onFocus={(e)=>{setFocus(true);rest.onFocus&&rest.onFocus(e)}}
8
9
  onBlur={(e)=>{setFocus(false);rest.onBlur&&rest.onBlur(e)}}
10
+ onMouseEnter={()=>setHover(true)}
11
+ onMouseLeave={()=>setHover(false)}
9
12
  style={{
10
13
  width:'100%',height:36,padding:'0 12px',
11
- fontFamily:'var(--font-sans)',fontSize:'var(--text-sm)',color:'var(--foreground)',
12
- background:'var(--black)',
13
- border:'1px solid ' + (invalid ? 'var(--state-error)' : focus ? 'var(--ring)' : 'var(--border)'),
14
- borderRadius:'var(--radius-sm)',outline:'none',
15
- transition:'border-color var(--duration-fast) var(--ease-out)',
14
+ fontFamily:'var(--font-sans)',fontSize:'var(--text-control)',color:'var(--foreground)',
15
+ // A hovered field lifts its SURFACE. Moving its edge from .15 to .16
16
+ // is a state you cannot see, and a state you cannot see is dead code.
17
+ background: hover && !focus ? 'var(--surface-3)' : 'var(--surface-2)',
18
+ border:'1px solid ' + (invalid ? 'var(--state-error)' : focus ? 'var(--border-focus)' : 'var(--border-control)'),
19
+ borderRadius:'var(--radius-md)',
20
+ outline:'none',
21
+ // Focus is the edge brightening plus a soft halo just outside it — the
22
+ // reference's .composer-box:focus-within, and the same recipe
23
+ // tokens/base.css gives a bare <input>. Not a second hard ring.
24
+ boxShadow: focus ? 'var(--ring-focus)' : 'none',
25
+ transition:'border-color var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast) var(--ease-out)',
16
26
  ...style,
17
27
  }}
18
28
  {...rest}
@@ -11,9 +11,9 @@ export function Select({ options = [], value, onChange, placeholder, style, chil
11
11
  onFocus={()=>setFocus(true)} onBlur={()=>setFocus(false)}
12
12
  style={{
13
13
  appearance:'none',width:'100%',height:36,padding:'0 32px 0 12px',
14
- fontFamily:'var(--font-sans)',fontSize:'var(--text-sm)',color:'var(--foreground)',
15
- background:'var(--black)',border:'1px solid ' + (focus ? 'var(--ring)' : 'var(--border)'),
16
- borderRadius:'var(--radius-sm)',outline:'none',cursor:'pointer',
14
+ 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
17
  transition:'border-color var(--duration-fast) var(--ease-out)',
18
18
  ...style,
19
19
  }}
@@ -10,7 +10,7 @@ export function Slider({ min = 0, max = 100, step = 1, value, defaultValue = 0,
10
10
  onChange={(e) => { const n = Number(e.target.value); if (value === undefined) setInner(n); onChange && onChange(n) }}
11
11
  style={{
12
12
  appearance:'none',WebkitAppearance:'none',width:'100%',height:16,background:'transparent',cursor:'pointer',
13
- backgroundImage:'linear-gradient(var(--primary),var(--primary)),linear-gradient(var(--neutral-800),var(--neutral-800))',
13
+ backgroundImage:'linear-gradient(var(--primary),var(--primary)),linear-gradient(var(--glass-strong),var(--glass-strong))',
14
14
  backgroundSize: pct + '% 4px, 100% 4px',
15
15
  backgroundPosition:'left center, left center',
16
16
  backgroundRepeat:'no-repeat',
@@ -9,9 +9,9 @@ export function Textarea({ rows = 4, style, ...rest }) {
9
9
  onBlur={(e)=>{setFocus(false);rest.onBlur&&rest.onBlur(e)}}
10
10
  style={{
11
11
  width:'100%',padding:'8px 12px',resize:'vertical',
12
- fontFamily:'var(--font-sans)',fontSize:'var(--text-sm)',lineHeight:'var(--leading-relaxed)',color:'var(--foreground)',
13
- background:'var(--black)',border:'1px solid ' + (focus ? 'var(--ring)' : 'var(--border)'),
14
- borderRadius:'var(--radius-sm)',outline:'none',
12
+ 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
15
  transition:'border-color var(--duration-fast) var(--ease-out)',
16
16
  ...style,
17
17
  }}
@@ -11,7 +11,7 @@ export function Accordion({ type = 'single', defaultValue = null, style, childre
11
11
  const isOpen = (v) => (type === 'multiple' ? open.includes(v) : open === v)
12
12
  return (
13
13
  <Ctx.Provider value={{ isOpen, toggle }}>
14
- <div style={{borderTop:'1px solid var(--border-hairline)',...style}} {...rest}>{children}</div>
14
+ <div style={{borderTop:'1px solid var(--border)',...style}} {...rest}>{children}</div>
15
15
  </Ctx.Provider>
16
16
  )
17
17
  }
@@ -20,7 +20,7 @@ export function AccordionItem({ value, title, style, children, ...rest }) {
20
20
  const { isOpen, toggle } = React.useContext(Ctx)
21
21
  const on = isOpen(value)
22
22
  return (
23
- <div style={{borderBottom:'1px solid var(--border-hairline)',...style}} {...rest}>
23
+ <div style={{borderBottom:'1px solid var(--border)',...style}} {...rest}>
24
24
  <button
25
25
  onClick={() => toggle(value)} aria-expanded={on}
26
26
  style={{display:'flex',width:'100%',alignItems:'center',justifyContent:'space-between',gap:16,padding:'18px 0',background:'transparent',border:'none',cursor:'pointer',textAlign:'left',fontFamily:'var(--font-sans)',fontSize:'var(--text-base)',fontWeight:'var(--weight-medium)',color:'var(--text-primary)'}}
@@ -15,24 +15,35 @@ export function Tabs({ defaultValue, value, onValueChange, style, children, ...r
15
15
 
16
16
  export function TabsList({ style, children, ...rest }) {
17
17
  return (
18
- <div role="tablist" style={{display:'inline-flex',alignItems:'center',gap:4,padding:4,borderRadius:'var(--radius-sm)',background:'var(--muted)',border:'1px solid var(--border)',...style}} {...rest}>{children}</div>
18
+ // alignSelf: a segmented control is as wide as its segments. Without it the
19
+ // list is a flex ITEM of the Tabs column and stretches to the full row,
20
+ // which turns a compact control into a full-width bar.
21
+ <div role="tablist" style={{display:'inline-flex',alignSelf:'flex-start',maxWidth:'100%',overflowX:'auto',alignItems:'center',gap:2,padding:3,borderRadius:'var(--radius-lg)',background:'var(--surface-2)',border:'1px solid var(--border)',...style}} {...rest}>{children}</div>
19
22
  )
20
23
  }
21
24
 
22
25
  export function TabsTrigger({ value, style, children, ...rest }) {
23
26
  const { value: active, setValue } = React.useContext(Ctx)
27
+ const [hover, setHover] = React.useState(false)
24
28
  const on = active === value
25
29
  return (
26
30
  <button
27
31
  role="tab" aria-selected={on} onClick={() => setValue(value)}
32
+ onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
28
33
  style={{
29
- display:'inline-flex',alignItems:'center',gap:8,height:30,padding:'0 12px',
34
+ display:'inline-flex',alignItems:'center',gap:8,height:30,padding:'0 14px',
30
35
  fontFamily:'var(--font-sans)',fontSize:'var(--text-sm)',fontWeight:'var(--weight-medium)',
31
- color: on ? 'var(--text-primary)' : 'var(--text-helper)',
32
- background: on ? 'var(--secondary)' : 'transparent',
33
- border:'1px solid ' + (on ? 'var(--border)' : 'transparent'),
34
- borderRadius:'calc(var(--radius-sm) - 2px)',cursor:'pointer',whiteSpace:'nowrap',
35
- transition:'color var(--duration-fast) var(--ease-out), background-color var(--duration-fast) var(--ease-out)',
36
+ letterSpacing:'var(--tracking-tight)',
37
+ color: on ? 'var(--text-primary)' : hover ? 'var(--text-secondary)' : 'var(--text-helper)',
38
+ // The selected tab is a surface that has RISEN out of the track: it
39
+ // lifts a rung, catches the light on its top edge and casts a small
40
+ // drop. A selected tab that only changes colour reads as a link that
41
+ // happens to be a different grey.
42
+ background: on ? 'var(--surface-3)' : 'transparent',
43
+ border:'1px solid ' + (on ? 'var(--border-strong)' : 'transparent'),
44
+ boxShadow: on ? 'var(--edge-highlight), var(--shadow-sm)' : 'none',
45
+ borderRadius:'calc(var(--radius-lg) - 3px)',cursor:'pointer',whiteSpace:'nowrap',
46
+ transition:'color var(--duration-fast) var(--ease-out), background-color var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast) var(--ease-out), transform var(--duration-press) var(--ease-out)',
36
47
  ...style,
37
48
  }}
38
49
  {...rest}
@@ -5,7 +5,7 @@
5
5
  <script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
6
6
  <script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
7
7
  <script src="../../_ds_bundle.js"></script>
8
- <style>body{margin:0;padding:20px;background:#000;font-family:var(--font-sans)}pre{margin:0;padding:16px;background:var(--card);border:1px solid var(--border-hairline);border-radius:var(--radius-lg);font-size:12px;line-height:1.7;color:var(--neutral-300);overflow:auto}</style></head>
8
+ <style>body{margin:0;padding:20px;background:#000;font-family:var(--font-sans)}pre{margin:0;padding:16px;background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);font-size:12px;line-height:1.7;color:var(--neutral-300);overflow:auto}</style></head>
9
9
  <body><div id="root"></div>
10
10
  <script type="text/babel">
11
11
  const { Tabs, TabsList, TabsTrigger, TabsContent, Accordion, AccordionItem } = window.HanzoDesignSystem_0a9445;
@@ -11,7 +11,7 @@ export function Dialog({ open, onOpenChange, children }) {
11
11
  return (
12
12
  <div
13
13
  onClick={() => onOpenChange && onOpenChange(false)}
14
- style={{position:'fixed',inset:0,zIndex:'var(--z-modal)',display:'flex',alignItems:'center',justifyContent:'center',padding:24,background:'var(--surface-scrim)',backdropFilter:'blur(4px)',animation:'hanzo-fade-up var(--duration-fast) var(--ease-out)'}}
14
+ style={{position:'fixed',inset:0,zIndex:'var(--z-modal)',display:'flex',alignItems:'center',justifyContent:'center',padding:24,background:'var(--surface-scrim)',backdropFilter:'blur(8px) saturate(140%)',animation:'hanzo-fade-up var(--duration-fast) var(--ease-out)'}}
15
15
  >
16
16
  {children}
17
17
  </div>
@@ -22,7 +22,7 @@ export function DialogContent({ width = 480, style, children, ...rest }) {
22
22
  return (
23
23
  <div
24
24
  role="dialog" aria-modal="true" onClick={(e) => e.stopPropagation()}
25
- style={{width:'100%',maxWidth:width,maxHeight:'85vh',overflowY:'auto',padding:24,display:'flex',flexDirection:'column',gap:16,background:'var(--popover)',color:'var(--popover-foreground)',border:'1px solid var(--border-hairline)',borderRadius:'var(--radius-xl)',boxShadow:'var(--shadow-floating)',...style}}
25
+ style={{width:'100%',maxWidth:width,maxHeight:'85vh',overflowY:'auto',padding:24,display:'flex',flexDirection:'column',gap:16,background:'var(--popover)',color:'var(--popover-foreground)',border:'1px solid var(--border-strong)',borderRadius:'var(--radius-xl)',boxShadow:'var(--edge-highlight), var(--shadow-floating)',animation:'hanzo-zoom-in var(--duration-base) var(--ease-emphasis)',...style}}
26
26
  {...rest}
27
27
  >{children}</div>
28
28
  )
@@ -19,7 +19,7 @@ export function DropdownMenu({ align = 'right', hover = false, trigger, style, c
19
19
  <span onClick={() => setOpen((v) => !v)} style={{display:'inline-flex'}}>{trigger}</span>
20
20
  {open && (
21
21
  <div style={{position:'absolute',top:'100%',[align]:0,paddingTop:12,zIndex:'var(--z-dropdown)',minWidth:208}}>
22
- <div style={{display:'flex',flexDirection:'column',padding:8,background:'var(--surface-overlay)',border:'1px solid var(--border-hairline)',borderRadius:'var(--radius-xl)',boxShadow:'var(--shadow-floating)',backdropFilter:'blur(16px)',animation:'hanzo-fade-down var(--duration-fast) var(--ease-out)'}}>
22
+ <div style={{display:'flex',flexDirection:'column',padding:6,background:'var(--surface-overlay)',border:'1px solid var(--border-strong)',borderRadius:'var(--radius-lg)',boxShadow:'var(--edge-highlight), var(--shadow-floating)',backdropFilter:'blur(20px) saturate(180%)',animation:'hanzo-zoom-in var(--duration-fast) var(--ease-emphasis)'}}>
23
23
  {children}
24
24
  </div>
25
25
  </div>
@@ -33,17 +33,21 @@ export function DropdownMenuItem({ desc, style, children, ...rest }) {
33
33
  return (
34
34
  <a
35
35
  onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
36
- style={{display:'block',padding: desc ? '10px 12px' : '8px 12px',borderRadius:'var(--radius-sm)',background: hover ? 'var(--neutral-900)' : 'transparent',cursor:'pointer',textDecoration:'none',transition:'background-color var(--duration-fast) var(--ease-out)',...style}}
36
+ style={{display:'block',padding: desc ? '10px 12px' : '8px 12px',borderRadius:'var(--radius-sm)',background: hover ? 'var(--glass-strong)' : 'transparent',cursor:'pointer',textDecoration:'none',transition:'background-color var(--duration-fast) var(--ease-out)',...style}}
37
37
  {...rest}
38
38
  >
39
- <span style={{display:'block',fontSize:'var(--text-sm)',fontWeight:'var(--weight-medium)',color:'var(--neutral-100)'}}>{children}</span>
40
- {desc && <span style={{display:'block',marginTop:2,fontSize:'var(--text-xs)',color:'var(--neutral-500)'}}>{desc}</span>}
39
+ {/* --text-primary / --text-tertiary, not --neutral-100 / --neutral-500.
40
+ The neutral ladder is a PALETTE: it does not invert, so a menu item
41
+ painted from it rendered #f5f5f5 text on the light theme's #ffffff
42
+ popover — the item did not change colour, it stopped being legible. */}
43
+ <span style={{display:'block',fontSize:'var(--text-sm)',fontWeight:'var(--weight-medium)',color:'var(--text-primary)'}}>{children}</span>
44
+ {desc && <span style={{display:'block',marginTop:2,fontSize:'var(--text-xs)',lineHeight:'var(--leading-normal)',color:'var(--text-tertiary)'}}>{desc}</span>}
41
45
  </a>
42
46
  )
43
47
  }
44
48
  export function DropdownMenuLabel({ style, children, ...rest }) {
45
- return <span style={{padding:'6px 12px',fontSize:'var(--text-xs)',fontWeight:'var(--weight-medium)',textTransform:'uppercase',letterSpacing:'var(--tracking-wide)',color:'var(--neutral-500)',...style}} {...rest}>{children}</span>
49
+ return <span style={{padding:'6px 12px',fontSize:'var(--text-xs)',fontWeight:'var(--weight-semibold)',textTransform:'uppercase',letterSpacing:'var(--tracking-widest)',color:'var(--text-tertiary)',...style}} {...rest}>{children}</span>
46
50
  }
47
51
  export function DropdownMenuSeparator({ style }) {
48
- return <span style={{height:1,margin:'6px 4px',background:'var(--border-hairline)',...style}} />
52
+ return <span style={{height:1,margin:'6px 4px',background:'var(--border)',...style}} />
49
53
  }
@@ -9,8 +9,8 @@ export function Sheet({ open, onOpenChange, side = 'right', width = 380, childre
9
9
  }, [open, onOpenChange])
10
10
  if (!open) return null
11
11
  const pos = side === 'left'
12
- ? { left: 0, top: 0, bottom: 0, width, borderRight: '1px solid var(--border-hairline)' }
13
- : { right: 0, top: 0, bottom: 0, width, borderLeft: '1px solid var(--border-hairline)' }
12
+ ? { left: 0, top: 0, bottom: 0, width, borderRight: '1px solid var(--border)' }
13
+ : { right: 0, top: 0, bottom: 0, width, borderLeft: '1px solid var(--border)' }
14
14
  return (
15
15
  <div onClick={() => onOpenChange && onOpenChange(false)} style={{position:'fixed',inset:0,zIndex:'var(--z-modal)',background:'var(--surface-scrim)'}}>
16
16
  <div
@@ -21,7 +21,7 @@ export function Sheet({ open, onOpenChange, side = 'right', width = 380, childre
21
21
  )
22
22
  }
23
23
  export function SheetHeader({ style, children, ...rest }) {
24
- return <div style={{padding:'20px 24px',borderBottom:'1px solid var(--border-hairline)',display:'flex',flexDirection:'column',gap:4,...style}} {...rest}>{children}</div>
24
+ return <div style={{padding:'20px 24px',borderBottom:'1px solid var(--border)',display:'flex',flexDirection:'column',gap:4,...style}} {...rest}>{children}</div>
25
25
  }
26
26
  export function SheetTitle({ style, children, ...rest }) {
27
27
  return <h2 style={{fontSize:'var(--text-base)',fontWeight:'var(--weight-semibold)',color:'var(--text-primary)',...style}} {...rest}>{children}</h2>
@@ -33,7 +33,7 @@ export function Toaster({ duration = 3500, position = 'bottom-right' }) {
33
33
  {items.map((i) => (
34
34
  <div key={i.id} style={{
35
35
  minWidth:260,maxWidth:380,padding:'12px 14px',display:'flex',alignItems:'flex-start',gap:10,
36
- background:'var(--surface-overlay)',border:'1px solid ' + (i.tone === 'error' ? 'var(--state-error)' : 'var(--border-hairline)'),
36
+ background:'var(--surface-overlay)',border:'1px solid ' + (i.tone === 'error' ? 'var(--state-error)' : 'var(--border)'),
37
37
  borderRadius:'var(--radius-lg)',boxShadow:'var(--shadow-floating)',backdropFilter:'blur(16px)',
38
38
  fontSize:'var(--text-sm)',color:'var(--neutral-100)',animation:'hanzo-slide-up-fade var(--duration-base) var(--ease-out)',
39
39
  }}>