@attrus-ds/core 0.2.0 → 0.3.1
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 +27 -27
- package/dist/assets/flags/Brazil-rounded.svg +15 -15
- package/dist/assets/flags/Brazil-square.svg +16 -16
- package/dist/assets/flags/Chile-rounded.svg +10 -10
- package/dist/assets/flags/Chile-square.svg +10 -10
- package/dist/assets/flags/Colombia-rounded.svg +9 -9
- package/dist/assets/flags/Colombia-square.svg +9 -9
- package/dist/assets/flags/EUA-rounded.svg +10 -10
- package/dist/assets/flags/EUA-square.svg +10 -10
- package/dist/assets/flags/EUR-rounded.svg +8 -8
- package/dist/assets/flags/EUR-square.svg +8 -8
- package/dist/assets/flags/Mexico-rounded.svg +18 -18
- package/dist/assets/flags/Mexico-square.svg +18 -18
- package/dist/assets/icon-attrus-dark.svg +2 -2
- package/dist/assets/icon-attrus-light.svg +2 -2
- package/dist/assets/logo-attrus-dark.svg +7 -7
- package/dist/assets/logo-attrus-light.svg +7 -7
- package/dist/css/components/buttons.css +301 -301
- package/dist/css/components/callout.css +195 -195
- package/dist/css/components/cards.css +272 -272
- package/dist/css/components/combobox.css +319 -319
- package/dist/css/components/empty-states.css +61 -61
- package/dist/css/components/inputs.css +975 -975
- package/dist/css/components/number-input.css +138 -138
- package/dist/css/components/overlays.css +374 -374
- package/dist/css/components/primitives.css +314 -314
- package/dist/css/components/sidebar.css +179 -179
- package/dist/css/components/smart-search.css +91 -91
- package/dist/css/components/status-pills.css +207 -207
- package/dist/css/components/stepper.css +309 -309
- package/dist/css/components/table.css +602 -602
- package/dist/css/components/tabs.css +319 -319
- package/dist/css/components/toast.css +243 -243
- package/dist/css/components/tooltip.css +231 -231
- package/dist/css/components/topbar.css +148 -148
- package/dist/css/tokens.css +666 -666
- package/dist/react/AppSidebar/AppSidebar.d.ts +50 -50
- package/dist/react/AppSidebar/AppSidebar.tsx +197 -197
- package/dist/react/AppTopbar/AppTopbar.d.ts +58 -58
- package/dist/react/AppTopbar/AppTopbar.tsx +203 -203
- package/dist/react/Atoms/Atoms.d.ts +149 -149
- package/dist/react/Atoms/Atoms.tsx +407 -407
- package/dist/react/Button/Button.d.ts +25 -25
- package/dist/react/Button/Button.tsx +83 -83
- package/dist/react/Callout/Callout.d.ts +29 -29
- package/dist/react/Callout/Callout.tsx +98 -98
- package/dist/react/Card/Card.d.ts +38 -38
- package/dist/react/Card/Card.tsx +93 -93
- package/dist/react/Combobox/Combobox.d.ts +52 -52
- package/dist/react/Combobox/Combobox.tsx +254 -254
- package/dist/react/DataTable/DataTable.d.ts +81 -81
- package/dist/react/DataTable/DataTable.tsx +308 -308
- package/dist/react/Drawer/Drawer.d.ts +35 -35
- package/dist/react/Drawer/Drawer.tsx +226 -226
- package/dist/react/Input/Input.d.ts +155 -155
- package/dist/react/Input/Input.tsx +478 -478
- package/dist/react/Modal/Modal.d.ts +27 -27
- package/dist/react/Modal/Modal.tsx +173 -173
- package/dist/react/NumberInput/NumberInput.d.ts +25 -25
- package/dist/react/NumberInput/NumberInput.tsx +117 -117
- package/dist/react/Pill/Pill.d.ts +31 -31
- package/dist/react/Pill/Pill.tsx +104 -104
- package/dist/react/SearchPill/SearchPill.d.ts +14 -14
- package/dist/react/SearchPill/SearchPill.tsx +40 -40
- package/dist/react/SmartSearch/SmartSearch.d.ts +65 -65
- package/dist/react/SmartSearch/SmartSearch.tsx +271 -271
- package/dist/react/Stepper/Stepper.d.ts +25 -25
- package/dist/react/Stepper/Stepper.tsx +94 -94
- package/dist/react/Tabs/Tabs.d.ts +32 -32
- package/dist/react/Tabs/Tabs.tsx +147 -147
- package/dist/react/Toast/Toast.d.ts +30 -30
- package/dist/react/Toast/Toast.tsx +94 -94
- package/dist/react/Tooltip/Tooltip.d.ts +23 -23
- package/dist/react/Tooltip/Tooltip.tsx +80 -80
- package/dist/react/ZeroState/ZeroState.d.ts +17 -17
- package/dist/react/ZeroState/ZeroState.tsx +45 -45
- package/dist/react/index.ts +35 -35
- package/dist/tokens/attrus-tokens.flat.json +1234 -1234
- package/dist/tokens/attrus-tokens.json +1089 -1089
- package/package.json +64 -64
|
@@ -1,271 +1,271 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ATTRUS SmartSearch — typed shell over the canonical `.ss` system
|
|
5
|
-
* (preview/components/smart-search.css). The global ⌘K command palette.
|
|
6
|
-
*
|
|
7
|
-
* The 8 canonical states (combobox.html §03) COMPOSE from props — the
|
|
8
|
-
* component renders whatever slices it receives, in the doc's order:
|
|
9
|
-
* input → chips → sections/list → hint → typeButtons → empty/loading/
|
|
10
|
-
* error illo → footer (kbd hints).
|
|
11
|
-
*
|
|
12
|
-
* inactive → no popover content (flat input)
|
|
13
|
-
* active → chips + sections (recents/suggestions)
|
|
14
|
-
* word → resultLabel + sections
|
|
15
|
-
* similar → hint + sections
|
|
16
|
-
* id → hint + typeButtons
|
|
17
|
-
* partial id → hint only
|
|
18
|
-
* no results → empty
|
|
19
|
-
* loading → loading
|
|
20
|
-
* error → error (retry)
|
|
21
|
-
*
|
|
22
|
-
* Keyboard: ↑/↓ move `.is-active` across items, Enter selects, Esc → onClose.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
export interface SSItem {
|
|
26
|
-
key: string;
|
|
27
|
-
/** Leading 28px tile glyph. */
|
|
28
|
-
icon?: React.ReactNode;
|
|
29
|
-
title: React.ReactNode;
|
|
30
|
-
/** Muted second line. */
|
|
31
|
-
sub?: React.ReactNode;
|
|
32
|
-
/** Right-aligned mono hint (id, shortcut). */
|
|
33
|
-
trailing?: React.ReactNode;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface SSSection {
|
|
37
|
-
/** Uppercase section label (e.g. "Counterparties"). */
|
|
38
|
-
label?: React.ReactNode;
|
|
39
|
-
items: SSItem[];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface SSTypeButton {
|
|
43
|
-
key: string;
|
|
44
|
-
icon?: React.ReactNode;
|
|
45
|
-
label: React.ReactNode;
|
|
46
|
-
/** Muted right hint (e.g. "transaction"). */
|
|
47
|
-
hint?: React.ReactNode;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface SmartSearchProps {
|
|
51
|
-
value: string;
|
|
52
|
-
onChange?: (value: string) => void;
|
|
53
|
-
placeholder?: string;
|
|
54
|
-
/** Leading input glyph (search icon). */
|
|
55
|
-
icon?: React.ReactNode;
|
|
56
|
-
/** Shows the × clear button when value is non-empty. */
|
|
57
|
-
onClear?: () => void;
|
|
58
|
-
/** Esc inside the input. */
|
|
59
|
-
onClose?: () => void;
|
|
60
|
-
/** Suggestion chips row (active state). */
|
|
61
|
-
chips?: Array<{ key: string; icon?: React.ReactNode; label: React.ReactNode }>;
|
|
62
|
-
onChipClick?: (key: string) => void;
|
|
63
|
-
/** Grouped result rows. */
|
|
64
|
-
sections?: SSSection[];
|
|
65
|
-
onSelect?: (key: string) => void;
|
|
66
|
-
/** "RESULTS (N)" line above the list. */
|
|
67
|
-
resultLabel?: React.ReactNode;
|
|
68
|
-
/** Tip card (similar match, ID format hint). */
|
|
69
|
-
hint?: React.ReactNode;
|
|
70
|
-
/** ID-disambiguation buttons. */
|
|
71
|
-
typeButtons?: SSTypeButton[];
|
|
72
|
-
onTypeSelect?: (key: string) => void;
|
|
73
|
-
/** No-results state. */
|
|
74
|
-
empty?: { title: React.ReactNode; sub?: React.ReactNode; illo?: React.ReactNode };
|
|
75
|
-
/** Skeleton rows while fetching. */
|
|
76
|
-
loading?: boolean;
|
|
77
|
-
/** Fetch failed — retry. */
|
|
78
|
-
error?: { title: React.ReactNode; sub?: React.ReactNode; retryLabel?: React.ReactNode; onRetry?: () => void; illo?: React.ReactNode };
|
|
79
|
-
/** Kbd hint footer (↑↓ navigate · ↵ open · esc close). Default true when the popover has content. */
|
|
80
|
-
footer?: boolean;
|
|
81
|
-
className?: string;
|
|
82
|
-
style?: React.CSSProperties;
|
|
83
|
-
'aria-label'?: string;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const SearchGlyph: React.FC = () => (
|
|
87
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
88
|
-
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" />
|
|
89
|
-
</svg>
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
export const SmartSearch: React.FC<SmartSearchProps> = ({
|
|
93
|
-
value,
|
|
94
|
-
onChange,
|
|
95
|
-
placeholder = 'Search or jump to…',
|
|
96
|
-
icon,
|
|
97
|
-
onClear,
|
|
98
|
-
onClose,
|
|
99
|
-
chips,
|
|
100
|
-
onChipClick,
|
|
101
|
-
sections,
|
|
102
|
-
onSelect,
|
|
103
|
-
resultLabel,
|
|
104
|
-
hint,
|
|
105
|
-
typeButtons,
|
|
106
|
-
onTypeSelect,
|
|
107
|
-
empty,
|
|
108
|
-
loading = false,
|
|
109
|
-
error,
|
|
110
|
-
footer,
|
|
111
|
-
className,
|
|
112
|
-
style,
|
|
113
|
-
...rest
|
|
114
|
-
}) => {
|
|
115
|
-
const [active, setActive] = React.useState<string | null>(null);
|
|
116
|
-
const flat = React.useMemo(() => (sections || []).reduce<SSItem[]>((a, s) => a.concat(s.items), []), [sections]);
|
|
117
|
-
|
|
118
|
-
const hasBody = Boolean(
|
|
119
|
-
(chips && chips.length) || (sections && sections.length) || hint != null ||
|
|
120
|
-
(typeButtons && typeButtons.length) || empty != null || loading || error != null
|
|
121
|
-
);
|
|
122
|
-
const showFooter = footer != null ? footer : hasBody;
|
|
123
|
-
|
|
124
|
-
const onKeyDown = (e: React.KeyboardEvent) => {
|
|
125
|
-
if (e.key === 'Escape') { if (onClose) onClose(); return; }
|
|
126
|
-
if (!flat.length) return;
|
|
127
|
-
const idx = flat.findIndex((i) => i.key === active);
|
|
128
|
-
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
129
|
-
e.preventDefault();
|
|
130
|
-
const next = e.key === 'ArrowDown'
|
|
131
|
-
? flat[Math.min(idx + 1, flat.length - 1)]
|
|
132
|
-
: flat[Math.max(idx - 1, 0)];
|
|
133
|
-
setActive(next.key);
|
|
134
|
-
} else if (e.key === 'Enter') {
|
|
135
|
-
const hit = flat.find((i) => i.key === active);
|
|
136
|
-
if (hit && onSelect) { e.preventDefault(); onSelect(hit.key); }
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
const renderItem = (it: SSItem) => (
|
|
141
|
-
<li
|
|
142
|
-
key={it.key}
|
|
143
|
-
className={['ss-list-item', it.key === active ? 'is-active' : ''].filter(Boolean).join(' ')}
|
|
144
|
-
onMouseEnter={() => setActive(it.key)}
|
|
145
|
-
onClick={() => onSelect && onSelect(it.key)}
|
|
146
|
-
>
|
|
147
|
-
{it.icon != null ? <span className="icon-leading">{it.icon}</span> : null}
|
|
148
|
-
<span className="text">
|
|
149
|
-
<span className="label">{it.title}</span>
|
|
150
|
-
{it.sub != null ? <span className="meta">{it.sub}</span> : null}
|
|
151
|
-
</span>
|
|
152
|
-
{it.trailing != null ? (
|
|
153
|
-
<span style={{ marginLeft: 'auto', flexShrink: 0, fontFamily: 'var(--font-mono)', fontSize: 'var(--fs-xxs)', color: 'var(--color-foreground-muted)' }}>{it.trailing}</span>
|
|
154
|
-
) : null}
|
|
155
|
-
</li>
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
return (
|
|
159
|
-
<div className={['ss', className || ''].filter(Boolean).join(' ')} style={style} role="search" aria-label={rest['aria-label'] || 'Smart search'}>
|
|
160
|
-
<div className={['ss-input', hasBody ? '' : 'ss-input-flat'].filter(Boolean).join(' ')}>
|
|
161
|
-
{icon != null ? icon : <SearchGlyph />}
|
|
162
|
-
<input
|
|
163
|
-
value={value}
|
|
164
|
-
placeholder={placeholder}
|
|
165
|
-
onChange={(e) => onChange && onChange(e.target.value)}
|
|
166
|
-
onKeyDown={onKeyDown}
|
|
167
|
-
role="combobox"
|
|
168
|
-
aria-expanded={hasBody}
|
|
169
|
-
/>
|
|
170
|
-
{onClear && value ? (
|
|
171
|
-
<button type="button" className="ss-clear" aria-label="Clear" onClick={onClear}>
|
|
172
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.25} strokeLinecap="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>
|
|
173
|
-
</button>
|
|
174
|
-
) : null}
|
|
175
|
-
</div>
|
|
176
|
-
|
|
177
|
-
{chips && chips.length ? (
|
|
178
|
-
<div className="ss-section">
|
|
179
|
-
<div className="ss-chips">
|
|
180
|
-
{chips.map((c) => (
|
|
181
|
-
<button key={c.key} type="button" className="ss-chip" onClick={() => onChipClick && onChipClick(c.key)}>
|
|
182
|
-
{c.icon}
|
|
183
|
-
{c.label}
|
|
184
|
-
</button>
|
|
185
|
-
))}
|
|
186
|
-
</div>
|
|
187
|
-
</div>
|
|
188
|
-
) : null}
|
|
189
|
-
|
|
190
|
-
{resultLabel != null ? (
|
|
191
|
-
<div className="ss-section ss-result-label" style={{ borderBottom: 0, paddingBottom: 0 }}><span className="l">{resultLabel}</span></div>
|
|
192
|
-
) : null}
|
|
193
|
-
|
|
194
|
-
{sections && sections.length ? sections.map((s, i) => (
|
|
195
|
-
<div key={i} className="ss-section">
|
|
196
|
-
{s.label != null ? <div className="ss-section-label">{s.label}</div> : null}
|
|
197
|
-
<ul className="ss-list">{s.items.map(renderItem)}</ul>
|
|
198
|
-
</div>
|
|
199
|
-
)) : null}
|
|
200
|
-
|
|
201
|
-
{hint != null ? (
|
|
202
|
-
<div className="ss-section"><div className="ss-hint">{hint}</div></div>
|
|
203
|
-
) : null}
|
|
204
|
-
|
|
205
|
-
{typeButtons && typeButtons.length ? (
|
|
206
|
-
<div className="ss-section">
|
|
207
|
-
<div className="ss-typebtns">
|
|
208
|
-
{typeButtons.map((b) => (
|
|
209
|
-
<button key={b.key} type="button" className="ss-typebtn" onClick={() => onTypeSelect && onTypeSelect(b.key)}>
|
|
210
|
-
{b.icon}
|
|
211
|
-
<span className="grow">{b.label}</span>
|
|
212
|
-
{b.hint != null ? <span style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--fs-xxs)', color: 'var(--color-foreground-muted)' }}>{b.hint}</span> : null}
|
|
213
|
-
</button>
|
|
214
|
-
))}
|
|
215
|
-
</div>
|
|
216
|
-
</div>
|
|
217
|
-
) : null}
|
|
218
|
-
|
|
219
|
-
{empty != null && !loading && !error ? (
|
|
220
|
-
<div className="ss-section">
|
|
221
|
-
<div className="ss-illo">
|
|
222
|
-
{empty.illo}
|
|
223
|
-
<span className="title">{empty.title}</span>
|
|
224
|
-
{empty.sub != null ? <span className="body">{empty.sub}</span> : null}
|
|
225
|
-
</div>
|
|
226
|
-
</div>
|
|
227
|
-
) : null}
|
|
228
|
-
|
|
229
|
-
{loading ? (
|
|
230
|
-
<div className="ss-skeleton" aria-busy="true" aria-hidden="true">
|
|
231
|
-
{[0, 1, 2].map((i) => (
|
|
232
|
-
<div key={i} className="ss-skel-row">
|
|
233
|
-
<span className="ss-skel-circle ss-skel-shimmer" />
|
|
234
|
-
<span className="ss-skel-bars">
|
|
235
|
-
<span className="ss-skel-bar long ss-skel-shimmer" />
|
|
236
|
-
<span className="ss-skel-bar short ss-skel-shimmer" />
|
|
237
|
-
</span>
|
|
238
|
-
</div>
|
|
239
|
-
))}
|
|
240
|
-
</div>
|
|
241
|
-
) : null}
|
|
242
|
-
|
|
243
|
-
{error != null ? (
|
|
244
|
-
<div className="ss-section">
|
|
245
|
-
<div className="ss-illo" role="alert">
|
|
246
|
-
{error.illo}
|
|
247
|
-
<span className="title">{error.title}</span>
|
|
248
|
-
{error.sub != null ? <span className="body">{error.sub}</span> : null}
|
|
249
|
-
{error.onRetry ? (
|
|
250
|
-
<button type="button" className="ss-typebtn" style={{ marginTop: 'var(--space-2)' }} onClick={error.onRetry}>
|
|
251
|
-
{error.retryLabel != null ? error.retryLabel : 'Try again'}
|
|
252
|
-
</button>
|
|
253
|
-
) : null}
|
|
254
|
-
</div>
|
|
255
|
-
</div>
|
|
256
|
-
) : null}
|
|
257
|
-
|
|
258
|
-
{showFooter ? (
|
|
259
|
-
<div className="ss-foot">
|
|
260
|
-
<span className="kbd-group"><kbd className="ss-kbd">↑</kbd><kbd className="ss-kbd">↓</kbd> navigate</span>
|
|
261
|
-
<span className="sep" />
|
|
262
|
-
<span className="kbd-group"><kbd className="ss-kbd">↵</kbd> open</span>
|
|
263
|
-
<span className="sep" />
|
|
264
|
-
<span className="kbd-group"><kbd className="ss-kbd">esc</kbd> close</span>
|
|
265
|
-
</div>
|
|
266
|
-
) : null}
|
|
267
|
-
</div>
|
|
268
|
-
);
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
export default SmartSearch;
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ATTRUS SmartSearch — typed shell over the canonical `.ss` system
|
|
5
|
+
* (preview/components/smart-search.css). The global ⌘K command palette.
|
|
6
|
+
*
|
|
7
|
+
* The 8 canonical states (combobox.html §03) COMPOSE from props — the
|
|
8
|
+
* component renders whatever slices it receives, in the doc's order:
|
|
9
|
+
* input → chips → sections/list → hint → typeButtons → empty/loading/
|
|
10
|
+
* error illo → footer (kbd hints).
|
|
11
|
+
*
|
|
12
|
+
* inactive → no popover content (flat input)
|
|
13
|
+
* active → chips + sections (recents/suggestions)
|
|
14
|
+
* word → resultLabel + sections
|
|
15
|
+
* similar → hint + sections
|
|
16
|
+
* id → hint + typeButtons
|
|
17
|
+
* partial id → hint only
|
|
18
|
+
* no results → empty
|
|
19
|
+
* loading → loading
|
|
20
|
+
* error → error (retry)
|
|
21
|
+
*
|
|
22
|
+
* Keyboard: ↑/↓ move `.is-active` across items, Enter selects, Esc → onClose.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export interface SSItem {
|
|
26
|
+
key: string;
|
|
27
|
+
/** Leading 28px tile glyph. */
|
|
28
|
+
icon?: React.ReactNode;
|
|
29
|
+
title: React.ReactNode;
|
|
30
|
+
/** Muted second line. */
|
|
31
|
+
sub?: React.ReactNode;
|
|
32
|
+
/** Right-aligned mono hint (id, shortcut). */
|
|
33
|
+
trailing?: React.ReactNode;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SSSection {
|
|
37
|
+
/** Uppercase section label (e.g. "Counterparties"). */
|
|
38
|
+
label?: React.ReactNode;
|
|
39
|
+
items: SSItem[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SSTypeButton {
|
|
43
|
+
key: string;
|
|
44
|
+
icon?: React.ReactNode;
|
|
45
|
+
label: React.ReactNode;
|
|
46
|
+
/** Muted right hint (e.g. "transaction"). */
|
|
47
|
+
hint?: React.ReactNode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SmartSearchProps {
|
|
51
|
+
value: string;
|
|
52
|
+
onChange?: (value: string) => void;
|
|
53
|
+
placeholder?: string;
|
|
54
|
+
/** Leading input glyph (search icon). */
|
|
55
|
+
icon?: React.ReactNode;
|
|
56
|
+
/** Shows the × clear button when value is non-empty. */
|
|
57
|
+
onClear?: () => void;
|
|
58
|
+
/** Esc inside the input. */
|
|
59
|
+
onClose?: () => void;
|
|
60
|
+
/** Suggestion chips row (active state). */
|
|
61
|
+
chips?: Array<{ key: string; icon?: React.ReactNode; label: React.ReactNode }>;
|
|
62
|
+
onChipClick?: (key: string) => void;
|
|
63
|
+
/** Grouped result rows. */
|
|
64
|
+
sections?: SSSection[];
|
|
65
|
+
onSelect?: (key: string) => void;
|
|
66
|
+
/** "RESULTS (N)" line above the list. */
|
|
67
|
+
resultLabel?: React.ReactNode;
|
|
68
|
+
/** Tip card (similar match, ID format hint). */
|
|
69
|
+
hint?: React.ReactNode;
|
|
70
|
+
/** ID-disambiguation buttons. */
|
|
71
|
+
typeButtons?: SSTypeButton[];
|
|
72
|
+
onTypeSelect?: (key: string) => void;
|
|
73
|
+
/** No-results state. */
|
|
74
|
+
empty?: { title: React.ReactNode; sub?: React.ReactNode; illo?: React.ReactNode };
|
|
75
|
+
/** Skeleton rows while fetching. */
|
|
76
|
+
loading?: boolean;
|
|
77
|
+
/** Fetch failed — retry. */
|
|
78
|
+
error?: { title: React.ReactNode; sub?: React.ReactNode; retryLabel?: React.ReactNode; onRetry?: () => void; illo?: React.ReactNode };
|
|
79
|
+
/** Kbd hint footer (↑↓ navigate · ↵ open · esc close). Default true when the popover has content. */
|
|
80
|
+
footer?: boolean;
|
|
81
|
+
className?: string;
|
|
82
|
+
style?: React.CSSProperties;
|
|
83
|
+
'aria-label'?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const SearchGlyph: React.FC = () => (
|
|
87
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
88
|
+
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" />
|
|
89
|
+
</svg>
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
export const SmartSearch: React.FC<SmartSearchProps> = ({
|
|
93
|
+
value,
|
|
94
|
+
onChange,
|
|
95
|
+
placeholder = 'Search or jump to…',
|
|
96
|
+
icon,
|
|
97
|
+
onClear,
|
|
98
|
+
onClose,
|
|
99
|
+
chips,
|
|
100
|
+
onChipClick,
|
|
101
|
+
sections,
|
|
102
|
+
onSelect,
|
|
103
|
+
resultLabel,
|
|
104
|
+
hint,
|
|
105
|
+
typeButtons,
|
|
106
|
+
onTypeSelect,
|
|
107
|
+
empty,
|
|
108
|
+
loading = false,
|
|
109
|
+
error,
|
|
110
|
+
footer,
|
|
111
|
+
className,
|
|
112
|
+
style,
|
|
113
|
+
...rest
|
|
114
|
+
}) => {
|
|
115
|
+
const [active, setActive] = React.useState<string | null>(null);
|
|
116
|
+
const flat = React.useMemo(() => (sections || []).reduce<SSItem[]>((a, s) => a.concat(s.items), []), [sections]);
|
|
117
|
+
|
|
118
|
+
const hasBody = Boolean(
|
|
119
|
+
(chips && chips.length) || (sections && sections.length) || hint != null ||
|
|
120
|
+
(typeButtons && typeButtons.length) || empty != null || loading || error != null
|
|
121
|
+
);
|
|
122
|
+
const showFooter = footer != null ? footer : hasBody;
|
|
123
|
+
|
|
124
|
+
const onKeyDown = (e: React.KeyboardEvent) => {
|
|
125
|
+
if (e.key === 'Escape') { if (onClose) onClose(); return; }
|
|
126
|
+
if (!flat.length) return;
|
|
127
|
+
const idx = flat.findIndex((i) => i.key === active);
|
|
128
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
const next = e.key === 'ArrowDown'
|
|
131
|
+
? flat[Math.min(idx + 1, flat.length - 1)]
|
|
132
|
+
: flat[Math.max(idx - 1, 0)];
|
|
133
|
+
setActive(next.key);
|
|
134
|
+
} else if (e.key === 'Enter') {
|
|
135
|
+
const hit = flat.find((i) => i.key === active);
|
|
136
|
+
if (hit && onSelect) { e.preventDefault(); onSelect(hit.key); }
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const renderItem = (it: SSItem) => (
|
|
141
|
+
<li
|
|
142
|
+
key={it.key}
|
|
143
|
+
className={['ss-list-item', it.key === active ? 'is-active' : ''].filter(Boolean).join(' ')}
|
|
144
|
+
onMouseEnter={() => setActive(it.key)}
|
|
145
|
+
onClick={() => onSelect && onSelect(it.key)}
|
|
146
|
+
>
|
|
147
|
+
{it.icon != null ? <span className="icon-leading">{it.icon}</span> : null}
|
|
148
|
+
<span className="text">
|
|
149
|
+
<span className="label">{it.title}</span>
|
|
150
|
+
{it.sub != null ? <span className="meta">{it.sub}</span> : null}
|
|
151
|
+
</span>
|
|
152
|
+
{it.trailing != null ? (
|
|
153
|
+
<span style={{ marginLeft: 'auto', flexShrink: 0, fontFamily: 'var(--font-mono)', fontSize: 'var(--fs-xxs)', color: 'var(--color-foreground-muted)' }}>{it.trailing}</span>
|
|
154
|
+
) : null}
|
|
155
|
+
</li>
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div className={['ss', className || ''].filter(Boolean).join(' ')} style={style} role="search" aria-label={rest['aria-label'] || 'Smart search'}>
|
|
160
|
+
<div className={['ss-input', hasBody ? '' : 'ss-input-flat'].filter(Boolean).join(' ')}>
|
|
161
|
+
{icon != null ? icon : <SearchGlyph />}
|
|
162
|
+
<input
|
|
163
|
+
value={value}
|
|
164
|
+
placeholder={placeholder}
|
|
165
|
+
onChange={(e) => onChange && onChange(e.target.value)}
|
|
166
|
+
onKeyDown={onKeyDown}
|
|
167
|
+
role="combobox"
|
|
168
|
+
aria-expanded={hasBody}
|
|
169
|
+
/>
|
|
170
|
+
{onClear && value ? (
|
|
171
|
+
<button type="button" className="ss-clear" aria-label="Clear" onClick={onClear}>
|
|
172
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.25} strokeLinecap="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>
|
|
173
|
+
</button>
|
|
174
|
+
) : null}
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
{chips && chips.length ? (
|
|
178
|
+
<div className="ss-section">
|
|
179
|
+
<div className="ss-chips">
|
|
180
|
+
{chips.map((c) => (
|
|
181
|
+
<button key={c.key} type="button" className="ss-chip" onClick={() => onChipClick && onChipClick(c.key)}>
|
|
182
|
+
{c.icon}
|
|
183
|
+
{c.label}
|
|
184
|
+
</button>
|
|
185
|
+
))}
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
) : null}
|
|
189
|
+
|
|
190
|
+
{resultLabel != null ? (
|
|
191
|
+
<div className="ss-section ss-result-label" style={{ borderBottom: 0, paddingBottom: 0 }}><span className="l">{resultLabel}</span></div>
|
|
192
|
+
) : null}
|
|
193
|
+
|
|
194
|
+
{sections && sections.length ? sections.map((s, i) => (
|
|
195
|
+
<div key={i} className="ss-section">
|
|
196
|
+
{s.label != null ? <div className="ss-section-label">{s.label}</div> : null}
|
|
197
|
+
<ul className="ss-list">{s.items.map(renderItem)}</ul>
|
|
198
|
+
</div>
|
|
199
|
+
)) : null}
|
|
200
|
+
|
|
201
|
+
{hint != null ? (
|
|
202
|
+
<div className="ss-section"><div className="ss-hint">{hint}</div></div>
|
|
203
|
+
) : null}
|
|
204
|
+
|
|
205
|
+
{typeButtons && typeButtons.length ? (
|
|
206
|
+
<div className="ss-section">
|
|
207
|
+
<div className="ss-typebtns">
|
|
208
|
+
{typeButtons.map((b) => (
|
|
209
|
+
<button key={b.key} type="button" className="ss-typebtn" onClick={() => onTypeSelect && onTypeSelect(b.key)}>
|
|
210
|
+
{b.icon}
|
|
211
|
+
<span className="grow">{b.label}</span>
|
|
212
|
+
{b.hint != null ? <span style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--fs-xxs)', color: 'var(--color-foreground-muted)' }}>{b.hint}</span> : null}
|
|
213
|
+
</button>
|
|
214
|
+
))}
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
) : null}
|
|
218
|
+
|
|
219
|
+
{empty != null && !loading && !error ? (
|
|
220
|
+
<div className="ss-section">
|
|
221
|
+
<div className="ss-illo">
|
|
222
|
+
{empty.illo}
|
|
223
|
+
<span className="title">{empty.title}</span>
|
|
224
|
+
{empty.sub != null ? <span className="body">{empty.sub}</span> : null}
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
) : null}
|
|
228
|
+
|
|
229
|
+
{loading ? (
|
|
230
|
+
<div className="ss-skeleton" aria-busy="true" aria-hidden="true">
|
|
231
|
+
{[0, 1, 2].map((i) => (
|
|
232
|
+
<div key={i} className="ss-skel-row">
|
|
233
|
+
<span className="ss-skel-circle ss-skel-shimmer" />
|
|
234
|
+
<span className="ss-skel-bars">
|
|
235
|
+
<span className="ss-skel-bar long ss-skel-shimmer" />
|
|
236
|
+
<span className="ss-skel-bar short ss-skel-shimmer" />
|
|
237
|
+
</span>
|
|
238
|
+
</div>
|
|
239
|
+
))}
|
|
240
|
+
</div>
|
|
241
|
+
) : null}
|
|
242
|
+
|
|
243
|
+
{error != null ? (
|
|
244
|
+
<div className="ss-section">
|
|
245
|
+
<div className="ss-illo" role="alert">
|
|
246
|
+
{error.illo}
|
|
247
|
+
<span className="title">{error.title}</span>
|
|
248
|
+
{error.sub != null ? <span className="body">{error.sub}</span> : null}
|
|
249
|
+
{error.onRetry ? (
|
|
250
|
+
<button type="button" className="ss-typebtn" style={{ marginTop: 'var(--space-2)' }} onClick={error.onRetry}>
|
|
251
|
+
{error.retryLabel != null ? error.retryLabel : 'Try again'}
|
|
252
|
+
</button>
|
|
253
|
+
) : null}
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
) : null}
|
|
257
|
+
|
|
258
|
+
{showFooter ? (
|
|
259
|
+
<div className="ss-foot">
|
|
260
|
+
<span className="kbd-group"><kbd className="ss-kbd">↑</kbd><kbd className="ss-kbd">↓</kbd> navigate</span>
|
|
261
|
+
<span className="sep" />
|
|
262
|
+
<span className="kbd-group"><kbd className="ss-kbd">↵</kbd> open</span>
|
|
263
|
+
<span className="sep" />
|
|
264
|
+
<span className="kbd-group"><kbd className="ss-kbd">esc</kbd> close</span>
|
|
265
|
+
</div>
|
|
266
|
+
) : null}
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export default SmartSearch;
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
|
|
3
|
-
export type StepState = 'todo' | 'current' | 'done' | 'error' | 'skipped';
|
|
4
|
-
|
|
5
|
-
export interface StepItem {
|
|
6
|
-
key: string;
|
|
7
|
-
label: React.ReactNode;
|
|
8
|
-
help?: React.ReactNode;
|
|
9
|
-
state?: StepState;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface StepperProps extends React.OlHTMLAttributes<HTMLOListElement> {
|
|
13
|
-
steps: StepItem[];
|
|
14
|
-
vertical?: boolean;
|
|
15
|
-
compact?: boolean;
|
|
16
|
-
/** Minimal capsule+label variant (.is-rule) for tight headers. */
|
|
17
|
-
rule?: boolean;
|
|
18
|
-
/** ≤640px: hide labels, keep markers (.is-collapse-sm). */
|
|
19
|
-
collapseOnMobile?: boolean;
|
|
20
|
-
/** Step click (e.g. jump back to a done step). */
|
|
21
|
-
onStepClick?: (key: string) => void;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export declare const Stepper: React.FC<StepperProps>;
|
|
25
|
-
export default Stepper;
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
export type StepState = 'todo' | 'current' | 'done' | 'error' | 'skipped';
|
|
4
|
+
|
|
5
|
+
export interface StepItem {
|
|
6
|
+
key: string;
|
|
7
|
+
label: React.ReactNode;
|
|
8
|
+
help?: React.ReactNode;
|
|
9
|
+
state?: StepState;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface StepperProps extends React.OlHTMLAttributes<HTMLOListElement> {
|
|
13
|
+
steps: StepItem[];
|
|
14
|
+
vertical?: boolean;
|
|
15
|
+
compact?: boolean;
|
|
16
|
+
/** Minimal capsule+label variant (.is-rule) for tight headers. */
|
|
17
|
+
rule?: boolean;
|
|
18
|
+
/** ≤640px: hide labels, keep markers (.is-collapse-sm). */
|
|
19
|
+
collapseOnMobile?: boolean;
|
|
20
|
+
/** Step click (e.g. jump back to a done step). */
|
|
21
|
+
onStepClick?: (key: string) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export declare const Stepper: React.FC<StepperProps>;
|
|
25
|
+
export default Stepper;
|