@weave-design-system/react 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/dist/index.cjs +7729 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +677 -0
- package/dist/index.d.ts +677 -0
- package/dist/index.js +7654 -0
- package/dist/index.js.map +1 -0
- package/dist/theme.css +2 -0
- package/dist/tokens.cjs +78 -0
- package/dist/tokens.cjs.map +1 -0
- package/dist/tokens.d.cts +67 -0
- package/dist/tokens.d.ts +67 -0
- package/dist/tokens.js +70 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +103 -0
- package/src/data-display/activity-feed/ActivityFeed.stories.tsx +16 -0
- package/src/data-display/activity-feed/ActivityFeed.test.tsx +11 -0
- package/src/data-display/activity-feed/ActivityFeed.tsx +34 -0
- package/src/data-display/activity-feed/index.ts +2 -0
- package/src/data-display/circular-progress/CircularProgress.stories.tsx +10 -0
- package/src/data-display/circular-progress/CircularProgress.test.tsx +11 -0
- package/src/data-display/circular-progress/CircularProgress.tsx +35 -0
- package/src/data-display/circular-progress/index.ts +2 -0
- package/src/data-display/empty-state/EmptyState.stories.tsx +15 -0
- package/src/data-display/empty-state/EmptyState.test.tsx +18 -0
- package/src/data-display/empty-state/EmptyState.tsx +34 -0
- package/src/data-display/empty-state/index.ts +2 -0
- package/src/data-display/progress-bar/ProgressBar.stories.tsx +10 -0
- package/src/data-display/progress-bar/ProgressBar.test.tsx +15 -0
- package/src/data-display/progress-bar/ProgressBar.tsx +38 -0
- package/src/data-display/progress-bar/index.ts +2 -0
- package/src/data-display/stat-card/StatCard.stories.tsx +10 -0
- package/src/data-display/stat-card/StatCard.test.tsx +15 -0
- package/src/data-display/stat-card/StatCard.tsx +40 -0
- package/src/data-display/stat-card/index.ts +2 -0
- package/src/data-display/table/Table.stories.tsx +44 -0
- package/src/data-display/table/Table.test.tsx +16 -0
- package/src/data-display/table/Table.tsx +71 -0
- package/src/data-display/table/index.ts +1 -0
- package/src/data-display/timeline/Timeline.stories.tsx +16 -0
- package/src/data-display/timeline/Timeline.test.tsx +11 -0
- package/src/data-display/timeline/Timeline.tsx +44 -0
- package/src/data-display/timeline/index.ts +2 -0
- package/src/docs/ComponentOverview.mdx +192 -0
- package/src/docs/DesignTokens.mdx +235 -0
- package/src/docs/GettingStarted.mdx +145 -0
- package/src/feedback/alert-banner/AlertBanner.stories.tsx +10 -0
- package/src/feedback/alert-banner/AlertBanner.test.tsx +16 -0
- package/src/feedback/alert-banner/AlertBanner.tsx +47 -0
- package/src/feedback/alert-banner/index.ts +2 -0
- package/src/feedback/modal/Modal.stories.tsx +31 -0
- package/src/feedback/modal/Modal.test.tsx +33 -0
- package/src/feedback/modal/Modal.tsx +88 -0
- package/src/feedback/modal/index.ts +2 -0
- package/src/feedback/skeleton-loader/SkeletonLoader.stories.tsx +23 -0
- package/src/feedback/skeleton-loader/SkeletonLoader.test.tsx +14 -0
- package/src/feedback/skeleton-loader/SkeletonLoader.tsx +61 -0
- package/src/feedback/skeleton-loader/index.ts +2 -0
- package/src/feedback/toast/Toast.stories.tsx +27 -0
- package/src/feedback/toast/Toast.test.tsx +32 -0
- package/src/feedback/toast/Toast.tsx +106 -0
- package/src/feedback/toast/index.ts +2 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/use-controllable-state.ts +34 -0
- package/src/hooks/use-focus-trap.ts +56 -0
- package/src/hooks/use-reduced-motion.ts +17 -0
- package/src/index.ts +148 -0
- package/src/layout/card/Card.stories.tsx +45 -0
- package/src/layout/card/Card.test.tsx +23 -0
- package/src/layout/card/Card.tsx +42 -0
- package/src/layout/card/Card.types.ts +6 -0
- package/src/layout/card/index.ts +2 -0
- package/src/layout/command-palette/CommandPalette.stories.tsx +34 -0
- package/src/layout/command-palette/CommandPalette.test.tsx +43 -0
- package/src/layout/command-palette/CommandPalette.tsx +188 -0
- package/src/layout/command-palette/CommandPalette.types.ts +18 -0
- package/src/layout/command-palette/index.ts +2 -0
- package/src/layout/sidebar/Sidebar.stories.tsx +60 -0
- package/src/layout/sidebar/Sidebar.test.tsx +27 -0
- package/src/layout/sidebar/Sidebar.tsx +57 -0
- package/src/layout/sidebar/Sidebar.types.ts +14 -0
- package/src/layout/sidebar/index.ts +2 -0
- package/src/layout/top-bar/TopBar.stories.tsx +51 -0
- package/src/layout/top-bar/TopBar.test.tsx +18 -0
- package/src/layout/top-bar/TopBar.tsx +19 -0
- package/src/layout/top-bar/TopBar.types.ts +10 -0
- package/src/layout/top-bar/index.ts +2 -0
- package/src/navigation/breadcrumbs/Breadcrumbs.stories.tsx +30 -0
- package/src/navigation/breadcrumbs/Breadcrumbs.test.tsx +43 -0
- package/src/navigation/breadcrumbs/Breadcrumbs.tsx +45 -0
- package/src/navigation/breadcrumbs/Breadcrumbs.types.ts +12 -0
- package/src/navigation/breadcrumbs/index.ts +2 -0
- package/src/navigation/sidebar-nav-item/SidebarNavItem.stories.tsx +41 -0
- package/src/navigation/sidebar-nav-item/SidebarNavItem.test.tsx +46 -0
- package/src/navigation/sidebar-nav-item/SidebarNavItem.tsx +38 -0
- package/src/navigation/sidebar-nav-item/SidebarNavItem.types.ts +12 -0
- package/src/navigation/sidebar-nav-item/index.ts +2 -0
- package/src/navigation/tabs/Tabs.stories.tsx +47 -0
- package/src/navigation/tabs/Tabs.test.tsx +67 -0
- package/src/navigation/tabs/Tabs.tsx +111 -0
- package/src/navigation/tabs/Tabs.types.ts +36 -0
- package/src/navigation/tabs/index.ts +2 -0
- package/src/patterns/accordion/Accordion.stories.tsx +25 -0
- package/src/patterns/accordion/Accordion.test.tsx +44 -0
- package/src/patterns/accordion/Accordion.tsx +92 -0
- package/src/patterns/accordion/index.ts +2 -0
- package/src/patterns/action-menu/ActionMenu.stories.tsx +18 -0
- package/src/patterns/action-menu/ActionMenu.test.tsx +18 -0
- package/src/patterns/action-menu/ActionMenu.tsx +41 -0
- package/src/patterns/action-menu/index.ts +2 -0
- package/src/patterns/carousel/Carousel.stories.tsx +16 -0
- package/src/patterns/carousel/Carousel.test.tsx +16 -0
- package/src/patterns/carousel/Carousel.tsx +69 -0
- package/src/patterns/carousel/index.ts +2 -0
- package/src/patterns/image-placeholder/ImagePlaceholder.stories.tsx +9 -0
- package/src/patterns/image-placeholder/ImagePlaceholder.test.tsx +10 -0
- package/src/patterns/image-placeholder/ImagePlaceholder.tsx +21 -0
- package/src/patterns/image-placeholder/index.ts +2 -0
- package/src/patterns/notification-dot/NotificationDot.stories.tsx +17 -0
- package/src/patterns/notification-dot/NotificationDot.test.tsx +14 -0
- package/src/patterns/notification-dot/NotificationDot.tsx +18 -0
- package/src/patterns/notification-dot/index.ts +2 -0
- package/src/patterns/pagination/Pagination.stories.tsx +14 -0
- package/src/patterns/pagination/Pagination.test.tsx +22 -0
- package/src/patterns/pagination/Pagination.tsx +67 -0
- package/src/patterns/pagination/index.ts +2 -0
- package/src/primitives/avatar/Avatar.stories.tsx +46 -0
- package/src/primitives/avatar/Avatar.test.tsx +35 -0
- package/src/primitives/avatar/Avatar.tsx +49 -0
- package/src/primitives/avatar/Avatar.types.ts +21 -0
- package/src/primitives/avatar/AvatarGroup.tsx +27 -0
- package/src/primitives/avatar/index.ts +3 -0
- package/src/primitives/badge/Badge.stories.tsx +28 -0
- package/src/primitives/badge/Badge.test.tsx +23 -0
- package/src/primitives/badge/Badge.tsx +44 -0
- package/src/primitives/badge/Badge.types.ts +14 -0
- package/src/primitives/badge/index.ts +2 -0
- package/src/primitives/button/Button.stories.tsx +81 -0
- package/src/primitives/button/Button.test.tsx +64 -0
- package/src/primitives/button/Button.tsx +85 -0
- package/src/primitives/button/Button.types.ts +17 -0
- package/src/primitives/button/index.ts +2 -0
- package/src/primitives/checkbox/Checkbox.stories.tsx +27 -0
- package/src/primitives/checkbox/Checkbox.test.tsx +30 -0
- package/src/primitives/checkbox/Checkbox.tsx +79 -0
- package/src/primitives/checkbox/Checkbox.types.ts +12 -0
- package/src/primitives/checkbox/index.ts +2 -0
- package/src/primitives/combobox/Combobox.stories.tsx +44 -0
- package/src/primitives/combobox/Combobox.test.tsx +44 -0
- package/src/primitives/combobox/Combobox.tsx +201 -0
- package/src/primitives/combobox/Combobox.types.ts +25 -0
- package/src/primitives/combobox/index.ts +2 -0
- package/src/primitives/date-input/DateInput.stories.tsx +23 -0
- package/src/primitives/date-input/DateInput.test.tsx +22 -0
- package/src/primitives/date-input/DateInput.tsx +66 -0
- package/src/primitives/date-input/DateInput.types.ts +10 -0
- package/src/primitives/date-input/index.ts +2 -0
- package/src/primitives/file-upload/FileUploadDropzone.stories.tsx +27 -0
- package/src/primitives/file-upload/FileUploadDropzone.test.tsx +26 -0
- package/src/primitives/file-upload/FileUploadDropzone.tsx +99 -0
- package/src/primitives/file-upload/FileUploadDropzone.types.ts +14 -0
- package/src/primitives/file-upload/index.ts +2 -0
- package/src/primitives/input/InputGroup.stories.tsx +31 -0
- package/src/primitives/input/InputGroup.test.tsx +40 -0
- package/src/primitives/input/InputGroup.tsx +65 -0
- package/src/primitives/input/InputGroup.types.ts +12 -0
- package/src/primitives/input/index.ts +2 -0
- package/src/primitives/link/Link.stories.tsx +28 -0
- package/src/primitives/link/Link.test.tsx +23 -0
- package/src/primitives/link/Link.tsx +28 -0
- package/src/primitives/link/Link.types.ts +8 -0
- package/src/primitives/link/index.ts +2 -0
- package/src/primitives/radio/Radio.stories.tsx +29 -0
- package/src/primitives/radio/Radio.test.tsx +32 -0
- package/src/primitives/radio/Radio.tsx +59 -0
- package/src/primitives/radio/Radio.types.ts +6 -0
- package/src/primitives/radio/index.ts +2 -0
- package/src/primitives/select/SelectGroup.stories.tsx +33 -0
- package/src/primitives/select/SelectGroup.test.tsx +34 -0
- package/src/primitives/select/SelectGroup.tsx +72 -0
- package/src/primitives/select/SelectGroup.types.ts +12 -0
- package/src/primitives/select/index.ts +2 -0
- package/src/primitives/slider/Slider.stories.tsx +23 -0
- package/src/primitives/slider/Slider.test.tsx +28 -0
- package/src/primitives/slider/Slider.tsx +80 -0
- package/src/primitives/slider/Slider.types.ts +22 -0
- package/src/primitives/slider/index.ts +2 -0
- package/src/primitives/textarea/TextareaGroup.stories.tsx +27 -0
- package/src/primitives/textarea/TextareaGroup.test.tsx +24 -0
- package/src/primitives/textarea/TextareaGroup.tsx +59 -0
- package/src/primitives/textarea/TextareaGroup.types.ts +10 -0
- package/src/primitives/textarea/index.ts +2 -0
- package/src/primitives/toggle/Toggle.stories.tsx +27 -0
- package/src/primitives/toggle/Toggle.test.tsx +31 -0
- package/src/primitives/toggle/Toggle.tsx +65 -0
- package/src/primitives/toggle/Toggle.types.ts +16 -0
- package/src/primitives/toggle/index.ts +2 -0
- package/src/primitives/tooltip/Tooltip.stories.tsx +45 -0
- package/src/primitives/tooltip/Tooltip.test.tsx +28 -0
- package/src/primitives/tooltip/Tooltip.tsx +94 -0
- package/src/primitives/tooltip/Tooltip.types.ts +16 -0
- package/src/primitives/tooltip/index.ts +2 -0
- package/src/productivity/comment-thread/CommentThread.stories.tsx +20 -0
- package/src/productivity/comment-thread/CommentThread.test.tsx +21 -0
- package/src/productivity/comment-thread/CommentThread.tsx +47 -0
- package/src/productivity/comment-thread/index.ts +2 -0
- package/src/productivity/kanban-board/KanbanBoard.tsx +41 -0
- package/src/productivity/kanban-board/index.ts +2 -0
- package/src/productivity/kanban-column/KanbanColumn.stories.tsx +131 -0
- package/src/productivity/kanban-column/KanbanColumn.test.tsx +18 -0
- package/src/productivity/kanban-column/KanbanColumn.tsx +58 -0
- package/src/productivity/kanban-column/SortableTaskCard.tsx +46 -0
- package/src/productivity/kanban-column/index.ts +4 -0
- package/src/productivity/priority-selector/PrioritySelector.stories.tsx +14 -0
- package/src/productivity/priority-selector/PrioritySelector.test.tsx +18 -0
- package/src/productivity/priority-selector/PrioritySelector.tsx +40 -0
- package/src/productivity/priority-selector/index.ts +2 -0
- package/src/productivity/rich-text-toolbar/RichTextToolbar.stories.tsx +19 -0
- package/src/productivity/rich-text-toolbar/RichTextToolbar.test.tsx +21 -0
- package/src/productivity/rich-text-toolbar/RichTextToolbar.tsx +50 -0
- package/src/productivity/rich-text-toolbar/index.ts +2 -0
- package/src/productivity/task-card/TaskCard.stories.tsx +20 -0
- package/src/productivity/task-card/TaskCard.test.tsx +21 -0
- package/src/productivity/task-card/TaskCard.tsx +76 -0
- package/src/productivity/task-card/index.ts +2 -0
- package/src/test-setup.ts +1 -0
- package/src/tokens/index.ts +1 -0
- package/src/tokens/tokens.ts +71 -0
- package/src/tokens/weave-theme.css +168 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/index.ts +1 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { useState, useId, useRef, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
useFloating,
|
|
4
|
+
useClick,
|
|
5
|
+
useDismiss,
|
|
6
|
+
useRole,
|
|
7
|
+
useInteractions,
|
|
8
|
+
useListNavigation,
|
|
9
|
+
offset,
|
|
10
|
+
flip,
|
|
11
|
+
size as floatingSize,
|
|
12
|
+
FloatingPortal,
|
|
13
|
+
FloatingFocusManager,
|
|
14
|
+
} from '@floating-ui/react';
|
|
15
|
+
import { Check, ChevronDown, AlertCircle } from 'lucide-react';
|
|
16
|
+
import { cn } from '../../utils/cn';
|
|
17
|
+
import { useControllableState } from '../../hooks/use-controllable-state';
|
|
18
|
+
import type { ComboboxProps } from './Combobox.types';
|
|
19
|
+
|
|
20
|
+
export function Combobox({
|
|
21
|
+
label,
|
|
22
|
+
options,
|
|
23
|
+
value: controlledValue,
|
|
24
|
+
defaultValue = '',
|
|
25
|
+
onChange,
|
|
26
|
+
placeholder = 'Select...',
|
|
27
|
+
error,
|
|
28
|
+
disabled = false,
|
|
29
|
+
className,
|
|
30
|
+
}: ComboboxProps) {
|
|
31
|
+
const id = useId();
|
|
32
|
+
const messageId = `${id}-message`;
|
|
33
|
+
const [value, setValue] = useControllableState({
|
|
34
|
+
value: controlledValue,
|
|
35
|
+
defaultValue,
|
|
36
|
+
onChange,
|
|
37
|
+
});
|
|
38
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
39
|
+
const [query, setQuery] = useState('');
|
|
40
|
+
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
|
41
|
+
const listRef = useRef<(HTMLElement | null)[]>([]);
|
|
42
|
+
|
|
43
|
+
const filtered = query
|
|
44
|
+
? options.filter((o) => o.label.toLowerCase().includes(query.toLowerCase()))
|
|
45
|
+
: options;
|
|
46
|
+
|
|
47
|
+
const selectedLabel = options.find((o) => o.value === value)?.label ?? '';
|
|
48
|
+
|
|
49
|
+
const { refs, floatingStyles, context } = useFloating({
|
|
50
|
+
open: isOpen,
|
|
51
|
+
onOpenChange: setIsOpen,
|
|
52
|
+
placement: 'bottom-start',
|
|
53
|
+
middleware: [
|
|
54
|
+
offset(4),
|
|
55
|
+
flip(),
|
|
56
|
+
floatingSize({
|
|
57
|
+
apply({ rects, elements }) {
|
|
58
|
+
Object.assign(elements.floating.style, { width: `${rects.reference.width}px` });
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const click = useClick(context);
|
|
65
|
+
const dismiss = useDismiss(context);
|
|
66
|
+
const role = useRole(context, { role: 'listbox' });
|
|
67
|
+
const listNav = useListNavigation(context, {
|
|
68
|
+
listRef,
|
|
69
|
+
activeIndex,
|
|
70
|
+
onNavigate: setActiveIndex,
|
|
71
|
+
virtual: true,
|
|
72
|
+
loop: true,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
|
|
76
|
+
click,
|
|
77
|
+
dismiss,
|
|
78
|
+
role,
|
|
79
|
+
listNav,
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
const handleSelect = useCallback(
|
|
83
|
+
(optionValue: string) => {
|
|
84
|
+
setValue(optionValue);
|
|
85
|
+
setIsOpen(false);
|
|
86
|
+
setQuery('');
|
|
87
|
+
},
|
|
88
|
+
[setValue],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className={cn('flex flex-col gap-1.5', className)}>
|
|
93
|
+
<label
|
|
94
|
+
htmlFor={id}
|
|
95
|
+
className="text-text-primary font-medium"
|
|
96
|
+
style={{ fontSize: 'var(--text-body-sm)', lineHeight: 'var(--leading-body-sm)' }}
|
|
97
|
+
>
|
|
98
|
+
{label}
|
|
99
|
+
</label>
|
|
100
|
+
<div className="relative" ref={refs.setReference}>
|
|
101
|
+
<input
|
|
102
|
+
id={id}
|
|
103
|
+
type="text"
|
|
104
|
+
role="combobox"
|
|
105
|
+
aria-expanded={isOpen}
|
|
106
|
+
aria-autocomplete="list"
|
|
107
|
+
aria-invalid={!!error}
|
|
108
|
+
aria-describedby={error ? messageId : undefined}
|
|
109
|
+
disabled={disabled}
|
|
110
|
+
placeholder={isOpen ? 'Type to filter...' : (selectedLabel || placeholder)}
|
|
111
|
+
value={isOpen ? query : selectedLabel}
|
|
112
|
+
onChange={(e) => {
|
|
113
|
+
setQuery(e.target.value);
|
|
114
|
+
if (!isOpen) setIsOpen(true);
|
|
115
|
+
}}
|
|
116
|
+
onFocus={() => {
|
|
117
|
+
if (!isOpen) setIsOpen(true);
|
|
118
|
+
setQuery('');
|
|
119
|
+
}}
|
|
120
|
+
className={cn(
|
|
121
|
+
'w-full rounded-md bg-white px-3 py-2 pr-10 text-text-primary placeholder:text-text-secondary',
|
|
122
|
+
'border outline-none transition-all',
|
|
123
|
+
'focus:border-rust focus:ring-2 focus:ring-rust-light',
|
|
124
|
+
error ? 'border-error' : 'border-border',
|
|
125
|
+
disabled && 'bg-surface opacity-50 cursor-not-allowed',
|
|
126
|
+
)}
|
|
127
|
+
style={{
|
|
128
|
+
fontSize: 'var(--text-body)',
|
|
129
|
+
lineHeight: 'var(--leading-body)',
|
|
130
|
+
transitionDuration: 'var(--duration-fast)',
|
|
131
|
+
}}
|
|
132
|
+
{...getReferenceProps()}
|
|
133
|
+
/>
|
|
134
|
+
<ChevronDown
|
|
135
|
+
size={16}
|
|
136
|
+
className={cn(
|
|
137
|
+
'pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-text-secondary transition-transform',
|
|
138
|
+
isOpen && 'rotate-180',
|
|
139
|
+
)}
|
|
140
|
+
style={{ transitionDuration: 'var(--duration-fast)' }}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
{isOpen && (
|
|
144
|
+
<FloatingPortal>
|
|
145
|
+
<FloatingFocusManager context={context} initialFocus={-1} visuallyHiddenDismiss>
|
|
146
|
+
<div
|
|
147
|
+
ref={refs.setFloating}
|
|
148
|
+
style={floatingStyles}
|
|
149
|
+
className="z-50 rounded-md border border-border bg-white shadow-2 py-1 max-h-60 overflow-y-auto"
|
|
150
|
+
{...getFloatingProps()}
|
|
151
|
+
>
|
|
152
|
+
{filtered.length === 0 ? (
|
|
153
|
+
<div
|
|
154
|
+
className="px-3 py-2 text-text-secondary"
|
|
155
|
+
style={{ fontSize: 'var(--text-body-sm)' }}
|
|
156
|
+
>
|
|
157
|
+
No results found
|
|
158
|
+
</div>
|
|
159
|
+
) : (
|
|
160
|
+
filtered.map((option, i) => (
|
|
161
|
+
<div
|
|
162
|
+
key={option.value}
|
|
163
|
+
ref={(node) => { listRef.current[i] = node; }}
|
|
164
|
+
role="option"
|
|
165
|
+
aria-selected={value === option.value}
|
|
166
|
+
tabIndex={activeIndex === i ? 0 : -1}
|
|
167
|
+
className={cn(
|
|
168
|
+
'flex items-center justify-between px-3 py-2 cursor-pointer transition-colors',
|
|
169
|
+
activeIndex === i && 'bg-surface',
|
|
170
|
+
value === option.value && 'text-rust font-medium',
|
|
171
|
+
)}
|
|
172
|
+
style={{
|
|
173
|
+
fontSize: 'var(--text-body)',
|
|
174
|
+
transitionDuration: 'var(--duration-micro)',
|
|
175
|
+
}}
|
|
176
|
+
{...getItemProps({
|
|
177
|
+
onClick: () => handleSelect(option.value),
|
|
178
|
+
})}
|
|
179
|
+
>
|
|
180
|
+
{option.label}
|
|
181
|
+
{value === option.value && <Check size={16} className="text-rust" />}
|
|
182
|
+
</div>
|
|
183
|
+
))
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
</FloatingFocusManager>
|
|
187
|
+
</FloatingPortal>
|
|
188
|
+
)}
|
|
189
|
+
{error && (
|
|
190
|
+
<p
|
|
191
|
+
id={messageId}
|
|
192
|
+
className="flex items-center gap-1 text-error"
|
|
193
|
+
style={{ fontSize: 'var(--text-body-sm)', lineHeight: 'var(--leading-body-sm)' }}
|
|
194
|
+
>
|
|
195
|
+
<AlertCircle size={14} />
|
|
196
|
+
{error}
|
|
197
|
+
</p>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ComboboxOption {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface ComboboxProps {
|
|
7
|
+
/** Label text */
|
|
8
|
+
label: string;
|
|
9
|
+
/** Available options */
|
|
10
|
+
options: ComboboxOption[];
|
|
11
|
+
/** Controlled selected value */
|
|
12
|
+
value?: string;
|
|
13
|
+
/** Default selected value */
|
|
14
|
+
defaultValue?: string;
|
|
15
|
+
/** Change handler */
|
|
16
|
+
onChange?: (value: string) => void;
|
|
17
|
+
/** Placeholder text */
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
/** Error message */
|
|
20
|
+
error?: string;
|
|
21
|
+
/** Whether the combobox is disabled */
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
/** Additional CSS classes */
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { DateInput } from './DateInput';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof DateInput> = {
|
|
5
|
+
title: 'Primitives/DateInput',
|
|
6
|
+
component: DateInput,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof DateInput>;
|
|
12
|
+
|
|
13
|
+
export const Default: Story = {
|
|
14
|
+
args: { label: 'Due Date' },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const WithError: Story = {
|
|
18
|
+
args: { label: 'Start Date', error: 'Start date must be in the future' },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const Disabled: Story = {
|
|
22
|
+
args: { label: 'Created', value: '2026-03-01', disabled: true },
|
|
23
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { DateInput } from './DateInput';
|
|
4
|
+
|
|
5
|
+
describe('DateInput', () => {
|
|
6
|
+
it('renders label and date input', () => {
|
|
7
|
+
render(<DateInput label="Due Date" />);
|
|
8
|
+
expect(screen.getByLabelText('Due Date')).toHaveAttribute('type', 'date');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('shows error state', () => {
|
|
12
|
+
render(<DateInput label="Due Date" error="Required" />);
|
|
13
|
+
expect(screen.getByText('Required')).toBeInTheDocument();
|
|
14
|
+
expect(screen.getByLabelText('Due Date')).toHaveAttribute('aria-invalid', 'true');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('forwards ref', () => {
|
|
18
|
+
const ref = vi.fn();
|
|
19
|
+
render(<DateInput label="Due Date" ref={ref} />);
|
|
20
|
+
expect(ref).toHaveBeenCalledWith(expect.any(HTMLInputElement));
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { forwardRef, useId } from 'react';
|
|
2
|
+
import { Calendar, AlertCircle } from 'lucide-react';
|
|
3
|
+
import { cn } from '../../utils/cn';
|
|
4
|
+
import type { DateInputProps } from './DateInput.types';
|
|
5
|
+
|
|
6
|
+
export const DateInput = forwardRef<HTMLInputElement, DateInputProps>(
|
|
7
|
+
({ label, helperText, error, className, disabled, id: idProp, ...props }, ref) => {
|
|
8
|
+
const autoId = useId();
|
|
9
|
+
const id = idProp ?? autoId;
|
|
10
|
+
const messageId = `${id}-message`;
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className={cn('flex flex-col gap-1.5', className)}>
|
|
14
|
+
<label
|
|
15
|
+
htmlFor={id}
|
|
16
|
+
className="text-text-primary font-medium"
|
|
17
|
+
style={{ fontSize: 'var(--text-body-sm)', lineHeight: 'var(--leading-body-sm)' }}
|
|
18
|
+
>
|
|
19
|
+
{label}
|
|
20
|
+
</label>
|
|
21
|
+
<div className="relative">
|
|
22
|
+
<input
|
|
23
|
+
ref={ref}
|
|
24
|
+
id={id}
|
|
25
|
+
type="date"
|
|
26
|
+
disabled={disabled}
|
|
27
|
+
aria-invalid={!!error}
|
|
28
|
+
aria-describedby={error || helperText ? messageId : undefined}
|
|
29
|
+
className={cn(
|
|
30
|
+
'w-full rounded-md bg-white px-3 py-2 pr-10 text-text-primary',
|
|
31
|
+
'border outline-none transition-all',
|
|
32
|
+
'focus:border-rust focus:ring-2 focus:ring-rust-light',
|
|
33
|
+
error ? 'border-error' : 'border-border',
|
|
34
|
+
disabled && 'bg-surface opacity-50 cursor-not-allowed',
|
|
35
|
+
)}
|
|
36
|
+
style={{
|
|
37
|
+
fontSize: 'var(--text-body)',
|
|
38
|
+
lineHeight: 'var(--leading-body)',
|
|
39
|
+
transitionDuration: 'var(--duration-fast)',
|
|
40
|
+
}}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
<Calendar
|
|
44
|
+
size={16}
|
|
45
|
+
className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-text-secondary"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
{(error || helperText) && (
|
|
49
|
+
<p
|
|
50
|
+
id={messageId}
|
|
51
|
+
className={cn(
|
|
52
|
+
'flex items-center gap-1',
|
|
53
|
+
error ? 'text-error' : 'text-text-secondary',
|
|
54
|
+
)}
|
|
55
|
+
style={{ fontSize: 'var(--text-body-sm)', lineHeight: 'var(--leading-body-sm)' }}
|
|
56
|
+
>
|
|
57
|
+
{error && <AlertCircle size={14} />}
|
|
58
|
+
{error || helperText}
|
|
59
|
+
</p>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
DateInput.displayName = 'DateInput';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface DateInputProps extends Omit<ComponentPropsWithoutRef<'input'>, 'type' | 'size'> {
|
|
4
|
+
/** Label text displayed above the input */
|
|
5
|
+
label: string;
|
|
6
|
+
/** Helper text displayed below the input */
|
|
7
|
+
helperText?: string;
|
|
8
|
+
/** Error message */
|
|
9
|
+
error?: string;
|
|
10
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { FileUploadDropzone } from './FileUploadDropzone';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof FileUploadDropzone> = {
|
|
5
|
+
title: 'Primitives/FileUploadDropzone',
|
|
6
|
+
component: FileUploadDropzone,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof FileUploadDropzone>;
|
|
12
|
+
|
|
13
|
+
export const Default: Story = {
|
|
14
|
+
args: {},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const ImagesOnly: Story = {
|
|
18
|
+
args: {
|
|
19
|
+
accept: 'image/*',
|
|
20
|
+
description: 'Drag & drop images here, or click to browse',
|
|
21
|
+
multiple: true,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const Disabled: Story = {
|
|
26
|
+
args: { disabled: true },
|
|
27
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
+
import { FileUploadDropzone } from './FileUploadDropzone';
|
|
5
|
+
|
|
6
|
+
describe('FileUploadDropzone', () => {
|
|
7
|
+
it('renders upload area', () => {
|
|
8
|
+
render(<FileUploadDropzone />);
|
|
9
|
+
expect(screen.getByRole('button', { name: 'Upload files' })).toBeInTheDocument();
|
|
10
|
+
expect(screen.getByText('Browse files')).toBeInTheDocument();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('renders custom description', () => {
|
|
14
|
+
render(<FileUploadDropzone description="Upload images" />);
|
|
15
|
+
expect(screen.getByText('Upload images')).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('calls onFiles when files selected via input', async () => {
|
|
19
|
+
const onFiles = vi.fn();
|
|
20
|
+
render(<FileUploadDropzone onFiles={onFiles} />);
|
|
21
|
+
const file = new File(['hello'], 'test.txt', { type: 'text/plain' });
|
|
22
|
+
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
23
|
+
await userEvent.upload(input, file);
|
|
24
|
+
expect(onFiles).toHaveBeenCalledWith([file]);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useState, useRef, useCallback } from 'react';
|
|
2
|
+
import { UploadCloud } from 'lucide-react';
|
|
3
|
+
import { cn } from '../../utils/cn';
|
|
4
|
+
import type { FileUploadDropzoneProps } from './FileUploadDropzone.types';
|
|
5
|
+
|
|
6
|
+
export function FileUploadDropzone({
|
|
7
|
+
onFiles,
|
|
8
|
+
accept,
|
|
9
|
+
multiple = false,
|
|
10
|
+
disabled = false,
|
|
11
|
+
description = 'Drag & drop files here, or click to browse',
|
|
12
|
+
className,
|
|
13
|
+
}: FileUploadDropzoneProps) {
|
|
14
|
+
const [isDragOver, setIsDragOver] = useState(false);
|
|
15
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
16
|
+
|
|
17
|
+
const handleFiles = useCallback(
|
|
18
|
+
(fileList: FileList | null) => {
|
|
19
|
+
if (!fileList || disabled) return;
|
|
20
|
+
onFiles?.(Array.from(fileList));
|
|
21
|
+
},
|
|
22
|
+
[onFiles, disabled],
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const handleDrop = useCallback(
|
|
26
|
+
(e: React.DragEvent) => {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
setIsDragOver(false);
|
|
29
|
+
handleFiles(e.dataTransfer.files);
|
|
30
|
+
},
|
|
31
|
+
[handleFiles],
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
role="button"
|
|
37
|
+
tabIndex={disabled ? -1 : 0}
|
|
38
|
+
aria-label="Upload files"
|
|
39
|
+
onClick={() => !disabled && inputRef.current?.click()}
|
|
40
|
+
onKeyDown={(e) => {
|
|
41
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
if (!disabled) inputRef.current?.click();
|
|
44
|
+
}
|
|
45
|
+
}}
|
|
46
|
+
onDragOver={(e) => {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
if (!disabled) setIsDragOver(true);
|
|
49
|
+
}}
|
|
50
|
+
onDragLeave={() => setIsDragOver(false)}
|
|
51
|
+
onDrop={handleDrop}
|
|
52
|
+
className={cn(
|
|
53
|
+
'flex flex-col items-center justify-center gap-3 rounded-lg border-2 border-dashed p-8 transition-colors cursor-pointer',
|
|
54
|
+
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rust',
|
|
55
|
+
isDragOver ? 'border-rust bg-rust-light/30' : 'border-border bg-surface/50',
|
|
56
|
+
disabled && 'opacity-50 cursor-not-allowed',
|
|
57
|
+
className,
|
|
58
|
+
)}
|
|
59
|
+
style={{ transitionDuration: 'var(--duration-fast)' }}
|
|
60
|
+
>
|
|
61
|
+
<UploadCloud size={32} className={cn('text-text-secondary', isDragOver && 'text-rust')} />
|
|
62
|
+
<p
|
|
63
|
+
className="text-text-secondary text-center"
|
|
64
|
+
style={{ fontSize: 'var(--text-body-sm)', lineHeight: 'var(--leading-body-sm)' }}
|
|
65
|
+
>
|
|
66
|
+
{description}
|
|
67
|
+
</p>
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
tabIndex={-1}
|
|
71
|
+
disabled={disabled}
|
|
72
|
+
className={cn(
|
|
73
|
+
'rounded-md bg-white border border-border px-3 py-1.5 text-text-primary font-medium transition-colors',
|
|
74
|
+
'hover:bg-surface',
|
|
75
|
+
)}
|
|
76
|
+
style={{
|
|
77
|
+
fontSize: 'var(--text-body-sm)',
|
|
78
|
+
transitionDuration: 'var(--duration-fast)',
|
|
79
|
+
}}
|
|
80
|
+
onClick={(e) => {
|
|
81
|
+
e.stopPropagation();
|
|
82
|
+
inputRef.current?.click();
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
Browse files
|
|
86
|
+
</button>
|
|
87
|
+
<input
|
|
88
|
+
ref={inputRef}
|
|
89
|
+
type="file"
|
|
90
|
+
accept={accept}
|
|
91
|
+
multiple={multiple}
|
|
92
|
+
disabled={disabled}
|
|
93
|
+
className="sr-only"
|
|
94
|
+
onChange={(e) => handleFiles(e.target.files)}
|
|
95
|
+
tabIndex={-1}
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface FileUploadDropzoneProps {
|
|
2
|
+
/** Called when files are dropped or selected */
|
|
3
|
+
onFiles?: (files: File[]) => void;
|
|
4
|
+
/** Accepted file types (e.g., "image/*,.pdf") */
|
|
5
|
+
accept?: string;
|
|
6
|
+
/** Allow multiple files */
|
|
7
|
+
multiple?: boolean;
|
|
8
|
+
/** Whether the dropzone is disabled */
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
/** Description text */
|
|
11
|
+
description?: string;
|
|
12
|
+
/** Additional CSS classes */
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { InputGroup } from './InputGroup';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof InputGroup> = {
|
|
5
|
+
title: 'Primitives/InputGroup',
|
|
6
|
+
component: InputGroup,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof InputGroup>;
|
|
12
|
+
|
|
13
|
+
export const Default: Story = {
|
|
14
|
+
args: { label: 'Email', placeholder: 'you@example.com' },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const WithHelperText: Story = {
|
|
18
|
+
args: { label: 'Password', type: 'password', helperText: 'Must be at least 8 characters' },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const ErrorState: Story = {
|
|
22
|
+
args: { label: 'Email', value: 'invalid', error: 'Please enter a valid email address' },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const SuccessState: Story = {
|
|
26
|
+
args: { label: 'Username', value: 'weaveuser', success: 'Username is available' },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const Disabled: Story = {
|
|
30
|
+
args: { label: 'Email', value: 'locked@example.com', disabled: true },
|
|
31
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
+
import { InputGroup } from './InputGroup';
|
|
5
|
+
|
|
6
|
+
describe('InputGroup', () => {
|
|
7
|
+
it('renders label and input', () => {
|
|
8
|
+
render(<InputGroup label="Email" />);
|
|
9
|
+
expect(screen.getByLabelText('Email')).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('shows helper text', () => {
|
|
13
|
+
render(<InputGroup label="Name" helperText="Enter your full name" />);
|
|
14
|
+
expect(screen.getByText('Enter your full name')).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('shows error state', () => {
|
|
18
|
+
render(<InputGroup label="Email" error="Invalid email" />);
|
|
19
|
+
expect(screen.getByText('Invalid email')).toBeInTheDocument();
|
|
20
|
+
expect(screen.getByLabelText('Email')).toHaveAttribute('aria-invalid', 'true');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('shows success state', () => {
|
|
24
|
+
render(<InputGroup label="Username" success="Available" />);
|
|
25
|
+
expect(screen.getByText('Available')).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('handles user input', async () => {
|
|
29
|
+
const onChange = vi.fn();
|
|
30
|
+
render(<InputGroup label="Email" onChange={onChange} />);
|
|
31
|
+
await userEvent.type(screen.getByLabelText('Email'), 'test');
|
|
32
|
+
expect(onChange).toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('forwards ref', () => {
|
|
36
|
+
const ref = vi.fn();
|
|
37
|
+
render(<InputGroup label="Email" ref={ref} />);
|
|
38
|
+
expect(ref).toHaveBeenCalledWith(expect.any(HTMLInputElement));
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { forwardRef, useId } from 'react';
|
|
2
|
+
import { AlertCircle, CheckCircle2 } from 'lucide-react';
|
|
3
|
+
import { cn } from '../../utils/cn';
|
|
4
|
+
import type { InputGroupProps } from './InputGroup.types';
|
|
5
|
+
|
|
6
|
+
export const InputGroup = forwardRef<HTMLInputElement, InputGroupProps>(
|
|
7
|
+
({ label, helperText, error, success, className, disabled, id: idProp, ...props }, ref) => {
|
|
8
|
+
const autoId = useId();
|
|
9
|
+
const id = idProp ?? autoId;
|
|
10
|
+
const messageId = `${id}-message`;
|
|
11
|
+
const state = error ? 'error' : success ? 'success' : 'default';
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className={cn('flex flex-col gap-1.5', className)}>
|
|
15
|
+
<label
|
|
16
|
+
htmlFor={id}
|
|
17
|
+
className="text-text-primary font-medium"
|
|
18
|
+
style={{ fontSize: 'var(--text-body-sm)', lineHeight: 'var(--leading-body-sm)' }}
|
|
19
|
+
>
|
|
20
|
+
{label}
|
|
21
|
+
</label>
|
|
22
|
+
<input
|
|
23
|
+
ref={ref}
|
|
24
|
+
id={id}
|
|
25
|
+
disabled={disabled}
|
|
26
|
+
aria-invalid={state === 'error'}
|
|
27
|
+
aria-describedby={error || success || helperText ? messageId : undefined}
|
|
28
|
+
className={cn(
|
|
29
|
+
'w-full rounded-md bg-white px-3 py-2 text-text-primary placeholder:text-text-secondary',
|
|
30
|
+
'border outline-none transition-all',
|
|
31
|
+
'focus:border-rust focus:ring-2 focus:ring-rust-light',
|
|
32
|
+
state === 'default' && 'border-border',
|
|
33
|
+
state === 'error' && 'border-error',
|
|
34
|
+
state === 'success' && 'border-success',
|
|
35
|
+
disabled && 'bg-surface opacity-50 cursor-not-allowed',
|
|
36
|
+
)}
|
|
37
|
+
style={{
|
|
38
|
+
fontSize: 'var(--text-body)',
|
|
39
|
+
lineHeight: 'var(--leading-body)',
|
|
40
|
+
transitionDuration: 'var(--duration-fast)',
|
|
41
|
+
}}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
{(error || success || helperText) && (
|
|
45
|
+
<p
|
|
46
|
+
id={messageId}
|
|
47
|
+
className={cn(
|
|
48
|
+
'flex items-center gap-1',
|
|
49
|
+
state === 'error' && 'text-error',
|
|
50
|
+
state === 'success' && 'text-success',
|
|
51
|
+
state === 'default' && 'text-text-secondary',
|
|
52
|
+
)}
|
|
53
|
+
style={{ fontSize: 'var(--text-body-sm)', lineHeight: 'var(--leading-body-sm)' }}
|
|
54
|
+
>
|
|
55
|
+
{state === 'error' && <AlertCircle size={14} />}
|
|
56
|
+
{state === 'success' && <CheckCircle2 size={14} />}
|
|
57
|
+
{error || success || helperText}
|
|
58
|
+
</p>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
InputGroup.displayName = 'InputGroup';
|