@attrus-ds/core 0.1.1 → 0.2.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.
Files changed (55) hide show
  1. package/README.md +63 -9
  2. package/dist/css/components/buttons.css +1 -1
  3. package/dist/css/components/combobox.css +17 -1
  4. package/dist/css/components/inputs.css +3 -55
  5. package/dist/css/components/overlays.css +28 -59
  6. package/dist/css/components/primitives.css +10 -1
  7. package/dist/css/components/sidebar.css +3 -3
  8. package/dist/css/components/smart-search.css +91 -0
  9. package/dist/css/components/table.css +10 -8
  10. package/dist/css/components/tabs.css +2 -2
  11. package/dist/css/components/toast.css +4 -0
  12. package/dist/css/components/tooltip.css +5 -0
  13. package/dist/css/index.css +1 -0
  14. package/dist/react/AppSidebar/AppSidebar.d.ts +50 -0
  15. package/dist/react/AppSidebar/AppSidebar.tsx +197 -0
  16. package/dist/react/AppTopbar/AppTopbar.d.ts +58 -0
  17. package/dist/react/AppTopbar/AppTopbar.tsx +203 -0
  18. package/dist/react/Atoms/Atoms.d.ts +149 -0
  19. package/dist/react/Atoms/Atoms.tsx +407 -0
  20. package/dist/react/Button/Button.d.ts +25 -0
  21. package/dist/react/Button/Button.tsx +83 -0
  22. package/dist/react/Callout/Callout.d.ts +29 -0
  23. package/dist/react/Callout/Callout.tsx +98 -0
  24. package/dist/react/Card/Card.d.ts +38 -0
  25. package/dist/react/Card/Card.tsx +93 -0
  26. package/dist/react/Combobox/Combobox.d.ts +52 -0
  27. package/dist/react/Combobox/Combobox.tsx +254 -0
  28. package/dist/react/DataTable/DataTable.d.ts +81 -0
  29. package/dist/react/DataTable/DataTable.tsx +308 -0
  30. package/dist/react/Drawer/Drawer.d.ts +35 -0
  31. package/dist/react/Drawer/Drawer.tsx +226 -0
  32. package/dist/react/Input/Input.d.ts +155 -0
  33. package/dist/react/Input/Input.tsx +478 -0
  34. package/dist/react/Modal/Modal.d.ts +27 -0
  35. package/dist/react/Modal/Modal.tsx +173 -0
  36. package/dist/react/NumberInput/NumberInput.d.ts +25 -0
  37. package/dist/react/NumberInput/NumberInput.tsx +117 -0
  38. package/dist/react/Pill/Pill.d.ts +31 -0
  39. package/dist/react/Pill/Pill.tsx +104 -0
  40. package/dist/react/SearchPill/SearchPill.d.ts +14 -0
  41. package/dist/react/SearchPill/SearchPill.tsx +40 -0
  42. package/dist/react/SmartSearch/SmartSearch.d.ts +65 -0
  43. package/dist/react/SmartSearch/SmartSearch.tsx +271 -0
  44. package/dist/react/Stepper/Stepper.d.ts +25 -0
  45. package/dist/react/Stepper/Stepper.tsx +94 -0
  46. package/dist/react/Tabs/Tabs.d.ts +32 -0
  47. package/dist/react/Tabs/Tabs.tsx +147 -0
  48. package/dist/react/Toast/Toast.d.ts +30 -0
  49. package/dist/react/Toast/Toast.tsx +94 -0
  50. package/dist/react/Tooltip/Tooltip.d.ts +23 -0
  51. package/dist/react/Tooltip/Tooltip.tsx +80 -0
  52. package/dist/react/ZeroState/ZeroState.d.ts +17 -0
  53. package/dist/react/ZeroState/ZeroState.tsx +45 -0
  54. package/dist/react/index.ts +35 -0
  55. package/package.json +12 -2
@@ -0,0 +1,173 @@
1
+ import * as React from 'react';
2
+
3
+ /**
4
+ * ATTRUS Modal — typed wrapper over the canonical `.modal` + `.scrim`
5
+ * (preview/components/overlays.css). Centered dialog with header/body/
6
+ * footer, sizes sm→xl and the destructive variant (top accent strip).
7
+ *
8
+ * Open/close: mount with open=true; an internal flag flips `.is-open`
9
+ * a tick later via setTimeout (NOT requestAnimationFrame — rAF is
10
+ * throttled in background tabs and the overlay would stay invisible).
11
+ * Esc and scrim-click call onClose. Exit: on open=false the dialog stays
12
+ * mounted for the exit transition (fade + slide, --duration-base = 200ms)
13
+ * and only then unmounts — closing mirrors opening.
14
+ */
15
+
16
+ export type ModalSize = 'sm' | 'md' | 'lg' | 'xl';
17
+
18
+ export interface ModalProps {
19
+ open: boolean;
20
+ onClose?: () => void;
21
+ size?: ModalSize;
22
+ title?: React.ReactNode;
23
+ subtitle?: React.ReactNode;
24
+ /** Icon tile above the title (destructive confirmations). */
25
+ icon?: React.ReactNode;
26
+ /** Danger accent strip + icon tint (.modal-destructive). */
27
+ destructive?: boolean;
28
+ /** Footer row (.modal-footer) — canonical Buttons, right-aligned. */
29
+ footer?: React.ReactNode;
30
+ /** Left-side footer aux content (enables space-between). */
31
+ footerAux?: React.ReactNode;
32
+ /** Gallery/doc rendering: panel only (no scrim), overlay behaviours off
33
+ (focus trap, scroll-lock, Esc). Position it inside a relative frame. */
34
+ static?: boolean;
35
+ children?: React.ReactNode;
36
+ className?: string;
37
+ }
38
+
39
+ const XGlyph: React.FC = () => (
40
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" aria-hidden="true">
41
+ <line x1="18" y1="6" x2="6" y2="18" />
42
+ <line x1="6" y1="6" x2="18" y2="18" />
43
+ </svg>
44
+ );
45
+
46
+ export const Modal: React.FC<ModalProps> = ({
47
+ open,
48
+ onClose,
49
+ size = 'md',
50
+ title,
51
+ subtitle,
52
+ icon,
53
+ destructive = false,
54
+ footer,
55
+ footerAux,
56
+ static: isStatic = false,
57
+ children,
58
+ className,
59
+ }) => {
60
+ const [shown, setShown] = React.useState(false);
61
+ const [mounted, setMounted] = React.useState(open);
62
+ const panelRef = React.useRef<HTMLDivElement>(null);
63
+
64
+ React.useEffect(() => {
65
+ if (open) {
66
+ setMounted(true);
67
+ return undefined;
68
+ }
69
+ // Closing: drop .is-open → CSS exit transition plays → unmount after.
70
+ setShown(false);
71
+ const t = setTimeout(() => setMounted(false), 200);
72
+ return () => clearTimeout(t);
73
+ }, [open]);
74
+
75
+ // Entry: commit the CLOSED state, force a layout flush, then flip
76
+ // .is-open — the fade+slide entry plays deterministically (a bare
77
+ // 10ms timer raced the first style calc and often skipped it).
78
+ React.useEffect(() => {
79
+ if (!open || !mounted || shown) return;
80
+ void panelRef.current?.offsetHeight;
81
+ setShown(true);
82
+ }, [open, mounted, shown]);
83
+
84
+ React.useEffect(() => {
85
+ if (!open || isStatic) return undefined;
86
+ const onKey = (e: KeyboardEvent) => {
87
+ if (e.key === 'Escape' && onClose) onClose();
88
+ };
89
+ document.addEventListener('keydown', onKey);
90
+ return () => document.removeEventListener('keydown', onKey);
91
+ }, [open, onClose, isStatic]);
92
+
93
+ // Body scroll-lock while open (doc a11y contract).
94
+ React.useEffect(() => {
95
+ if (!open || !mounted || isStatic) return undefined;
96
+ const prev = document.body.style.overflow;
97
+ document.body.style.overflow = 'hidden';
98
+ return () => { document.body.style.overflow = prev; };
99
+ }, [open, mounted, isStatic]);
100
+
101
+ // Focus: into the dialog on open, restore the opener on close; Tab cycles.
102
+ const prevFocus = React.useRef<HTMLElement | null>(null);
103
+ React.useEffect(() => {
104
+ if (isStatic) return;
105
+ if (shown) {
106
+ prevFocus.current = document.activeElement as HTMLElement | null;
107
+ const first = panelRef.current?.querySelector<HTMLElement>(
108
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
109
+ );
110
+ (first || panelRef.current)?.focus?.();
111
+ } else if (prevFocus.current) {
112
+ prevFocus.current.focus?.();
113
+ prevFocus.current = null;
114
+ }
115
+ }, [shown]);
116
+
117
+ const onTrapKeyDown = (e: React.KeyboardEvent) => {
118
+ if (e.key !== 'Tab' || !panelRef.current) return;
119
+ const focusables = Array.from(
120
+ panelRef.current.querySelectorAll<HTMLElement>(
121
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
122
+ )
123
+ ).filter((el) => !el.hasAttribute('disabled'));
124
+ if (!focusables.length) return;
125
+ const first = focusables[0];
126
+ const last = focusables[focusables.length - 1];
127
+ if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
128
+ else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
129
+ };
130
+
131
+ if (!mounted) return null;
132
+
133
+ const cls = [
134
+ 'modal',
135
+ `modal-${size}`,
136
+ destructive ? 'modal-destructive' : '',
137
+ shown ? 'is-open' : '',
138
+ className || '',
139
+ ]
140
+ .filter(Boolean)
141
+ .join(' ');
142
+
143
+ return (
144
+ <React.Fragment>
145
+ {isStatic ? null : <div className={['scrim', shown ? 'is-open' : ''].filter(Boolean).join(' ')} onClick={onClose} />}
146
+ <div ref={panelRef} className={cls} role="dialog" aria-modal="true" tabIndex={-1} onKeyDown={onTrapKeyDown}>
147
+ {title != null || subtitle != null || onClose ? (
148
+ <div className="modal-header">
149
+ <div className="modal-header-text">
150
+ {icon != null ? <span className="modal-icon">{icon}</span> : null}
151
+ {title != null ? <div className="modal-title">{title}</div> : null}
152
+ {subtitle != null ? <div className="modal-subtitle">{subtitle}</div> : null}
153
+ </div>
154
+ {onClose ? (
155
+ <button type="button" className="modal-close" aria-label="Close" onClick={onClose}>
156
+ <XGlyph />
157
+ </button>
158
+ ) : null}
159
+ </div>
160
+ ) : null}
161
+ {children != null ? <div className="modal-body">{children}</div> : null}
162
+ {footer != null ? (
163
+ <div className={['modal-footer', footerAux != null ? 'modal-footer-spread' : ''].filter(Boolean).join(' ')}>
164
+ {footerAux != null ? <div className="modal-footer-aux">{footerAux}</div> : null}
165
+ {footer}
166
+ </div>
167
+ ) : null}
168
+ </div>
169
+ </React.Fragment>
170
+ );
171
+ };
172
+
173
+ export default Modal;
@@ -0,0 +1,25 @@
1
+ import * as React from 'react';
2
+
3
+ export type NumberInputSize = 'sm' | 'md' | 'lg';
4
+
5
+ export interface NumberInputProps {
6
+ value: number;
7
+ onChange?: (value: number) => void;
8
+ min?: number;
9
+ max?: number;
10
+ step?: number;
11
+ size?: NumberInputSize;
12
+ /** Static cell before the field (e.g. "$"). */
13
+ prefix?: React.ReactNode;
14
+ /** Static cell after the field (e.g. "%", "days"). */
15
+ suffix?: React.ReactNode;
16
+ disabled?: boolean;
17
+ invalid?: boolean;
18
+ 'aria-label'?: string;
19
+ id?: string;
20
+ className?: string;
21
+ style?: React.CSSProperties;
22
+ }
23
+
24
+ export declare const NumberInput: React.FC<NumberInputProps>;
25
+ export default NumberInput;
@@ -0,0 +1,117 @@
1
+ import * as React from 'react';
2
+
3
+ /**
4
+ * ATTRUS NumberInput — typed wrapper over the canonical `.number-input`
5
+ * (preview/components/number-input.css): − / + accelerator buttons
6
+ * around a numeric field. For bounded quantities (counts, %, days).
7
+ */
8
+
9
+ export type NumberInputSize = 'sm' | 'md' | 'lg';
10
+
11
+ export interface NumberInputProps {
12
+ value: number;
13
+ onChange?: (value: number) => void;
14
+ min?: number;
15
+ max?: number;
16
+ step?: number;
17
+ size?: NumberInputSize;
18
+ /** Static cell before the field (e.g. "$"). */
19
+ prefix?: React.ReactNode;
20
+ /** Static cell after the field (e.g. "%", "days"). */
21
+ suffix?: React.ReactNode;
22
+ disabled?: boolean;
23
+ invalid?: boolean;
24
+ 'aria-label'?: string;
25
+ id?: string;
26
+ className?: string;
27
+ style?: React.CSSProperties;
28
+ }
29
+
30
+ const Minus: React.FC = () => (
31
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.4} strokeLinecap="round" aria-hidden="true">
32
+ <line x1="5" y1="12" x2="19" y2="12" />
33
+ </svg>
34
+ );
35
+ const Plus: React.FC = () => (
36
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.4} strokeLinecap="round" aria-hidden="true">
37
+ <line x1="12" y1="5" x2="12" y2="19" />
38
+ <line x1="5" y1="12" x2="19" y2="12" />
39
+ </svg>
40
+ );
41
+
42
+ function clamp(v: number, min?: number, max?: number): number {
43
+ if (min != null && v < min) return min;
44
+ if (max != null && v > max) return max;
45
+ return v;
46
+ }
47
+
48
+ export const NumberInput: React.FC<NumberInputProps> = ({
49
+ value,
50
+ onChange,
51
+ min,
52
+ max,
53
+ step = 1,
54
+ size = 'md',
55
+ prefix,
56
+ suffix,
57
+ disabled = false,
58
+ invalid = false,
59
+ className,
60
+ style,
61
+ ...rest
62
+ }) => {
63
+ const cls = [
64
+ 'number-input',
65
+ size !== 'md' ? `number-input-${size}` : '',
66
+ disabled ? 'is-disabled' : '',
67
+ invalid ? 'is-invalid' : '',
68
+ className || '',
69
+ ]
70
+ .filter(Boolean)
71
+ .join(' ');
72
+
73
+ const set = (v: number) => {
74
+ if (!disabled && onChange) onChange(clamp(v, min, max));
75
+ };
76
+
77
+ return (
78
+ <div className={cls} style={style}>
79
+ <button
80
+ type="button"
81
+ className="number-input-btn"
82
+ aria-label="Decrease"
83
+ disabled={disabled || (min != null && value <= min)}
84
+ onClick={() => set(value - step)}
85
+ >
86
+ <Minus />
87
+ </button>
88
+ {prefix != null ? <span className="number-input-prefix">{prefix}</span> : null}
89
+ <input
90
+ type="number"
91
+ className="number-input-field"
92
+ value={value}
93
+ min={min}
94
+ max={max}
95
+ step={step}
96
+ disabled={disabled}
97
+ onChange={(e) => {
98
+ const v = parseFloat(e.target.value);
99
+ if (!isNaN(v)) set(v);
100
+ }}
101
+ {...rest}
102
+ />
103
+ {suffix != null ? <span className="number-input-suffix">{suffix}</span> : null}
104
+ <button
105
+ type="button"
106
+ className="number-input-btn"
107
+ aria-label="Increase"
108
+ disabled={disabled || (max != null && value >= max)}
109
+ onClick={() => set(value + step)}
110
+ >
111
+ <Plus />
112
+ </button>
113
+ </div>
114
+ );
115
+ };
116
+
117
+ export default NumberInput;
@@ -0,0 +1,31 @@
1
+ import * as React from 'react';
2
+
3
+ export type PillTone = 'success' | 'warning' | 'danger' | 'info' | 'neutral' | 'brand';
4
+ export type PillSize = 'sm' | 'md' | 'lg';
5
+ export type PillAppearance = 'soft' | 'solid' | 'outline';
6
+
7
+ export interface PillProps extends React.HTMLAttributes<HTMLElement> {
8
+ /** Semantic tone. Variant carries meaning; label is required. Default: 'neutral'. */
9
+ tone?: PillTone;
10
+ /** sm=20px, md=24px (default), lg=28px. */
11
+ size?: PillSize;
12
+ /** soft (tinted, default) | solid (filled) | outline (transparent bg). */
13
+ appearance?: PillAppearance;
14
+ /** Leading status dot (decorative — meaning lives in label + tone). */
15
+ dot?: boolean;
16
+ /** Leading icon slot. */
17
+ icon?: React.ReactNode;
18
+ /** Renders a <button> with hover/focus/selected states (filter chips). */
19
+ interactive?: boolean;
20
+ /** Selected state for interactive pills (sets aria-pressed). */
21
+ selected?: boolean;
22
+ /** Glassy translucent treatment for dark surfaces. */
23
+ onInverse?: boolean;
24
+ /** Renders the trailing close (×) — `.pill-x` — and calls back on click (removable filter chips). */
25
+ onRemove?: (e: React.MouseEvent) => void;
26
+ /** Dimmed, non-interactive. */
27
+ disabled?: boolean;
28
+ }
29
+
30
+ export declare const Pill: React.FC<PillProps>;
31
+ export default Pill;
@@ -0,0 +1,104 @@
1
+ import * as React from 'react';
2
+
3
+ /**
4
+ * ATTRUS Pill — typed wrapper over the canonical `.pill` system
5
+ * (preview/components/status-pills.css). CSS stays the visual source
6
+ * of truth; this component only composes class names.
7
+ */
8
+
9
+ export type PillTone = 'success' | 'warning' | 'danger' | 'info' | 'neutral' | 'brand';
10
+ export type PillSize = 'sm' | 'md' | 'lg';
11
+ export type PillAppearance = 'soft' | 'solid' | 'outline';
12
+
13
+ export interface PillProps extends React.HTMLAttributes<HTMLElement> {
14
+ /** Semantic tone. Variant carries meaning; label is required. Default: 'neutral'. */
15
+ tone?: PillTone;
16
+ /** sm=20px, md=24px (default), lg=28px. */
17
+ size?: PillSize;
18
+ /** soft (tinted, default) | solid (filled) | outline (transparent bg). */
19
+ appearance?: PillAppearance;
20
+ /** Leading status dot (decorative — meaning lives in label + tone). */
21
+ dot?: boolean;
22
+ /** Leading icon slot. */
23
+ icon?: React.ReactNode;
24
+ /** Renders a <button> with hover/focus/selected states (filter chips). */
25
+ interactive?: boolean;
26
+ /** Selected state for interactive pills (sets aria-pressed). */
27
+ selected?: boolean;
28
+ /** Glassy translucent treatment for dark surfaces. */
29
+ onInverse?: boolean;
30
+ /** Renders the trailing close (×) — `.pill-x` — and calls back on click (removable filter chips). */
31
+ onRemove?: (e: React.MouseEvent) => void;
32
+ /** Dimmed, non-interactive. */
33
+ disabled?: boolean;
34
+ }
35
+
36
+ export const Pill: React.FC<PillProps> = ({
37
+ tone = 'neutral',
38
+ size = 'md',
39
+ appearance = 'soft',
40
+ dot = false,
41
+ icon,
42
+ interactive = false,
43
+ selected = false,
44
+ onInverse = false,
45
+ onRemove,
46
+ disabled = false,
47
+ className,
48
+ children,
49
+ ...rest
50
+ }) => {
51
+ const cls = [
52
+ 'pill',
53
+ tone,
54
+ size !== 'md' ? size : '',
55
+ appearance !== 'soft' ? appearance : '',
56
+ interactive ? 'interactive' : '',
57
+ onInverse ? 'on-inverse' : '',
58
+ className || '',
59
+ ]
60
+ .filter(Boolean)
61
+ .join(' ');
62
+
63
+ const content = (
64
+ <React.Fragment>
65
+ {dot ? <span className="dot" /> : null}
66
+ {icon}
67
+ {children != null ? <span className="pill-label">{children}</span> : null}
68
+ {onRemove ? (
69
+ <span
70
+ className="pill-x"
71
+ role="button"
72
+ tabIndex={0}
73
+ aria-label="Remove"
74
+ onClick={(e) => { e.stopPropagation(); onRemove(e); }}
75
+ onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); onRemove(e as unknown as React.MouseEvent); } }}
76
+ >
77
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.25" strokeLinecap="round"><line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/></svg>
78
+ </span>
79
+ ) : null}
80
+ </React.Fragment>
81
+ );
82
+
83
+ if (interactive) {
84
+ return (
85
+ <button
86
+ type="button"
87
+ className={cls}
88
+ aria-pressed={selected}
89
+ disabled={disabled}
90
+ {...(rest as React.ButtonHTMLAttributes<HTMLButtonElement>)}
91
+ >
92
+ {content}
93
+ </button>
94
+ );
95
+ }
96
+
97
+ return (
98
+ <span className={cls} aria-disabled={disabled || undefined} {...rest}>
99
+ {content}
100
+ </span>
101
+ );
102
+ };
103
+
104
+ export default Pill;
@@ -0,0 +1,14 @@
1
+ import * as React from 'react';
2
+
3
+ export interface SearchPillProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
4
+ /** Keyboard hint rendered as a .kbd chip (e.g. "⌘K"). */
5
+ shortcut?: string;
6
+ /** Leading icon — defaults to the canonical search glyph. */
7
+ icon?: React.ReactNode;
8
+ /** Wrapper (pill) className/style — the input itself fills it. */
9
+ wrapperClassName?: string;
10
+ wrapperStyle?: React.CSSProperties;
11
+ }
12
+
13
+ export declare const SearchPill: React.FC<SearchPillProps>;
14
+ export default SearchPill;
@@ -0,0 +1,40 @@
1
+ import * as React from 'react';
2
+
3
+ /**
4
+ * ATTRUS SearchPill — typed wrapper over the canonical `.pill-search`
5
+ * (preview/components/inputs.css): leading icon + borderless input +
6
+ * optional .kbd shortcut hint. The ⌘K-style global search bar.
7
+ */
8
+
9
+ export interface SearchPillProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
10
+ /** Keyboard hint rendered as a .kbd chip (e.g. "⌘K"). */
11
+ shortcut?: string;
12
+ /** Leading icon — defaults to the canonical search glyph. */
13
+ icon?: React.ReactNode;
14
+ /** Wrapper (pill) className/style — the input itself fills it. */
15
+ wrapperClassName?: string;
16
+ wrapperStyle?: React.CSSProperties;
17
+ }
18
+
19
+ const SearchGlyph: React.FC = () => (
20
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
21
+ <circle cx="11" cy="11" r="8" />
22
+ <path d="m21 21-4.3-4.3" />
23
+ </svg>
24
+ );
25
+
26
+ export const SearchPill: React.FC<SearchPillProps> = ({
27
+ shortcut,
28
+ icon,
29
+ wrapperClassName,
30
+ wrapperStyle,
31
+ ...rest
32
+ }) => (
33
+ <div className={['pill-search', wrapperClassName || ''].filter(Boolean).join(' ')} style={wrapperStyle}>
34
+ {icon != null ? icon : <SearchGlyph />}
35
+ <input type="search" {...rest} />
36
+ {shortcut ? <span className="kbd">{shortcut}</span> : null}
37
+ </div>
38
+ );
39
+
40
+ export default SearchPill;
@@ -0,0 +1,65 @@
1
+ import * as React from 'react';
2
+
3
+ export interface SSItem {
4
+ key: string;
5
+ /** Leading 28px tile glyph (`.icon-leading`). */
6
+ icon?: React.ReactNode;
7
+ title: React.ReactNode;
8
+ /** Muted second line (`.text .meta`). */
9
+ sub?: React.ReactNode;
10
+ /** Right-aligned mono hint (id). */
11
+ trailing?: React.ReactNode;
12
+ }
13
+
14
+ export interface SSSection {
15
+ /** Uppercase section label (e.g. "Counterparties"). */
16
+ label?: React.ReactNode;
17
+ items: SSItem[];
18
+ }
19
+
20
+ export interface SSTypeButton {
21
+ key: string;
22
+ icon?: React.ReactNode;
23
+ label: React.ReactNode;
24
+ /** Muted right hint (e.g. "transaction"). */
25
+ hint?: React.ReactNode;
26
+ }
27
+
28
+ export interface SmartSearchProps {
29
+ value: string;
30
+ onChange?: (value: string) => void;
31
+ placeholder?: string;
32
+ /** Leading input glyph (search icon). */
33
+ icon?: React.ReactNode;
34
+ /** Shows the × clear button when value is non-empty. */
35
+ onClear?: () => void;
36
+ /** Esc inside the input. */
37
+ onClose?: () => void;
38
+ /** Suggestion chips row (active state — `.ss-chips`). */
39
+ chips?: Array<{ key: string; icon?: React.ReactNode; label: React.ReactNode }>;
40
+ onChipClick?: (key: string) => void;
41
+ /** Grouped result rows (`.ss-section` + `.ss-list`). */
42
+ sections?: SSSection[];
43
+ onSelect?: (key: string) => void;
44
+ /** "RESULTS (N)" line above the list (`.ss-result-label`). */
45
+ resultLabel?: React.ReactNode;
46
+ /** Tip card — similar match / ID format hint (`.ss-hint`). */
47
+ hint?: React.ReactNode;
48
+ /** ID-disambiguation buttons (`.ss-typebtns`). */
49
+ typeButtons?: SSTypeButton[];
50
+ onTypeSelect?: (key: string) => void;
51
+ /** No-results state (`.ss-illo`). */
52
+ empty?: { title: React.ReactNode; sub?: React.ReactNode; illo?: React.ReactNode };
53
+ /** Skeleton rows while fetching (`.ss-skeleton`). */
54
+ loading?: boolean;
55
+ /** Fetch failed — retry. */
56
+ error?: { title: React.ReactNode; sub?: React.ReactNode; retryLabel?: React.ReactNode; onRetry?: () => void; illo?: React.ReactNode };
57
+ /** Kbd hint footer (`.ss-foot`). Default: true when the popover has content. */
58
+ footer?: boolean;
59
+ className?: string;
60
+ style?: React.CSSProperties;
61
+ 'aria-label'?: string;
62
+ }
63
+
64
+ export declare const SmartSearch: React.FC<SmartSearchProps>;
65
+ export default SmartSearch;