@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.
- package/components/core/Avatar.jsx +1 -1
- package/components/core/Badge.jsx +10 -7
- package/components/core/Button.jsx +35 -10
- package/components/core/Card.jsx +20 -5
- package/components/core/Progress.jsx +1 -1
- package/components/core/Table.jsx +2 -2
- package/components/forms/Checkbox.jsx +1 -1
- package/components/forms/Input.jsx +15 -5
- package/components/forms/Select.jsx +3 -3
- package/components/forms/Slider.jsx +1 -1
- package/components/forms/Textarea.jsx +3 -3
- package/components/navigation/Accordion.jsx +2 -2
- package/components/navigation/Tabs.jsx +18 -7
- package/components/navigation/navigation.card.html +1 -1
- package/components/overlays/Dialog.jsx +2 -2
- package/components/overlays/DropdownMenu.jsx +10 -6
- package/components/overlays/Sheet.jsx +3 -3
- package/components/overlays/Toaster.jsx +1 -1
- package/dist/tokens.gen.d.ts +126 -76
- package/dist/tokens.gen.d.ts.map +1 -1
- package/dist/tokens.gen.js +126 -76
- package/package.json +1 -1
- package/scripts/check-tokens.mjs +71 -4
- package/src/tokens.gen.ts +126 -76
- package/styles.css +486 -84
- package/tailwind.css +486 -84
- package/tokens/base.css +146 -5
- package/tokens/colors.css +181 -66
- package/tokens/elevation.css +76 -5
- package/tokens/motion.css +15 -0
- package/tokens/spacing.css +46 -7
- package/tokens/typography.css +21 -0
|
@@ -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
|
|
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
|
|
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(--
|
|
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
|
|
8
|
-
tier1:{background:'var(--
|
|
9
|
-
tier2:{background:'var(--
|
|
10
|
-
tier3:{background:'var(--
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
package/components/core/Card.jsx
CHANGED
|
@@ -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 ' +
|
|
20
|
+
border:'1px solid ' + edge,
|
|
20
21
|
background:FILL[variant] || FILL.default,
|
|
21
|
-
|
|
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
|
-
|
|
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-
|
|
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(--
|
|
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(--
|
|
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
|
|
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-
|
|
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-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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-
|
|
15
|
-
background:'var(--
|
|
16
|
-
borderRadius:'var(--radius-sm)',
|
|
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(--
|
|
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-
|
|
13
|
-
background:'var(--
|
|
14
|
-
borderRadius:'var(--radius-sm)',
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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(
|
|
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-
|
|
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:
|
|
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(--
|
|
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
|
-
|
|
40
|
-
|
|
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-
|
|
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
|
|
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
|
|
13
|
-
: { right: 0, top: 0, bottom: 0, width, borderLeft: '1px solid var(--border
|
|
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
|
|
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
|
|
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
|
}}>
|