zark-design 1.0.0 → 2.0.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/bin/cli.js +7 -6
- package/package.json +2 -2
- package/templates/README.md +264 -0
- package/templates/html/components.css +740 -0
- package/templates/html/index.html +135 -0
- package/templates/html/showcase.html +3550 -0
- package/templates/{tokens.css → html/tokens.css} +89 -112
- package/templates/jsx/App.example.jsx +229 -0
- package/templates/jsx/components/AlertCritical.jsx +43 -0
- package/templates/jsx/components/Avatar.jsx +41 -0
- package/templates/jsx/components/Badge.jsx +12 -0
- package/templates/jsx/components/Banner.jsx +42 -0
- package/templates/jsx/components/Button.jsx +43 -0
- package/templates/jsx/components/Chip.jsx +28 -0
- package/templates/jsx/components/CodeBlock.jsx +42 -0
- package/templates/jsx/components/EmptyState.jsx +27 -0
- package/templates/jsx/components/Funnel.jsx +55 -0
- package/templates/jsx/components/Input.jsx +53 -0
- package/templates/jsx/components/KanbanColumn.jsx +51 -0
- package/templates/jsx/components/Kbd.jsx +11 -0
- package/templates/jsx/components/LeadCard.jsx +79 -0
- package/templates/jsx/components/Modal.jsx +57 -0
- package/templates/jsx/components/Panel.jsx +25 -0
- package/templates/jsx/components/Section.jsx +28 -0
- package/templates/jsx/components/Segmented.jsx +26 -0
- package/templates/jsx/components/Sidebar.jsx +49 -0
- package/templates/jsx/components/Spec.jsx +19 -0
- package/templates/jsx/components/StatCard.jsx +44 -0
- package/templates/jsx/components/TableActions.jsx +34 -0
- package/templates/jsx/components/Tag.jsx +21 -0
- package/templates/jsx/components/TagDot.jsx +26 -0
- package/templates/jsx/components/Toast.jsx +25 -0
- package/templates/jsx/components/Toggle.jsx +29 -0
- package/templates/jsx/components.css +740 -0
- package/templates/{icons.jsx → jsx/icons.jsx} +20 -9
- package/templates/jsx/index.js +31 -0
- package/templates/jsx/tokens.css +283 -0
- package/templates/jsx/tokens.js +62 -0
- package/templates/REFERENCE.md +0 -376
- package/templates/SHOWCASE.html +0 -254
- package/templates/brand.jsx +0 -89
- package/templates/components.jsx +0 -385
- package/templates/design-canvas.jsx +0 -789
- package/templates/foundations.jsx +0 -363
- package/templates/layouts.jsx +0 -232
- package/templates/patterns.jsx +0 -268
- package/templates/primitives.jsx +0 -306
- package/templates/visual-references/pasted-1777605750385-0.png +0 -0
- package/templates/visual-references/pasted-1777605766298-0.png +0 -0
- package/templates/visual-references/pasted-1777605775820-0.png +0 -0
- package/templates/visual-references/pasted-1777605789833-0.png +0 -0
- package/templates/visual-references/pasted-1777605802420-0.png +0 -0
- package/templates/visual-references/pasted-1777605812470-0.png +0 -0
- package/templates/visual-references/pasted-1777605817688-0.png +0 -0
- package/templates/visual-references/pasted-1777605828485-0.png +0 -0
- package/templates/visual-references/pasted-1777605837137-0.png +0 -0
- package/templates/visual-references/pasted-1777605849789-0.png +0 -0
- package/templates/visual-references/pasted-1777605864942-0.png +0 -0
- package/templates/visual-references/pasted-1777605877920-0.png +0 -0
- package/templates/visual-references/pasted-1777605897353-0.png +0 -0
- /package/templates/{assets/zark-logo.png → html/assets/logo-zark-laranja.png} +0 -0
- /package/templates/{assets → html/assets}/zark-icon.png +0 -0
- /package/templates/{visual-references/logo-zark-principal.png → jsx/assets/logo-zark-laranja.png} +0 -0
- /package/templates/{visual-references/icon-zark.png → jsx/assets/zark-icon.png} +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Announce — top announcement bar (full-width, neutro)
|
|
5
|
+
*/
|
|
6
|
+
export function Announce({ children, className = '', ...rest }) {
|
|
7
|
+
return <div className={`announce ${className}`.trim()} {...rest}>{children}</div>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Tip — in-page tip card com ícone ember + título + descrição
|
|
12
|
+
*
|
|
13
|
+
* @param eyebrow string (uppercase mono small)
|
|
14
|
+
* @param title string
|
|
15
|
+
* @param desc string
|
|
16
|
+
* @param icon ReactNode
|
|
17
|
+
* @param onClose function opcional (mostra close X)
|
|
18
|
+
*/
|
|
19
|
+
export function Tip({ eyebrow, title, desc, icon, onClose }) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="tip">
|
|
22
|
+
{icon && <span className="ico">{icon}</span>}
|
|
23
|
+
<div style={{ flex: 1 }}>
|
|
24
|
+
{eyebrow && <div className="what">{eyebrow}</div>}
|
|
25
|
+
{title && <div className="title">{title}</div>}
|
|
26
|
+
{desc && <div className="desc">{desc}</div>}
|
|
27
|
+
</div>
|
|
28
|
+
{onClose && (
|
|
29
|
+
<svg
|
|
30
|
+
width="12" height="12" viewBox="0 0 18 18"
|
|
31
|
+
fill="none" stroke="currentColor" strokeWidth="1.5"
|
|
32
|
+
style={{ color: 'var(--ink-300)', cursor: 'pointer' }}
|
|
33
|
+
onClick={onClose}
|
|
34
|
+
>
|
|
35
|
+
<path d="M5 5l8 8M13 5l-8 8"/>
|
|
36
|
+
</svg>
|
|
37
|
+
)}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default Announce;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Button — 5 variants × 4 sizes
|
|
5
|
+
* @param variant primary · secondary · soft · ghost · danger
|
|
6
|
+
* @param size xs · sm · md · lg
|
|
7
|
+
* @param icon ReactNode rendered before children
|
|
8
|
+
* @param iconRight ReactNode rendered after children
|
|
9
|
+
* @param iconOnly boolean — square button with no padding
|
|
10
|
+
* @param full boolean — width: 100%
|
|
11
|
+
* @param loading boolean — replaces icon with spinner
|
|
12
|
+
*/
|
|
13
|
+
export function Button({
|
|
14
|
+
variant = 'secondary',
|
|
15
|
+
size = 'md',
|
|
16
|
+
icon,
|
|
17
|
+
iconRight,
|
|
18
|
+
iconOnly = false,
|
|
19
|
+
full = false,
|
|
20
|
+
loading = false,
|
|
21
|
+
className = '',
|
|
22
|
+
children,
|
|
23
|
+
...rest
|
|
24
|
+
}) {
|
|
25
|
+
const cls = [
|
|
26
|
+
'btn',
|
|
27
|
+
`btn-${variant}`,
|
|
28
|
+
`btn-${size}`,
|
|
29
|
+
iconOnly && 'btn-icon-only',
|
|
30
|
+
full && 'btn-full',
|
|
31
|
+
className,
|
|
32
|
+
].filter(Boolean).join(' ');
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<button className={cls} {...rest}>
|
|
36
|
+
{loading ? <span className="spinner"/> : icon}
|
|
37
|
+
{children}
|
|
38
|
+
{iconRight}
|
|
39
|
+
</button>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default Button;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chip — filter pill-style (mas raio sm, sem pill)
|
|
5
|
+
* @param items [{ value, label, count?, icon? }]
|
|
6
|
+
* @param value selected value
|
|
7
|
+
* @param onChange function(nextValue)
|
|
8
|
+
*/
|
|
9
|
+
export function Chips({ items = [], value, onChange, className = '' }) {
|
|
10
|
+
return (
|
|
11
|
+
<div className={`chips ${className}`.trim()}>
|
|
12
|
+
{items.map(it => (
|
|
13
|
+
<button
|
|
14
|
+
key={it.value}
|
|
15
|
+
type="button"
|
|
16
|
+
className={`chip ${it.value === value ? 'active' : ''}`.trim()}
|
|
17
|
+
onClick={() => onChange?.(it.value)}
|
|
18
|
+
>
|
|
19
|
+
{it.icon}
|
|
20
|
+
{it.label}
|
|
21
|
+
{typeof it.count === 'number' && <span className="count">({it.count})</span>}
|
|
22
|
+
</button>
|
|
23
|
+
))}
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default Chips;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CodeBlock — bg --code-bg warm, font-mono, com botões eye/copy no canto.
|
|
5
|
+
* Pode receber tanto string quanto JSX (pra syntax highlighting custom via spans .k .s).
|
|
6
|
+
*
|
|
7
|
+
* @param code string ou ReactNode
|
|
8
|
+
* @param onCopy function — handler do copy
|
|
9
|
+
* @param onPreview function — handler do eye
|
|
10
|
+
*/
|
|
11
|
+
export function CodeBlock({ code, onCopy, onPreview, className = '' }) {
|
|
12
|
+
const handleCopy = () => {
|
|
13
|
+
if (onCopy) return onCopy();
|
|
14
|
+
if (typeof code === 'string' && navigator.clipboard) {
|
|
15
|
+
navigator.clipboard.writeText(code);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className={`code-block ${className}`.trim()}>
|
|
21
|
+
<div className="copy">
|
|
22
|
+
{onPreview && (
|
|
23
|
+
<button className="btn btn-ghost btn-icon-only btn-xs" onClick={onPreview} title="Preview">
|
|
24
|
+
<svg width="12" height="12" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
25
|
+
<path d="M1 9c2-3.5 5-5 8-5s6 1.5 8 5c-2 3.5-5 5-8 5s-6-1.5-8-5z"/>
|
|
26
|
+
<circle cx="9" cy="9" r="2"/>
|
|
27
|
+
</svg>
|
|
28
|
+
</button>
|
|
29
|
+
)}
|
|
30
|
+
<button className="btn btn-ghost btn-icon-only btn-xs" onClick={handleCopy} title="Copiar">
|
|
31
|
+
<svg width="12" height="12" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
32
|
+
<rect x="6" y="6" width="9" height="9" rx="1.5"/>
|
|
33
|
+
<path d="M3 12V4a1 1 0 011-1h8"/>
|
|
34
|
+
</svg>
|
|
35
|
+
</button>
|
|
36
|
+
</div>
|
|
37
|
+
{typeof code === 'string' ? <pre style={{ margin: 0 }}>{code}</pre> : code}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default CodeBlock;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* EmptyState — dashed border + sparkle/icon + texto muted
|
|
5
|
+
* Usado em colunas Kanban vazias, drop zones do Pipeline, listas sem dados.
|
|
6
|
+
*
|
|
7
|
+
* @param icon ReactNode — default usa Sparkles
|
|
8
|
+
* @param text string ou ReactNode
|
|
9
|
+
* @param action ReactNode opcional (botão, link)
|
|
10
|
+
*/
|
|
11
|
+
export function EmptyState({ icon, text, action, style, ...rest }) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="empty-state" style={style} {...rest}>
|
|
14
|
+
<div className="ico">
|
|
15
|
+
{icon || (
|
|
16
|
+
<svg width="24" height="24" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
17
|
+
<path d="M9 2l1.5 4.5L15 8l-4.5 1.5L9 14l-1.5-4.5L3 8l4.5-1.5z"/>
|
|
18
|
+
</svg>
|
|
19
|
+
)}
|
|
20
|
+
</div>
|
|
21
|
+
{text && <div className="text">{text}</div>}
|
|
22
|
+
{action}
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default EmptyState;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Funnel — horizontal segmented funnel (Pipeline de Vendas)
|
|
5
|
+
*
|
|
6
|
+
* @param stages [{ label, count, color, percent }]
|
|
7
|
+
* color — qualquer CSS color (sugestão: var(--ember-500), var(--success-500), etc)
|
|
8
|
+
* percent — 0-100, largura proporcional do segmento
|
|
9
|
+
*/
|
|
10
|
+
export function Funnel({ stages = [] }) {
|
|
11
|
+
const total = stages.reduce((s, x) => s + (x.percent || 0), 0);
|
|
12
|
+
const fillRest = Math.max(0, 100 - total);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div>
|
|
16
|
+
<div className="funnel">
|
|
17
|
+
{stages.map((s, i) => (
|
|
18
|
+
<span key={i} style={{ width: `${s.percent || 0}%`, background: s.color }}/>
|
|
19
|
+
))}
|
|
20
|
+
{fillRest > 0 && <span style={{ width: `${fillRest}%`, background: 'var(--ink-200)' }}/>}
|
|
21
|
+
</div>
|
|
22
|
+
<div className="funnel-stages" style={{ gridTemplateColumns: `repeat(${stages.length}, 1fr)` }}>
|
|
23
|
+
{stages.map((s, i) => (
|
|
24
|
+
<div key={i} className="funnel-stage">
|
|
25
|
+
<span className="funnel-stage-label" style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
26
|
+
<span style={{ width: 6, height: 6, borderRadius: '50%', background: s.color }}/>
|
|
27
|
+
{s.label}
|
|
28
|
+
</span>
|
|
29
|
+
<span className="funnel-stage-count">{s.count}</span>
|
|
30
|
+
</div>
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* ProgressBar — linear progress (meta, recebido vs previsto)
|
|
39
|
+
* @param value 0-100
|
|
40
|
+
* @param tone 'success' (default) | 'ember' | 'danger'
|
|
41
|
+
*/
|
|
42
|
+
export function ProgressBar({ value = 0, tone = 'success', height = 4 }) {
|
|
43
|
+
const colorMap = {
|
|
44
|
+
success: 'var(--success-500)',
|
|
45
|
+
ember: 'var(--ember-500)',
|
|
46
|
+
danger: 'var(--danger-500)',
|
|
47
|
+
};
|
|
48
|
+
return (
|
|
49
|
+
<div className="stat-progress" style={{ height }}>
|
|
50
|
+
<span style={{ width: `${Math.max(0, Math.min(100, value))}%`, background: colorMap[tone] }}/>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default Funnel;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Input — text input with optional icon and suffix
|
|
5
|
+
* @param size sm · md · lg
|
|
6
|
+
* @param icon ReactNode rendered before input
|
|
7
|
+
* @param suffix ReactNode rendered after input
|
|
8
|
+
* @param mono boolean — monospace font (API keys, codes)
|
|
9
|
+
* @param invalid boolean — danger border + ring
|
|
10
|
+
*/
|
|
11
|
+
export function Input({
|
|
12
|
+
size = 'md',
|
|
13
|
+
icon,
|
|
14
|
+
suffix,
|
|
15
|
+
mono = false,
|
|
16
|
+
invalid = false,
|
|
17
|
+
className = '',
|
|
18
|
+
...rest
|
|
19
|
+
}) {
|
|
20
|
+
const cls = [
|
|
21
|
+
'input',
|
|
22
|
+
`input-${size}`,
|
|
23
|
+
mono && 'mono',
|
|
24
|
+
invalid && 'invalid',
|
|
25
|
+
className,
|
|
26
|
+
].filter(Boolean).join(' ');
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<label className={cls}>
|
|
30
|
+
{icon && <span className="ico">{icon}</span>}
|
|
31
|
+
<input {...rest}/>
|
|
32
|
+
{suffix && <span className="suffix">{suffix}</span>}
|
|
33
|
+
</label>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* UrlInputFull — full-width URL input with scheme prefix
|
|
39
|
+
* Substitui o antigo .url-pill (search-style)
|
|
40
|
+
*/
|
|
41
|
+
export function UrlInputFull({ scheme = 'https://', placeholder, schemeIcon, ...rest }) {
|
|
42
|
+
return (
|
|
43
|
+
<div className="url-input-full">
|
|
44
|
+
<span className="scheme">
|
|
45
|
+
{schemeIcon}
|
|
46
|
+
{scheme}
|
|
47
|
+
</span>
|
|
48
|
+
<input placeholder={placeholder} {...rest}/>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default Input;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* KanbanColumn — coluna de Kanban com header + dot colorido + contador
|
|
5
|
+
*
|
|
6
|
+
* @param title string
|
|
7
|
+
* @param count number
|
|
8
|
+
* @param dotColor CSS color (segue cor do status)
|
|
9
|
+
* @param onAddClick function — handler do "+ Nova tarefa"
|
|
10
|
+
* @param addLabel string — default "+ Nova tarefa"
|
|
11
|
+
* @param children cards (LeadCard, custom...) ou EmptyState
|
|
12
|
+
*/
|
|
13
|
+
export function KanbanColumn({
|
|
14
|
+
title,
|
|
15
|
+
count,
|
|
16
|
+
dotColor = 'var(--ink-300)',
|
|
17
|
+
onAddClick,
|
|
18
|
+
addLabel = 'Nova tarefa',
|
|
19
|
+
children,
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<div className="k-col">
|
|
23
|
+
<div className="k-col-head">
|
|
24
|
+
<span className="lh">
|
|
25
|
+
<span className="dot" style={{ background: dotColor }}/>
|
|
26
|
+
{title}
|
|
27
|
+
</span>
|
|
28
|
+
<span className="count">{count}</span>
|
|
29
|
+
</div>
|
|
30
|
+
{onAddClick && (
|
|
31
|
+
<button className="k-add" onClick={onAddClick}>
|
|
32
|
+
<svg width="12" height="12" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
|
33
|
+
<path d="M9 4v10M4 9h10"/>
|
|
34
|
+
</svg>
|
|
35
|
+
{addLabel}
|
|
36
|
+
</button>
|
|
37
|
+
)}
|
|
38
|
+
{children}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* KanbanBoard — wrapper opcional que aplica o display:flex correto + scroll horizontal.
|
|
45
|
+
* Use se quiser; também pode renderizar colunas direto num div.kanban próprio.
|
|
46
|
+
*/
|
|
47
|
+
export function KanbanBoard({ children, className = '', ...rest }) {
|
|
48
|
+
return <div className={`kanban ${className}`.trim()} {...rest}>{children}</div>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default KanbanColumn;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Kbd — tecla com bg surface, border line-200, font-mono
|
|
5
|
+
* @example <Kbd>⌘</Kbd><Kbd>K</Kbd>
|
|
6
|
+
*/
|
|
7
|
+
export function Kbd({ children, className = '', ...rest }) {
|
|
8
|
+
return <span className={`kbd ${className}`.trim()} {...rest}>{children}</span>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default Kbd;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LeadCard — card compacto para Pipeline de Vendas
|
|
5
|
+
*
|
|
6
|
+
* @param name string — nome do lead
|
|
7
|
+
* @param company string — sublinha (TR PAULO, Save Car, etc)
|
|
8
|
+
* @param status ReactNode — ex: <TagDot kind="status" value="paused">PARADO</TagDot>
|
|
9
|
+
* @param hot boolean — mostra flame icon ember antes do nome
|
|
10
|
+
* @param channels ReactNode[] — ícones de canal (telefone, instagram, email)
|
|
11
|
+
* @param time string — "5d", "2h ago"
|
|
12
|
+
* @param followUp string — "Follow-up: 27/04/2026" (renderiza em ember)
|
|
13
|
+
* @param indicator string — "Indicado por X"
|
|
14
|
+
* @param paused boolean — adiciona border-left amber
|
|
15
|
+
* @param overdue boolean — adiciona border-left vermelho
|
|
16
|
+
*/
|
|
17
|
+
export function LeadCard({
|
|
18
|
+
name,
|
|
19
|
+
company,
|
|
20
|
+
status,
|
|
21
|
+
hot = false,
|
|
22
|
+
channels,
|
|
23
|
+
time,
|
|
24
|
+
followUp,
|
|
25
|
+
indicator,
|
|
26
|
+
paused = false,
|
|
27
|
+
overdue = false,
|
|
28
|
+
className = '',
|
|
29
|
+
...rest
|
|
30
|
+
}) {
|
|
31
|
+
const cls = [
|
|
32
|
+
'lead-card',
|
|
33
|
+
paused && 'is-paused',
|
|
34
|
+
overdue && 'is-overdue',
|
|
35
|
+
className,
|
|
36
|
+
].filter(Boolean).join(' ');
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className={cls} {...rest}>
|
|
40
|
+
<div className="lead-card-head">
|
|
41
|
+
<span className="lead-card-name">
|
|
42
|
+
{hot && (
|
|
43
|
+
<svg className="flame" width="14" height="14" viewBox="0 0 18 18" fill="currentColor">
|
|
44
|
+
<path d="M9 2c0 0 4 3 4 7a4 4 0 11-8 0c0-2 1-3 1-3 0 1 1 2 2 2 0-2 1-4 1-6z"/>
|
|
45
|
+
</svg>
|
|
46
|
+
)}
|
|
47
|
+
{name}
|
|
48
|
+
</span>
|
|
49
|
+
{status}
|
|
50
|
+
</div>
|
|
51
|
+
{company && <div className="lead-card-sub">{company}</div>}
|
|
52
|
+
{(channels || time) && (
|
|
53
|
+
<div className="lead-card-meta">
|
|
54
|
+
{channels && <span className="icons">{channels}</span>}
|
|
55
|
+
{time && <span style={{ fontFamily: 'var(--font-mono)' }}>{time}</span>}
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
{followUp && (
|
|
59
|
+
<div className="lead-card-followup">
|
|
60
|
+
<svg width="11" height="11" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
61
|
+
<rect x="3" y="4" width="12" height="11" rx="1.5"/>
|
|
62
|
+
<path d="M3 7h12M6 2v3M12 2v3"/>
|
|
63
|
+
</svg>
|
|
64
|
+
{followUp}
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
{indicator && (
|
|
68
|
+
<div className="lead-card-foot">
|
|
69
|
+
<svg width="11" height="11" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
70
|
+
<path d="M3 9c2-3 5-4 6-4s4 1 6 4c-2 3-5 4-6 4s-4-1-6-4z"/>
|
|
71
|
+
</svg>
|
|
72
|
+
{indicator}
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default LeadCard;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Modal — overlay + card max-width 460px, raio 2xl, shadow modal
|
|
6
|
+
*
|
|
7
|
+
* @param open boolean
|
|
8
|
+
* @param onClose function
|
|
9
|
+
* @param title string (Space Grotesk)
|
|
10
|
+
* @param description string opcional
|
|
11
|
+
* @param footer ReactNode (botões à direita)
|
|
12
|
+
* @param closeOnOverlay boolean — default true
|
|
13
|
+
*/
|
|
14
|
+
export function Modal({
|
|
15
|
+
open,
|
|
16
|
+
onClose,
|
|
17
|
+
title,
|
|
18
|
+
description,
|
|
19
|
+
footer,
|
|
20
|
+
closeOnOverlay = true,
|
|
21
|
+
children,
|
|
22
|
+
}) {
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!open) return;
|
|
25
|
+
const onKey = (e) => e.key === 'Escape' && onClose?.();
|
|
26
|
+
window.addEventListener('keydown', onKey);
|
|
27
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
28
|
+
}, [open, onClose]);
|
|
29
|
+
|
|
30
|
+
if (!open) return null;
|
|
31
|
+
|
|
32
|
+
const content = (
|
|
33
|
+
<div
|
|
34
|
+
className="modal-overlay"
|
|
35
|
+
onClick={(e) => closeOnOverlay && e.target === e.currentTarget && onClose?.()}
|
|
36
|
+
>
|
|
37
|
+
<div className="modal" role="dialog" aria-modal="true">
|
|
38
|
+
{(title || description) && (
|
|
39
|
+
<div className="modal-head">
|
|
40
|
+
{title && (
|
|
41
|
+
<div className="modal-title-row">
|
|
42
|
+
<h3 className="modal-title">{title}</h3>
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
45
|
+
{description && <p className="modal-desc">{description}</p>}
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
{children && <div className="modal-body">{children}</div>}
|
|
49
|
+
{footer && <div className="modal-foot">{footer}</div>}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return typeof document !== 'undefined' ? createPortal(content, document.body) : content;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default Modal;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Panel — card padrão com surface bg, border line-200, raio xl, sombra sm
|
|
5
|
+
*
|
|
6
|
+
* @param title string ou ReactNode
|
|
7
|
+
* @param kicker string opcional (uppercase mono à direita)
|
|
8
|
+
* @param flush boolean — remove o padding interno (padding: 0)
|
|
9
|
+
*/
|
|
10
|
+
export function Panel({ title, kicker, flush = false, className = '', children }) {
|
|
11
|
+
const bodyCls = `panel-body ${flush ? 'flush' : ''}`.trim();
|
|
12
|
+
return (
|
|
13
|
+
<div className={`panel ${className}`.trim()}>
|
|
14
|
+
{(title || kicker) && (
|
|
15
|
+
<div className="panel-head">
|
|
16
|
+
{title && <div className="panel-title">{title}</div>}
|
|
17
|
+
{kicker && <div className="panel-kicker">{kicker}</div>}
|
|
18
|
+
</div>
|
|
19
|
+
)}
|
|
20
|
+
<div className={bodyCls}>{children}</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default Panel;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Section — bloco de página com eyebrow + título display + descrição
|
|
5
|
+
*
|
|
6
|
+
* @param eyebrow uppercase mono small (ex: "01 — Foundations")
|
|
7
|
+
* @param title h2 em Space Grotesk
|
|
8
|
+
* @param description texto auxiliar à direita
|
|
9
|
+
* @param id anchor para nav
|
|
10
|
+
*/
|
|
11
|
+
export function Section({ eyebrow, title, description, id, children }) {
|
|
12
|
+
return (
|
|
13
|
+
<section id={id} className="section">
|
|
14
|
+
<div className="section-inner">
|
|
15
|
+
<div className="section-header">
|
|
16
|
+
<div>
|
|
17
|
+
{eyebrow && <div className="eyebrow">{eyebrow}</div>}
|
|
18
|
+
{title && <h2>{title}</h2>}
|
|
19
|
+
</div>
|
|
20
|
+
{description && <p className="section-desc">{description}</p>}
|
|
21
|
+
</div>
|
|
22
|
+
{children}
|
|
23
|
+
</div>
|
|
24
|
+
</section>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default Section;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Segmented — radio-style horizontal selector
|
|
5
|
+
* @param items [{ value, label, icon? }]
|
|
6
|
+
* @param value selected value
|
|
7
|
+
* @param onChange function(nextValue)
|
|
8
|
+
*/
|
|
9
|
+
export function Segmented({ items = [], value, onChange, className = '' }) {
|
|
10
|
+
return (
|
|
11
|
+
<div className={`segmented ${className}`.trim()}>
|
|
12
|
+
{items.map(it => (
|
|
13
|
+
<button
|
|
14
|
+
key={it.value}
|
|
15
|
+
type="button"
|
|
16
|
+
className={it.value === value ? 'active' : ''}
|
|
17
|
+
onClick={() => onChange?.(it.value)}
|
|
18
|
+
>
|
|
19
|
+
{it.icon}{it.label}
|
|
20
|
+
</button>
|
|
21
|
+
))}
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default Segmented;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sidebar — 240px, bg canvas, com slot para brand/logo no topo.
|
|
5
|
+
*
|
|
6
|
+
* @param logo ReactNode — passe <img src="..." className="zk-sidebar-logo"/>
|
|
7
|
+
* @param children qualquer conteúdo (use SidebarLink + SidebarSection)
|
|
8
|
+
* @param footer ReactNode opcional fixo no bottom
|
|
9
|
+
*/
|
|
10
|
+
export function Sidebar({ logo, footer, children }) {
|
|
11
|
+
return (
|
|
12
|
+
<aside className="zk-sidebar">
|
|
13
|
+
{logo && <div className="zk-sidebar-brand">{logo}</div>}
|
|
14
|
+
{children}
|
|
15
|
+
{footer && (
|
|
16
|
+
<div style={{ marginTop: 'auto', paddingTop: 12, borderTop: '1px solid var(--line-200)' }}>
|
|
17
|
+
{footer}
|
|
18
|
+
</div>
|
|
19
|
+
)}
|
|
20
|
+
</aside>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Section header — eyebrow uppercase mono pra agrupar links */
|
|
25
|
+
export function SidebarSection({ children }) {
|
|
26
|
+
return <div className="zk-sidebar-eyebrow">{children}</div>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* SidebarLink — item de navegação
|
|
31
|
+
* @param active boolean
|
|
32
|
+
* @param icon ReactNode
|
|
33
|
+
* @param children label
|
|
34
|
+
* @param shortcut ReactNode opcional (ex: <Kbd>⌥D</Kbd>)
|
|
35
|
+
*/
|
|
36
|
+
export function SidebarLink({ active = false, icon, shortcut, onClick, href, children }) {
|
|
37
|
+
const cls = `zk-sidebar-link ${active ? 'active' : ''}`.trim();
|
|
38
|
+
const content = (
|
|
39
|
+
<>
|
|
40
|
+
{icon && <span style={{ display: 'inline-flex', color: active ? 'var(--ember-600)' : 'var(--ink-400)' }}>{icon}</span>}
|
|
41
|
+
<span style={{ flex: 1 }}>{children}</span>
|
|
42
|
+
{shortcut}
|
|
43
|
+
</>
|
|
44
|
+
);
|
|
45
|
+
if (href) return <a href={href} className={cls} onClick={onClick}>{content}</a>;
|
|
46
|
+
return <button type="button" className={cls} onClick={onClick} style={{ background: 'inherit', border: 'none', textAlign: 'left', font: 'inherit' }}>{content}</button>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default Sidebar;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Spec — linha label/value separada por dashed line.
|
|
5
|
+
* Usar dentro de Panel para listar specs técnicas (heights, paddings, tokens).
|
|
6
|
+
*
|
|
7
|
+
* @param label string
|
|
8
|
+
* @param value string ou ReactNode (renderizado em mono ink-600)
|
|
9
|
+
*/
|
|
10
|
+
export function Spec({ label, value, className = '' }) {
|
|
11
|
+
return (
|
|
12
|
+
<div className={`spec ${className}`.trim()}>
|
|
13
|
+
<span className="l">{label}</span>
|
|
14
|
+
<span className="v">{value}</span>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default Spec;
|