@hanzo/design 0.1.0
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 +21 -0
- package/README.md +57 -0
- package/assets/brand/hero-visualization.png +0 -0
- package/assets/brand/industry-cloud.png +0 -0
- package/assets/brand/industry-engineering.png +0 -0
- package/assets/brand/industry-infrastructure.png +0 -0
- package/assets/brand/og-image.png +0 -0
- package/assets/favicon.svg +8 -0
- package/assets/logo-dark.svg +9 -0
- package/assets/logo-light.svg +9 -0
- package/assets/logo-wordmark.png +0 -0
- package/assets/logo.svg +9 -0
- package/assets/logos/anthropic.svg +3 -0
- package/assets/logos/deepseek.svg +4 -0
- package/assets/logos/google.svg +6 -0
- package/assets/logos/groq.svg +3 -0
- package/assets/logos/huggingface.svg +3 -0
- package/assets/logos/mistral.svg +22 -0
- package/assets/logos/openai.svg +3 -0
- package/assets/logos/openrouter.svg +1 -0
- package/assets/logos/partners/aws.svg +12 -0
- package/assets/logos/partners/googlecloud.svg +3 -0
- package/assets/logos/partners/lux.svg +6 -0
- package/assets/logos/partners/nvidia.svg +7 -0
- package/assets/logos/partners/techstars.svg +1 -0
- package/assets/logos/partners/zoo-labs-foundation.svg +7 -0
- package/assets/logos/qwen.svg +1 -0
- package/assets/logos/replicate.svg +3 -0
- package/assets/logos/together.svg +6 -0
- package/assets/logos/xai.svg +3 -0
- package/assets/providers/claude.svg +3 -0
- package/assets/providers/codex.svg +3 -0
- package/assets/providers/copilot.svg +3 -0
- package/assets/providers/cursor.svg +3 -0
- package/assets/providers/gemini.svg +3 -0
- package/assets/providers/grok.svg +4 -0
- package/assets/providers/ollama.svg +7 -0
- package/components/core/Avatar.d.ts +5 -0
- package/components/core/Avatar.jsx +15 -0
- package/components/core/Avatar.prompt.md +8 -0
- package/components/core/Badge.d.ts +10 -0
- package/components/core/Badge.jsx +47 -0
- package/components/core/Badge.prompt.md +9 -0
- package/components/core/Button.d.ts +9 -0
- package/components/core/Button.jsx +48 -0
- package/components/core/Button.prompt.md +9 -0
- package/components/core/Card.d.ts +15 -0
- package/components/core/Card.jsx +46 -0
- package/components/core/Card.prompt.md +13 -0
- package/components/core/ChromeText.d.ts +10 -0
- package/components/core/ChromeText.jsx +29 -0
- package/components/core/ChromeText.prompt.md +8 -0
- package/components/core/HanzoLogo.d.ts +11 -0
- package/components/core/HanzoLogo.jsx +28 -0
- package/components/core/HanzoLogo.prompt.md +8 -0
- package/components/core/Icon.d.ts +10 -0
- package/components/core/Icon.jsx +43 -0
- package/components/core/Icon.prompt.md +9 -0
- package/components/core/Progress.d.ts +8 -0
- package/components/core/Progress.jsx +11 -0
- package/components/core/Progress.prompt.md +8 -0
- package/components/core/Table.d.ts +11 -0
- package/components/core/Table.jsx +30 -0
- package/components/core/Table.prompt.md +10 -0
- package/components/core/core.card.html +56 -0
- package/components/forms/Checkbox.d.ts +10 -0
- package/components/forms/Checkbox.jsx +33 -0
- package/components/forms/Checkbox.prompt.md +8 -0
- package/components/forms/Input.d.ts +3 -0
- package/components/forms/Input.jsx +21 -0
- package/components/forms/Input.prompt.md +8 -0
- package/components/forms/Label.d.ts +3 -0
- package/components/forms/Label.jsx +9 -0
- package/components/forms/Label.prompt.md +8 -0
- package/components/forms/Select.d.ts +9 -0
- package/components/forms/Select.jsx +33 -0
- package/components/forms/Select.prompt.md +7 -0
- package/components/forms/Slider.d.ts +7 -0
- package/components/forms/Slider.jsx +22 -0
- package/components/forms/Slider.prompt.md +7 -0
- package/components/forms/Textarea.d.ts +3 -0
- package/components/forms/Textarea.jsx +21 -0
- package/components/forms/Textarea.prompt.md +7 -0
- package/components/forms/forms.card.html +39 -0
- package/components/navigation/Accordion.d.ts +11 -0
- package/components/navigation/Accordion.jsx +37 -0
- package/components/navigation/Accordion.prompt.md +10 -0
- package/components/navigation/Tabs.d.ts +10 -0
- package/components/navigation/Tabs.jsx +47 -0
- package/components/navigation/Tabs.prompt.md +11 -0
- package/components/navigation/navigation.card.html +31 -0
- package/components/overlays/Dialog.d.ts +12 -0
- package/components/overlays/Dialog.jsx +41 -0
- package/components/overlays/Dialog.prompt.md +13 -0
- package/components/overlays/DropdownMenu.d.ts +13 -0
- package/components/overlays/DropdownMenu.jsx +49 -0
- package/components/overlays/DropdownMenu.prompt.md +11 -0
- package/components/overlays/Sheet.d.ts +12 -0
- package/components/overlays/Sheet.jsx +31 -0
- package/components/overlays/Sheet.prompt.md +10 -0
- package/components/overlays/Toaster.d.ts +12 -0
- package/components/overlays/Toaster.jsx +49 -0
- package/components/overlays/Toaster.prompt.md +9 -0
- package/components/overlays/overlays.card.html +44 -0
- package/content/README.md +13 -0
- package/content/taglines.md +46 -0
- package/content/voice.md +52 -0
- package/docs/README.md +44 -0
- package/docs/integrate.md +86 -0
- package/guidelines/brand-elevation.card.html +14 -0
- package/guidelines/brand-imagery.card.html +16 -0
- package/guidelines/brand-mark.card.html +21 -0
- package/guidelines/brand-motion.card.html +14 -0
- package/guidelines/brand-partner-logos.card.html +23 -0
- package/guidelines/brand-radii.card.html +17 -0
- package/guidelines/color-borders.card.html +16 -0
- package/guidelines/color-neutrals.card.html +24 -0
- package/guidelines/color-opacity-ladder.card.html +22 -0
- package/guidelines/color-semantic.card.html +17 -0
- package/guidelines/color-surfaces.card.html +17 -0
- package/guidelines/color-text-ranks.card.html +16 -0
- package/guidelines/iconography.card.html +30 -0
- package/guidelines/layout-containers.card.html +15 -0
- package/guidelines/spacing-golden.card.html +19 -0
- package/guidelines/spacing-scale.card.html +23 -0
- package/guidelines/type-body.card.html +14 -0
- package/guidelines/type-chrome.card.html +14 -0
- package/guidelines/type-display.card.html +14 -0
- package/guidelines/type-eyebrow.card.html +14 -0
- package/guidelines/type-mono.card.html +14 -0
- package/guidelines/type-scale.card.html +20 -0
- package/package.json +45 -0
- package/prompts/README.md +26 -0
- package/prompts/pages.md +53 -0
- package/prompts/rules.md +58 -0
- package/prompts/system.md +102 -0
- package/styles.css +11 -0
- package/tokens/base.css +12 -0
- package/tokens/colors.css +125 -0
- package/tokens/elevation.css +19 -0
- package/tokens/fonts.css +13 -0
- package/tokens/motion.css +24 -0
- package/tokens/radius.css +10 -0
- package/tokens/spacing.css +45 -0
- package/tokens/typography.css +46 -0
- package/tokens/z.css +14 -0
- package/ui_kits/hanzo_ai_site/SiteChrome.jsx +131 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
const FILL = {
|
|
4
|
+
default:'var(--surface-card)',
|
|
5
|
+
emphasis:'var(--surface-card-emphasis)',
|
|
6
|
+
quiet:'var(--surface-card-quiet)',
|
|
7
|
+
plain:'transparent',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Card({ variant = 'default', sheen = false, interactive = false, style, children, ...rest }) {
|
|
11
|
+
const [hover, setHover] = React.useState(false)
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
onMouseEnter={interactive ? () => setHover(true) : undefined}
|
|
15
|
+
onMouseLeave={interactive ? () => setHover(false) : undefined}
|
|
16
|
+
style={{
|
|
17
|
+
position:'relative',overflow:'hidden',
|
|
18
|
+
borderRadius:'var(--radius-lg)',
|
|
19
|
+
border:'1px solid ' + (hover ? 'var(--border-strong)' : 'var(--border-hairline)'),
|
|
20
|
+
background:FILL[variant] || FILL.default,
|
|
21
|
+
transition:'border-color var(--duration-fast) var(--ease-out)',
|
|
22
|
+
...style,
|
|
23
|
+
}}
|
|
24
|
+
{...rest}
|
|
25
|
+
>
|
|
26
|
+
{sheen && <div aria-hidden style={{position:'absolute',inset:0,pointerEvents:'none',background:'var(--sheen-card)',opacity:0.7}} />}
|
|
27
|
+
{children}
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function CardHeader({ style, children, ...rest }) {
|
|
33
|
+
return <div style={{position:'relative',padding:'24px 24px 0',display:'flex',flexDirection:'column',gap:6,...style}} {...rest}>{children}</div>
|
|
34
|
+
}
|
|
35
|
+
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>
|
|
37
|
+
}
|
|
38
|
+
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>
|
|
40
|
+
}
|
|
41
|
+
export function CardContent({ style, children, ...rest }) {
|
|
42
|
+
return <div style={{position:'relative',padding:24,...style}} {...rest}>{children}</div>
|
|
43
|
+
}
|
|
44
|
+
export function CardFooter({ style, children, ...rest }) {
|
|
45
|
+
return <div style={{position:'relative',padding:'0 24px 24px',display:'flex',alignItems:'center',gap:12,...style}} {...rest}>{children}</div>
|
|
46
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
The default grouping surface: 1px hairline border, near-black fill, 12px radius.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<Card interactive sheen>
|
|
5
|
+
<CardContent>
|
|
6
|
+
<Icon name="Shield" size={24} />
|
|
7
|
+
<CardTitle style={{marginTop:12}}>Private by default</CardTitle>
|
|
8
|
+
<CardDescription>Every request is scoped to your org.</CardDescription>
|
|
9
|
+
</CardContent>
|
|
10
|
+
</Card>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Fills: default (grid tiles) · emphasis (featured) · quiet (landing story cards) · plain. Hover only ever brightens the border — cards never lift, scale, or glow.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Chrome-gradient headline text — the one gradient Hanzo permits. */
|
|
2
|
+
export interface ChromeTextProps extends React.HTMLAttributes<HTMLElement> {
|
|
3
|
+
/** Element to render, e.g. "h1" | "h2". */
|
|
4
|
+
as?: keyof JSX.IntrinsicElements;
|
|
5
|
+
/** chrome = white→white/60 (default). deep = white→neutral-500, for very large display type. */
|
|
6
|
+
tone?: 'chrome' | 'deep';
|
|
7
|
+
/** Optional uppercase eyebrow rendered above the headline. */
|
|
8
|
+
preHeading?: string;
|
|
9
|
+
}
|
|
10
|
+
export function ChromeText(props: ChromeTextProps): JSX.Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
/** The canonical Hanzo headline treatment: white → white/60 chrome gradient,
|
|
4
|
+
* clipped to the text. Never a saturated or rainbow gradient. */
|
|
5
|
+
export function ChromeText({ as = 'span', tone = 'chrome', preHeading, preHeadingStyle, style, children, ...rest }) {
|
|
6
|
+
const Tag = as
|
|
7
|
+
const gradient = tone === 'deep' ? 'var(--gradient-chrome-2)' : 'var(--gradient-chrome)'
|
|
8
|
+
const node = (
|
|
9
|
+
<Tag
|
|
10
|
+
style={{
|
|
11
|
+
backgroundImage:gradient,
|
|
12
|
+
WebkitBackgroundClip:'text',backgroundClip:'text',
|
|
13
|
+
WebkitTextFillColor:'transparent',color:'transparent',
|
|
14
|
+
display:'inline-block',
|
|
15
|
+
...style,
|
|
16
|
+
}}
|
|
17
|
+
{...rest}
|
|
18
|
+
>
|
|
19
|
+
{children}
|
|
20
|
+
</Tag>
|
|
21
|
+
)
|
|
22
|
+
if (!preHeading) return node
|
|
23
|
+
return (
|
|
24
|
+
<span style={{display:'flex',flexDirection:'column',gap:12,alignItems:'inherit'}}>
|
|
25
|
+
<span style={{fontSize:'var(--text-xs)',fontWeight:'var(--weight-semibold)',textTransform:'uppercase',letterSpacing:'var(--tracking-widest)',color:'var(--text-helper)',...preHeadingStyle}}>{preHeading}</span>
|
|
26
|
+
{node}
|
|
27
|
+
</span>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Wraps hero and section headlines in the chrome gradient (white → white/60).
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<ChromeText as="h1" style={{fontSize:'var(--text-6xl)',fontWeight:700}}>Enso, our frontier model</ChromeText>
|
|
5
|
+
<ChromeText as="h2" preHeading="AI cloud" style={{fontSize:'var(--text-4xl)',fontWeight:700}}>Built for agents</ChromeText>
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
Every product-page h1 uses this. `tone="deep"` (white → neutral-500) is for the very largest display type. Never substitute a hued gradient.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** The Hanzo mark from the press kit.
|
|
2
|
+
export interface HanzoLogoProps {
|
|
3
|
+
/** Rendered square size in px. Nav/footer use 22; hero lockups 80. */
|
|
4
|
+
size?: number;
|
|
5
|
+
/** white on dark surfaces (default), black on light, inherit to take currentColor. */
|
|
6
|
+
variant?: 'white' | 'black' | 'inherit';
|
|
7
|
+
title?: string;
|
|
8
|
+
}
|
|
9
|
+
export function HanzoLogo(props: HanzoLogoProps): JSX.Element;
|
|
10
|
+
export interface HanzoWordmarkProps extends HanzoLogoProps { label?: string }
|
|
11
|
+
export function HanzoWordmark(props: HanzoWordmarkProps): JSX.Element;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
/** The Hanzo mark, exactly as shipped in the press kit (assets/logo.svg).
|
|
4
|
+
* Path data is the press-kit source; the light seams stay #DDDDDD. */
|
|
5
|
+
export function HanzoLogo({ size = 22, variant = 'white', title = 'Hanzo', style, ...rest }) {
|
|
6
|
+
const fill = variant === 'white' ? '#ffffff' : variant === 'black' ? 'var(--hanzo-black)' : 'currentColor'
|
|
7
|
+
return (
|
|
8
|
+
<svg viewBox="0 0 67 67" width={size} height={size} xmlns="http://www.w3.org/2000/svg" role="img" aria-label={title} style={{ display: 'block', flexShrink: 0, ...style }} {...rest}>
|
|
9
|
+
<path d="M22.21 67V44.6369H0V67H22.21Z" fill={fill}></path>
|
|
10
|
+
<path d="M0 44.6369L22.21 46.8285V44.6369H0Z" fill="#DDDDDD"></path>
|
|
11
|
+
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill={fill}></path>
|
|
12
|
+
<path d="M22.21 0H0V22.3184H22.21V0Z" fill={fill}></path>
|
|
13
|
+
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill={fill}></path>
|
|
14
|
+
<path d="M66.6753 22.3185L44.5098 20.0822V22.3185H66.6753Z" fill="#DDDDDD"></path>
|
|
15
|
+
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill={fill}></path>
|
|
16
|
+
</svg>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Mark + wordmark lockup, as used in the nav and footer. */
|
|
21
|
+
export function HanzoWordmark({ size = 22, label = 'Hanzo AI', variant = 'white', style }) {
|
|
22
|
+
return (
|
|
23
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, ...style }}>
|
|
24
|
+
<HanzoLogo size={size} variant={variant} />
|
|
25
|
+
<span style={{ fontSize: 15, fontWeight: 600, letterSpacing: 'var(--tracking-tight)', color: 'var(--text-primary)' }}>{label}</span>
|
|
26
|
+
</span>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
The Hanzo mark (and mark+wordmark lockup) — use instead of typing "Hanzo" where a logo belongs.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<HanzoLogo size={22} variant="white" />
|
|
5
|
+
<HanzoWordmark label="Hanzo AI" />
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
Nav and footer use size 22; the wordmark collapses to the bare mark once the page scrolls. Never recolour with hue, never stretch, keep clear space around it.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Lucide icon glyph, the only icon set Hanzo uses. */
|
|
2
|
+
export interface IconProps extends React.SVGAttributes<SVGSVGElement> {
|
|
3
|
+
/** Lucide icon name, PascalCase or kebab-case: "ArrowRight" | "arrow-right". */
|
|
4
|
+
name: string;
|
|
5
|
+
/** Pixel size. Hanzo sizes: 12 inline, 16 in CTAs, 24 card eyebrow, 40 section accent. */
|
|
6
|
+
size?: number;
|
|
7
|
+
strokeWidth?: number;
|
|
8
|
+
color?: string;
|
|
9
|
+
}
|
|
10
|
+
export function Icon(props: IconProps): JSX.Element;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
/** Lucide glyph renderer. Hanzo uses lucide-react 0.462.0 and no other icon set;
|
|
4
|
+
* in a plain browser page the same set arrives as the lucide UMD bundle, which
|
|
5
|
+
* this component reads. Load it once per page:
|
|
6
|
+
* <script src="https://unpkg.com/lucide@0.462.0/dist/umd/lucide.js"></script> */
|
|
7
|
+
const pascal = (n) => n.replace(/(^|[-_ ])(\w)/g, (_, __, c) => c.toUpperCase())
|
|
8
|
+
|
|
9
|
+
function lookup(name) {
|
|
10
|
+
const g = typeof window !== 'undefined' ? window.lucide : null
|
|
11
|
+
if (!g) return null
|
|
12
|
+
const set = g.icons || g
|
|
13
|
+
return set[name] || set[pascal(name)] || null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Icon({ name, size = 16, strokeWidth = 2, color = 'currentColor', style, ...rest }) {
|
|
17
|
+
const node = lookup(name)
|
|
18
|
+
const kids = []
|
|
19
|
+
if (Array.isArray(node)) {
|
|
20
|
+
node.forEach((child, i) => {
|
|
21
|
+
if (Array.isArray(child)) kids.push(React.createElement(child[0], { key: i, ...child[1] }))
|
|
22
|
+
else if (child && child.tag) kids.push(React.createElement(child.tag, { key: i, ...child.attrs }))
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
return (
|
|
26
|
+
<svg
|
|
27
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
28
|
+
width={size}
|
|
29
|
+
height={size}
|
|
30
|
+
viewBox="0 0 24 24"
|
|
31
|
+
fill="none"
|
|
32
|
+
stroke={color}
|
|
33
|
+
strokeWidth={strokeWidth}
|
|
34
|
+
strokeLinecap="round"
|
|
35
|
+
strokeLinejoin="round"
|
|
36
|
+
aria-hidden="true"
|
|
37
|
+
style={{ flexShrink: 0, display: 'block', ...style }}
|
|
38
|
+
{...rest}
|
|
39
|
+
>
|
|
40
|
+
{kids}
|
|
41
|
+
</svg>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Renders a lucide glyph — the only icon set on Hanzo surfaces.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<Icon name="ArrowRight" size={16} />
|
|
5
|
+
<Icon name="Shield" size={24} color="var(--text-secondary)" />
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
Requires the lucide UMD bundle on the page: `<script src="https://unpkg.com/lucide@0.462.0/dist/umd/lucide.js"></script>`.
|
|
9
|
+
Sizes in use: 12 (inline with body text), 16 (button/CTA), 24 (card eyebrow), 40 (section accent). Stroke stays at the default 2. Colour inherits — never tint an icon with hue.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Thin monochrome meter — white fill on a neutral-800 track. */
|
|
2
|
+
export interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
value?: number;
|
|
4
|
+
max?: number;
|
|
5
|
+
/** Track height in px. 6 in dashboards, 1.5–2 for inline meters. */
|
|
6
|
+
height?: number;
|
|
7
|
+
}
|
|
8
|
+
export function Progress(props: ProgressProps): JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
export function Progress({ value = 0, max = 100, height = 6, style, ...rest }) {
|
|
4
|
+
const pct = Math.max(0, Math.min(100, (value / max) * 100))
|
|
5
|
+
return (
|
|
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}>
|
|
8
|
+
<div style={{height:'100%',width:pct+'%',background:'var(--primary)',borderRadius:'var(--radius-full)',transition:'width var(--duration-base) var(--ease-out)'}} />
|
|
9
|
+
</div>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Usage/quota meter. White fill on a neutral-800 track — no colour thresholds.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<Progress value={68} height={6} />
|
|
5
|
+
<Progress value={92} height={2} style={{width:64}} />
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
Used for agent memory, credit balance, and resource usage. Pair with a numeric label; the bar alone never carries the value.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Dense data table. Rows separated by neutral-800 hairlines; header sits on pure black. */
|
|
2
|
+
export interface TableRowProps extends React.HTMLAttributes<HTMLTableRowElement> {
|
|
3
|
+
/** Adds the neutral-900/60 hover fill — use when the row opens something. */
|
|
4
|
+
interactive?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export function Table(props: React.TableHTMLAttributes<HTMLTableElement>): JSX.Element;
|
|
7
|
+
export function TableHeader(props: React.HTMLAttributes<HTMLTableSectionElement>): JSX.Element;
|
|
8
|
+
export function TableBody(props: React.HTMLAttributes<HTMLTableSectionElement>): JSX.Element;
|
|
9
|
+
export function TableRow(props: TableRowProps): JSX.Element;
|
|
10
|
+
export function TableHead(props: React.ThHTMLAttributes<HTMLTableCellElement> & { align?: 'left' | 'right' | 'center' }): JSX.Element;
|
|
11
|
+
export function TableCell(props: React.TdHTMLAttributes<HTMLTableCellElement> & { align?: 'left' | 'right' | 'center' }): JSX.Element;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
export function Table({ style, children, ...rest }) {
|
|
4
|
+
return <table style={{width:'100%',borderCollapse:'collapse',fontSize:'var(--text-sm)',...style}} {...rest}>{children}</table>
|
|
5
|
+
}
|
|
6
|
+
export function TableHeader({ style, children, ...rest }) {
|
|
7
|
+
return <thead style={{background:'var(--black)',...style}} {...rest}>{children}</thead>
|
|
8
|
+
}
|
|
9
|
+
export function TableBody({ style, children, ...rest }) {
|
|
10
|
+
return <tbody style={style} {...rest}>{children}</tbody>
|
|
11
|
+
}
|
|
12
|
+
export function TableRow({ interactive = false, style, children, ...rest }) {
|
|
13
|
+
const [hover, setHover] = React.useState(false)
|
|
14
|
+
return (
|
|
15
|
+
<tr
|
|
16
|
+
onMouseEnter={interactive ? () => setHover(true) : undefined}
|
|
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}}
|
|
19
|
+
{...rest}
|
|
20
|
+
>
|
|
21
|
+
{children}
|
|
22
|
+
</tr>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
export function TableHead({ align = 'left', style, children, ...rest }) {
|
|
26
|
+
return <th style={{padding:'12px 24px',textAlign:align,fontWeight:'var(--weight-medium)',color:'var(--text-helper)',...style}} {...rest}>{children}</th>
|
|
27
|
+
}
|
|
28
|
+
export function TableCell({ align = 'left', style, children, ...rest }) {
|
|
29
|
+
return <td style={{padding:'16px 24px',textAlign:align,color:'var(--foreground)',...style}} {...rest}>{children}</td>
|
|
30
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Invoice / agent / usage tables in the console.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<Table>
|
|
5
|
+
<TableHeader><TableRow><TableHead>Invoice</TableHead><TableHead align="right">Amount</TableHead></TableRow></TableHeader>
|
|
6
|
+
<TableBody><TableRow interactive><TableCell>INV-20230501</TableCell><TableCell align="right">$25.00</TableCell></TableRow></TableBody>
|
|
7
|
+
</Table>
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Wrap in a `Card` for the rounded hairline frame. Status cells use `Badge` with the opacity ladder, not coloured chips.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<!-- @dsCard group="Components" viewport="700x340" name="Core" subtitle="Button, Badge, Card, Progress, Avatar, ChromeText, Icon" -->
|
|
2
|
+
<!doctype html><html class="dark"><head><meta charset="utf-8"><title>Core components</title>
|
|
3
|
+
<link rel="stylesheet" href="../../styles.css">
|
|
4
|
+
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
5
|
+
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
|
6
|
+
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
|
7
|
+
<script src="https://unpkg.com/lucide@0.462.0/dist/umd/lucide.js"></script>
|
|
8
|
+
<script src="../../_ds_bundle.js"></script>
|
|
9
|
+
<style>body{margin:0;padding:20px;background:#000;font-family:var(--font-sans)}.row{display:flex;flex-wrap:wrap;align-items:center;gap:10px}.col{display:flex;flex-direction:column;gap:14px}.lbl{font-size:10px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--neutral-600)}</style></head><body><div id="root"></div>
|
|
10
|
+
<script type="text/babel">
|
|
11
|
+
const { Button, Badge, StatusBadge, Card, CardContent, CardTitle, CardDescription, ChromeText, Progress, Avatar, AvatarFallback, Icon, HanzoLogo } = window.HanzoDesignSystem_0a9445;
|
|
12
|
+
function Demo(){
|
|
13
|
+
return (<div className="col">
|
|
14
|
+
<div className="row">
|
|
15
|
+
<Button pill>Try Hanzo <Icon name="ChevronDown" /></Button>
|
|
16
|
+
<Button variant="secondary">Secondary</Button>
|
|
17
|
+
<Button variant="outline"><Icon name="Filter" /> Filter</Button>
|
|
18
|
+
<Button variant="ghost">Ghost</Button>
|
|
19
|
+
<Button variant="destructive">Delete</Button>
|
|
20
|
+
<Button variant="link">Read the docs</Button>
|
|
21
|
+
<Button variant="ghost" size="icon"><Icon name="Settings" /></Button>
|
|
22
|
+
<Button disabled>Disabled</Button>
|
|
23
|
+
<Button size="sm" variant="outline">Small</Button>
|
|
24
|
+
<Button size="lg" pill>Get started <Icon name="ArrowRight" /></Button>
|
|
25
|
+
</div>
|
|
26
|
+
<div className="row">
|
|
27
|
+
<Badge>Default</Badge><Badge variant="tier1">Edge</Badge><Badge variant="tier2">Pro</Badge><Badge variant="tier3">Ultra</Badge>
|
|
28
|
+
<Badge variant="outline">Open source</Badge><Badge variant="error">Failed</Badge>
|
|
29
|
+
<StatusBadge status="online" label="All systems normal" />
|
|
30
|
+
<Avatar size={28}><AvatarFallback>ZK</AvatarFallback></Avatar>
|
|
31
|
+
<Avatar size={28} />
|
|
32
|
+
<HanzoLogo size={22} />
|
|
33
|
+
</div>
|
|
34
|
+
<div className="row" style={{alignItems:'stretch'}}>
|
|
35
|
+
<Card interactive sheen style={{flex:1,minWidth:200}}>
|
|
36
|
+
<CardContent>
|
|
37
|
+
<Icon name="Shield" size={24} color="var(--text-secondary)" />
|
|
38
|
+
<CardTitle style={{marginTop:12}}>Private by default</CardTitle>
|
|
39
|
+
<CardDescription>Every request scoped to your org.</CardDescription>
|
|
40
|
+
</CardContent>
|
|
41
|
+
</Card>
|
|
42
|
+
<Card variant="emphasis" style={{flex:1,minWidth:200}}>
|
|
43
|
+
<CardContent>
|
|
44
|
+
<div className="lbl">Credits</div>
|
|
45
|
+
<div style={{fontSize:'var(--text-2xl)',fontWeight:600,color:'#fff',margin:'6px 0 10px'}}>$412.80</div>
|
|
46
|
+
<Progress value={68} />
|
|
47
|
+
</CardContent>
|
|
48
|
+
</Card>
|
|
49
|
+
<Card variant="plain" style={{flex:1,minWidth:200,display:'flex',alignItems:'center',justifyContent:'center',padding:16}}>
|
|
50
|
+
<ChromeText as="div" style={{fontSize:'var(--text-3xl)',fontWeight:700,letterSpacing:'-.025em'}}>Chrome text</ChromeText>
|
|
51
|
+
</Card>
|
|
52
|
+
</div>
|
|
53
|
+
</div>)
|
|
54
|
+
}
|
|
55
|
+
ReactDOM.createRoot(document.getElementById('root')).render(<Demo />);
|
|
56
|
+
</script></body></html>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Square 16px checkbox; checked state is a white fill with a black tick. */
|
|
2
|
+
export interface CheckboxProps {
|
|
3
|
+
checked?: boolean;
|
|
4
|
+
defaultChecked?: boolean;
|
|
5
|
+
onChange?: (checked: boolean) => void;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
/** Optional inline label rendered to the right. */
|
|
8
|
+
label?: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
export function Checkbox(props: CheckboxProps): JSX.Element;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
export function Checkbox({ checked, defaultChecked, onChange, disabled, label, style, ...rest }) {
|
|
4
|
+
const [inner, setInner] = React.useState(!!defaultChecked)
|
|
5
|
+
const on = checked === undefined ? inner : checked
|
|
6
|
+
const toggle = () => {
|
|
7
|
+
if (disabled) return
|
|
8
|
+
if (checked === undefined) setInner(!on)
|
|
9
|
+
onChange && onChange(!on)
|
|
10
|
+
}
|
|
11
|
+
const box = (
|
|
12
|
+
<span
|
|
13
|
+
role="checkbox" aria-checked={on} tabIndex={disabled ? -1 : 0}
|
|
14
|
+
onClick={toggle}
|
|
15
|
+
onKeyDown={(e)=>{if(e.key===' '||e.key==='Enter'){e.preventDefault();toggle()}}}
|
|
16
|
+
style={{
|
|
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)'),
|
|
19
|
+
background: on ? 'var(--primary)' : 'transparent',
|
|
20
|
+
cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.5 : 1,
|
|
21
|
+
transition:'background-color var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out)',
|
|
22
|
+
...style,
|
|
23
|
+
}}
|
|
24
|
+
{...rest}
|
|
25
|
+
>
|
|
26
|
+
{on && (
|
|
27
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="var(--primary-foreground)" strokeWidth="4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12" /></svg>
|
|
28
|
+
)}
|
|
29
|
+
</span>
|
|
30
|
+
)
|
|
31
|
+
if (!label) return box
|
|
32
|
+
return <span style={{display:'inline-flex',alignItems:'center',gap:8,fontSize:'var(--text-sm)',color:'var(--text-secondary)'}}>{box}{label}</span>
|
|
33
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Checkbox for opt-ins and multi-select filters.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<Checkbox label="Auto-recharge credits" defaultChecked />
|
|
5
|
+
<Checkbox checked={agree} onChange={setAgree} label="I agree to the terms" />
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
Checked = white fill + black tick. There is no indeterminate state in the Hanzo set.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
export function Input({ invalid = false, style, ...rest }) {
|
|
4
|
+
const [focus, setFocus] = React.useState(false)
|
|
5
|
+
return (
|
|
6
|
+
<input
|
|
7
|
+
onFocus={(e)=>{setFocus(true);rest.onFocus&&rest.onFocus(e)}}
|
|
8
|
+
onBlur={(e)=>{setFocus(false);rest.onBlur&&rest.onBlur(e)}}
|
|
9
|
+
style={{
|
|
10
|
+
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)',
|
|
16
|
+
...style,
|
|
17
|
+
}}
|
|
18
|
+
{...rest}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Text field for search, forms, and settings.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<Input placeholder="Search agents…" />
|
|
5
|
+
<Input type="email" invalid aria-describedby="err" />
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
For search fields, place a 16px lucide `Search` icon absolutely at 12px from the left and pad the input to 36px. Focus shows a `--ring` border, never a removed outline.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
export function Label({ required = false, style, children, ...rest }) {
|
|
4
|
+
return (
|
|
5
|
+
<label style={{display:'inline-block',fontSize:'var(--text-sm)',fontWeight:'var(--weight-medium)',color:'var(--text-secondary)',...style}} {...rest}>
|
|
6
|
+
{children}{required && <span aria-hidden style={{color:'var(--text-helper)'}}> *</span>}
|
|
7
|
+
</label>
|
|
8
|
+
)
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Dropdown select. Same chrome as Input plus a chevron. */
|
|
2
|
+
export interface SelectOption { value: string; label: string }
|
|
3
|
+
export interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'onChange'> {
|
|
4
|
+
options?: (SelectOption | string)[];
|
|
5
|
+
value?: string;
|
|
6
|
+
onChange?: (value: string) => void;
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
}
|
|
9
|
+
export function Select(props: SelectProps): JSX.Element;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
/** Native-select build of the @hanzo/ui Select — same chrome, no portal. */
|
|
4
|
+
export function Select({ options = [], value, onChange, placeholder, style, children, ...rest }) {
|
|
5
|
+
const [focus, setFocus] = React.useState(false)
|
|
6
|
+
return (
|
|
7
|
+
<span style={{position:'relative',display:'inline-flex',alignItems:'center',width:'100%'}}>
|
|
8
|
+
<select
|
|
9
|
+
value={value}
|
|
10
|
+
onChange={(e)=>onChange && onChange(e.target.value)}
|
|
11
|
+
onFocus={()=>setFocus(true)} onBlur={()=>setFocus(false)}
|
|
12
|
+
style={{
|
|
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',
|
|
17
|
+
transition:'border-color var(--duration-fast) var(--ease-out)',
|
|
18
|
+
...style,
|
|
19
|
+
}}
|
|
20
|
+
{...rest}
|
|
21
|
+
>
|
|
22
|
+
{placeholder && <option value="">{placeholder}</option>}
|
|
23
|
+
{options.map((o) => {
|
|
24
|
+
const v = typeof o === 'string' ? o : o.value
|
|
25
|
+
const l = typeof o === 'string' ? o : o.label
|
|
26
|
+
return <option key={v} value={v}>{l}</option>
|
|
27
|
+
})}
|
|
28
|
+
{children}
|
|
29
|
+
</select>
|
|
30
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--text-helper)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" style={{position:'absolute',right:10,pointerEvents:'none'}}><polyline points="6 9 12 15 18 9" /></svg>
|
|
31
|
+
</span>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Single-choice dropdown for settings and filters.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<Select value={org} onChange={setOrg} options={[{value:'hanzo',label:'Hanzo AI'},{value:'zoo',label:'Zoo Labs'}]} />
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
For 2–3 short options prefer `Tabs` or a segmented row of `Button variant="outline"`; reach for Select at 4+.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Range control — white filled track on neutral-800, used on the pricing/team-size calculators. */
|
|
2
|
+
export interface SliderProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'> {
|
|
3
|
+
min?: number; max?: number; step?: number;
|
|
4
|
+
value?: number; defaultValue?: number;
|
|
5
|
+
onChange?: (value: number) => void;
|
|
6
|
+
}
|
|
7
|
+
export function Slider(props: SliderProps): JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
export function Slider({ min = 0, max = 100, step = 1, value, defaultValue = 0, onChange, style, ...rest }) {
|
|
4
|
+
const [inner, setInner] = React.useState(defaultValue)
|
|
5
|
+
const v = value === undefined ? inner : value
|
|
6
|
+
const pct = ((v - min) / (max - min)) * 100
|
|
7
|
+
return (
|
|
8
|
+
<input
|
|
9
|
+
type="range" min={min} max={max} step={step} value={v}
|
|
10
|
+
onChange={(e) => { const n = Number(e.target.value); if (value === undefined) setInner(n); onChange && onChange(n) }}
|
|
11
|
+
style={{
|
|
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))',
|
|
14
|
+
backgroundSize: pct + '% 4px, 100% 4px',
|
|
15
|
+
backgroundPosition:'left center, left center',
|
|
16
|
+
backgroundRepeat:'no-repeat',
|
|
17
|
+
...style,
|
|
18
|
+
}}
|
|
19
|
+
{...rest}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
export function Textarea({ rows = 4, style, ...rest }) {
|
|
4
|
+
const [focus, setFocus] = React.useState(false)
|
|
5
|
+
return (
|
|
6
|
+
<textarea
|
|
7
|
+
rows={rows}
|
|
8
|
+
onFocus={(e)=>{setFocus(true);rest.onFocus&&rest.onFocus(e)}}
|
|
9
|
+
onBlur={(e)=>{setFocus(false);rest.onBlur&&rest.onBlur(e)}}
|
|
10
|
+
style={{
|
|
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',
|
|
15
|
+
transition:'border-color var(--duration-fast) var(--ease-out)',
|
|
16
|
+
...style,
|
|
17
|
+
}}
|
|
18
|
+
{...rest}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
}
|