rizzo-css 0.0.62 → 0.0.63
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/README.md +9 -5
- package/bin/rizzo-css.js +247 -27
- package/dist/rizzo.min.css +5 -3
- package/package.json +14 -7
- package/scaffold/astro/Footer.astro +8 -0
- package/scaffold/astro/Settings.astro +8 -2
- package/scaffold/astro/Tabs.astro +2 -2
- package/scaffold/react/Accordion.tsx +143 -0
- package/scaffold/react/Alert.tsx +90 -0
- package/scaffold/react/AlertDialog.tsx +80 -0
- package/scaffold/react/AspectRatio.tsx +32 -0
- package/scaffold/react/Avatar.tsx +53 -0
- package/scaffold/react/BackToTop.tsx +62 -0
- package/scaffold/react/Badge.tsx +39 -0
- package/scaffold/react/Breadcrumb.tsx +81 -0
- package/scaffold/react/Button.tsx +40 -0
- package/scaffold/react/ButtonGroup.tsx +24 -0
- package/scaffold/react/Card.tsx +26 -0
- package/scaffold/react/Checkbox.tsx +40 -0
- package/scaffold/react/Collapsible.tsx +58 -0
- package/scaffold/react/ContextMenu.tsx +67 -0
- package/scaffold/react/CopyToClipboard.tsx +128 -0
- package/scaffold/react/Dashboard.tsx +23 -0
- package/scaffold/react/Divider.tsx +47 -0
- package/scaffold/react/DocsSidebar.tsx +48 -0
- package/scaffold/react/Dropdown.tsx +256 -0
- package/scaffold/react/Empty.tsx +29 -0
- package/scaffold/react/FontSwitcher.tsx +68 -0
- package/scaffold/react/Footer.tsx +55 -0
- package/scaffold/react/FormGroup.tsx +57 -0
- package/scaffold/react/HoverCard.tsx +61 -0
- package/scaffold/react/Icons.tsx +22 -0
- package/scaffold/react/Input.tsx +69 -0
- package/scaffold/react/Kbd.tsx +16 -0
- package/scaffold/react/Label.tsx +16 -0
- package/scaffold/react/Modal.tsx +149 -0
- package/scaffold/react/Navbar.tsx +72 -0
- package/scaffold/react/Pagination.tsx +155 -0
- package/scaffold/react/Popover.tsx +66 -0
- package/scaffold/react/ProgressBar.tsx +66 -0
- package/scaffold/react/Radio.tsx +38 -0
- package/scaffold/react/ResizableHandle.tsx +24 -0
- package/scaffold/react/ResizablePane.tsx +29 -0
- package/scaffold/react/ResizablePaneGroup.tsx +29 -0
- package/scaffold/react/ScrollArea.tsx +29 -0
- package/scaffold/react/Search.tsx +62 -0
- package/scaffold/react/Select.tsx +65 -0
- package/scaffold/react/Separator.tsx +33 -0
- package/scaffold/react/Settings.tsx +60 -0
- package/scaffold/react/Sheet.tsx +86 -0
- package/scaffold/react/Skeleton.tsx +32 -0
- package/scaffold/react/Slider.tsx +66 -0
- package/scaffold/react/SoundEffects.tsx +15 -0
- package/scaffold/react/Spinner.tsx +36 -0
- package/scaffold/react/Switch.tsx +52 -0
- package/scaffold/react/Table.tsx +178 -0
- package/scaffold/react/Tabs.tsx +143 -0
- package/scaffold/react/Textarea.tsx +69 -0
- package/scaffold/react/ThemeSwitcher.tsx +89 -0
- package/scaffold/react/Toast.tsx +43 -0
- package/scaffold/react/Toggle.tsx +45 -0
- package/scaffold/react/ToggleGroup.tsx +34 -0
- package/scaffold/react/Tooltip.tsx +40 -0
- package/scaffold/vanilla/README-RIZZO.md +1 -1
- package/scaffold/vanilla/components/accordion.html +30 -0
- package/scaffold/vanilla/components/alert-dialog.html +30 -0
- package/scaffold/vanilla/components/alert.html +30 -0
- package/scaffold/vanilla/components/aspect-ratio.html +30 -0
- package/scaffold/vanilla/components/avatar.html +30 -0
- package/scaffold/vanilla/components/back-to-top.html +30 -0
- package/scaffold/vanilla/components/badge.html +30 -0
- package/scaffold/vanilla/components/breadcrumb.html +30 -0
- package/scaffold/vanilla/components/button-group.html +30 -0
- package/scaffold/vanilla/components/button.html +30 -0
- package/scaffold/vanilla/components/cards.html +30 -0
- package/scaffold/vanilla/components/collapsible.html +30 -0
- package/scaffold/vanilla/components/context-menu.html +30 -0
- package/scaffold/vanilla/components/copy-to-clipboard.html +30 -0
- package/scaffold/vanilla/components/dashboard.html +30 -0
- package/scaffold/vanilla/components/divider.html +30 -0
- package/scaffold/vanilla/components/docs-sidebar.html +30 -0
- package/scaffold/vanilla/components/dropdown.html +30 -0
- package/scaffold/vanilla/components/empty.html +30 -0
- package/scaffold/vanilla/components/font-switcher.html +30 -0
- package/scaffold/vanilla/components/footer.html +30 -0
- package/scaffold/vanilla/components/forms.html +30 -0
- package/scaffold/vanilla/components/hover-card.html +30 -0
- package/scaffold/vanilla/components/icons.html +30 -0
- package/scaffold/vanilla/components/index.html +30 -0
- package/scaffold/vanilla/components/kbd.html +30 -0
- package/scaffold/vanilla/components/label.html +30 -0
- package/scaffold/vanilla/components/modal.html +30 -0
- package/scaffold/vanilla/components/navbar.html +30 -0
- package/scaffold/vanilla/components/pagination.html +30 -0
- package/scaffold/vanilla/components/popover.html +30 -0
- package/scaffold/vanilla/components/progress-bar.html +30 -0
- package/scaffold/vanilla/components/resizable.html +30 -0
- package/scaffold/vanilla/components/scroll-area.html +30 -0
- package/scaffold/vanilla/components/search.html +30 -0
- package/scaffold/vanilla/components/separator.html +30 -0
- package/scaffold/vanilla/components/settings.html +30 -0
- package/scaffold/vanilla/components/sheet.html +30 -0
- package/scaffold/vanilla/components/skeleton.html +30 -0
- package/scaffold/vanilla/components/slider.html +30 -0
- package/scaffold/vanilla/components/sound-effects.html +30 -0
- package/scaffold/vanilla/components/spinner.html +30 -0
- package/scaffold/vanilla/components/switch.html +30 -0
- package/scaffold/vanilla/components/table.html +30 -0
- package/scaffold/vanilla/components/tabs.html +30 -0
- package/scaffold/vanilla/components/theme-switcher.html +30 -0
- package/scaffold/vanilla/components/toast.html +30 -0
- package/scaffold/vanilla/components/toggle-group.html +30 -0
- package/scaffold/vanilla/components/toggle.html +30 -0
- package/scaffold/vanilla/components/tooltip.html +30 -0
- package/scaffold/vanilla/index.html +30 -0
- package/scaffold/vue/Accordion.vue +9 -0
- package/scaffold/vue/Alert.vue +9 -0
- package/scaffold/vue/AlertDialog.vue +9 -0
- package/scaffold/vue/AspectRatio.vue +9 -0
- package/scaffold/vue/Avatar.vue +9 -0
- package/scaffold/vue/BackToTop.vue +9 -0
- package/scaffold/vue/Badge.vue +28 -0
- package/scaffold/vue/Breadcrumb.vue +9 -0
- package/scaffold/vue/Button.vue +23 -0
- package/scaffold/vue/ButtonGroup.vue +9 -0
- package/scaffold/vue/Card.vue +21 -0
- package/scaffold/vue/Checkbox.vue +31 -0
- package/scaffold/vue/Collapsible.vue +9 -0
- package/scaffold/vue/ContextMenu.vue +9 -0
- package/scaffold/vue/CopyToClipboard.vue +9 -0
- package/scaffold/vue/Dashboard.vue +9 -0
- package/scaffold/vue/Divider.vue +23 -0
- package/scaffold/vue/DocsSidebar.vue +9 -0
- package/scaffold/vue/Dropdown.vue +9 -0
- package/scaffold/vue/Empty.vue +9 -0
- package/scaffold/vue/FontSwitcher.vue +9 -0
- package/scaffold/vue/Footer.vue +9 -0
- package/scaffold/vue/FormGroup.vue +45 -0
- package/scaffold/vue/HoverCard.vue +9 -0
- package/scaffold/vue/Icons.vue +9 -0
- package/scaffold/vue/Input.vue +59 -0
- package/scaffold/vue/Kbd.vue +9 -0
- package/scaffold/vue/Label.vue +23 -0
- package/scaffold/vue/Modal.vue +9 -0
- package/scaffold/vue/Navbar.vue +9 -0
- package/scaffold/vue/Pagination.vue +9 -0
- package/scaffold/vue/Popover.vue +9 -0
- package/scaffold/vue/ProgressBar.vue +9 -0
- package/scaffold/vue/Radio.vue +29 -0
- package/scaffold/vue/ResizableHandle.vue +9 -0
- package/scaffold/vue/ResizablePane.vue +9 -0
- package/scaffold/vue/ResizablePaneGroup.vue +9 -0
- package/scaffold/vue/ScrollArea.vue +9 -0
- package/scaffold/vue/Search.vue +9 -0
- package/scaffold/vue/Select.vue +52 -0
- package/scaffold/vue/Separator.vue +9 -0
- package/scaffold/vue/Settings.vue +9 -0
- package/scaffold/vue/Sheet.vue +9 -0
- package/scaffold/vue/Skeleton.vue +9 -0
- package/scaffold/vue/Slider.vue +9 -0
- package/scaffold/vue/SoundEffects.vue +9 -0
- package/scaffold/vue/Spinner.vue +21 -0
- package/scaffold/vue/Switch.vue +9 -0
- package/scaffold/vue/Table.vue +9 -0
- package/scaffold/vue/Tabs.vue +9 -0
- package/scaffold/vue/Textarea.vue +60 -0
- package/scaffold/vue/ThemeSwitcher.vue +9 -0
- package/scaffold/vue/Toast.vue +9 -0
- package/scaffold/vue/Toggle.vue +9 -0
- package/scaffold/vue/ToggleGroup.vue +9 -0
- package/scaffold/vue/Tooltip.vue +9 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { useState, useRef, useEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
export interface MenuItem {
|
|
5
|
+
label: string;
|
|
6
|
+
value?: string;
|
|
7
|
+
href?: string;
|
|
8
|
+
onClick?: (value: string) => void;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
separator?: boolean;
|
|
11
|
+
submenu?: MenuItem[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DropdownProps {
|
|
15
|
+
trigger: string;
|
|
16
|
+
items: MenuItem[];
|
|
17
|
+
id?: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
position?: 'left' | 'right';
|
|
20
|
+
align?: 'start' | 'end';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function Dropdown({
|
|
24
|
+
trigger,
|
|
25
|
+
items,
|
|
26
|
+
id: idProp,
|
|
27
|
+
className = '',
|
|
28
|
+
position = 'left',
|
|
29
|
+
align = 'start',
|
|
30
|
+
}: DropdownProps) {
|
|
31
|
+
const dropdownId = idProp ?? `dropdown-${Math.random().toString(36).slice(2, 11)}`;
|
|
32
|
+
const menuId = `${dropdownId}-menu`;
|
|
33
|
+
const triggerId = `${dropdownId}-trigger`;
|
|
34
|
+
const [open, setOpen] = useState(false);
|
|
35
|
+
const [openSubmenuIndex, setOpenSubmenuIndex] = useState<number | null>(null);
|
|
36
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
37
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
38
|
+
|
|
39
|
+
const closeMenu = () => {
|
|
40
|
+
setOpen(false);
|
|
41
|
+
setOpenSubmenuIndex(null);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const toggleSubmenu = (index: number) => {
|
|
45
|
+
setOpenSubmenuIndex((prev) => (prev === index ? null : index));
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleItemClick = (item: MenuItem, e: React.MouseEvent) => {
|
|
49
|
+
if (item.separator || item.disabled) return;
|
|
50
|
+
if (item.submenu?.length) {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (item.href) window.location.href = item.href;
|
|
55
|
+
item.onClick?.(item.value ?? item.label);
|
|
56
|
+
closeMenu();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const handleKeydown = (e: KeyboardEvent) => {
|
|
61
|
+
if (!open) return;
|
|
62
|
+
if (e.key === 'Escape') {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
closeMenu();
|
|
65
|
+
triggerRef.current?.focus();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
document.addEventListener('keydown', handleKeydown);
|
|
69
|
+
return () => document.removeEventListener('keydown', handleKeydown);
|
|
70
|
+
}, [open]);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
74
|
+
if (
|
|
75
|
+
menuRef.current &&
|
|
76
|
+
triggerRef.current &&
|
|
77
|
+
!menuRef.current.contains(e.target as Node) &&
|
|
78
|
+
!triggerRef.current.contains(e.target as Node)
|
|
79
|
+
) {
|
|
80
|
+
closeMenu();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
document.addEventListener('click', handleClickOutside);
|
|
84
|
+
return () => document.removeEventListener('click', handleClickOutside);
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
const classes = ['dropdown', className].filter(Boolean).join(' ').trim();
|
|
88
|
+
const menuClasses = `dropdown__menu dropdown__menu--${position} dropdown__menu--${align} ${open ? 'dropdown__menu--open' : ''}`.trim();
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className={classes} data-dropdown={dropdownId}>
|
|
92
|
+
<button
|
|
93
|
+
ref={triggerRef}
|
|
94
|
+
type="button"
|
|
95
|
+
className="dropdown__trigger"
|
|
96
|
+
id={triggerId}
|
|
97
|
+
aria-expanded={open}
|
|
98
|
+
aria-haspopup="true"
|
|
99
|
+
aria-controls={menuId}
|
|
100
|
+
aria-label={trigger}
|
|
101
|
+
onClick={() => setOpen((o) => !o)}
|
|
102
|
+
onKeyDown={(e) => {
|
|
103
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
setOpen((o) => !o);
|
|
106
|
+
}
|
|
107
|
+
if (e.key === 'ArrowDown' && !open) {
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
setOpen(true);
|
|
110
|
+
}
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
<span className="dropdown__trigger-text">{trigger}</span>
|
|
114
|
+
<span className="dropdown__icon" aria-hidden="true">
|
|
115
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
116
|
+
<path d="m6 9 6 6 6-6" />
|
|
117
|
+
</svg>
|
|
118
|
+
</span>
|
|
119
|
+
</button>
|
|
120
|
+
<div
|
|
121
|
+
ref={menuRef}
|
|
122
|
+
className={menuClasses}
|
|
123
|
+
id={menuId}
|
|
124
|
+
role="menu"
|
|
125
|
+
aria-labelledby={triggerId}
|
|
126
|
+
aria-label={`${trigger} menu`}
|
|
127
|
+
aria-orientation="vertical"
|
|
128
|
+
aria-hidden={!open}
|
|
129
|
+
tabIndex={-1}
|
|
130
|
+
>
|
|
131
|
+
{items.map((item, index) => {
|
|
132
|
+
if (item.separator) {
|
|
133
|
+
return <div key={index} className="dropdown__separator" role="separator" />;
|
|
134
|
+
}
|
|
135
|
+
const hasSubmenu = item.submenu && item.submenu.length > 0;
|
|
136
|
+
const isSubmenuOpen = openSubmenuIndex === index;
|
|
137
|
+
return (
|
|
138
|
+
<div
|
|
139
|
+
key={index}
|
|
140
|
+
className={`dropdown__item-wrapper ${hasSubmenu ? 'dropdown__item-wrapper--has-submenu' : ''}`.trim()}
|
|
141
|
+
>
|
|
142
|
+
{item.href && !hasSubmenu ? (
|
|
143
|
+
<a
|
|
144
|
+
className={`dropdown__item ${item.disabled ? 'dropdown__item--disabled' : ''}`.trim()}
|
|
145
|
+
role="menuitem"
|
|
146
|
+
href={item.href}
|
|
147
|
+
aria-label={item.label}
|
|
148
|
+
aria-disabled={item.disabled ? 'true' : undefined}
|
|
149
|
+
tabIndex={open ? 0 : -1}
|
|
150
|
+
onClick={(e) => {
|
|
151
|
+
if (item.disabled) e.preventDefault();
|
|
152
|
+
else closeMenu();
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
<span>{item.label}</span>
|
|
156
|
+
</a>
|
|
157
|
+
) : (
|
|
158
|
+
<div
|
|
159
|
+
className={`dropdown__item ${item.disabled ? 'dropdown__item--disabled' : ''} ${hasSubmenu ? 'dropdown__item--has-submenu' : ''}`.trim()}
|
|
160
|
+
role="menuitem"
|
|
161
|
+
aria-disabled={item.disabled ? 'true' : undefined}
|
|
162
|
+
aria-expanded={hasSubmenu ? isSubmenuOpen : undefined}
|
|
163
|
+
aria-haspopup={hasSubmenu ? 'true' : undefined}
|
|
164
|
+
tabIndex={open ? 0 : -1}
|
|
165
|
+
onClick={(e) => {
|
|
166
|
+
if (item.disabled) return;
|
|
167
|
+
if (hasSubmenu) {
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
toggleSubmenu(index);
|
|
170
|
+
} else {
|
|
171
|
+
handleItemClick(item, e);
|
|
172
|
+
}
|
|
173
|
+
}}
|
|
174
|
+
onKeyDown={(e) => {
|
|
175
|
+
if (item.disabled) return;
|
|
176
|
+
if (hasSubmenu && (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowRight')) {
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
toggleSubmenu(index);
|
|
179
|
+
} else if (!hasSubmenu && (e.key === 'Enter' || e.key === ' ')) {
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
handleItemClick(item, e as unknown as React.MouseEvent);
|
|
182
|
+
}
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
<span>{item.label}</span>
|
|
186
|
+
{hasSubmenu && (
|
|
187
|
+
<span className="dropdown__item-arrow" aria-hidden="true">
|
|
188
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
189
|
+
<path d="m9 6 6 6-6 6" />
|
|
190
|
+
</svg>
|
|
191
|
+
</span>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
{hasSubmenu && item.submenu && (
|
|
196
|
+
<div
|
|
197
|
+
className={`dropdown__submenu ${isSubmenuOpen ? 'dropdown__submenu--open' : ''}`.trim()}
|
|
198
|
+
role="menu"
|
|
199
|
+
aria-label={`${item.label} submenu`}
|
|
200
|
+
aria-hidden={!isSubmenuOpen}
|
|
201
|
+
>
|
|
202
|
+
{item.submenu.map((subItem, i) =>
|
|
203
|
+
subItem.separator ? (
|
|
204
|
+
<div key={i} className="dropdown__separator" role="separator" />
|
|
205
|
+
) : subItem.href ? (
|
|
206
|
+
<a
|
|
207
|
+
key={i}
|
|
208
|
+
className={`dropdown__item dropdown__submenu-item ${subItem.disabled ? 'dropdown__item--disabled' : ''}`.trim()}
|
|
209
|
+
role="menuitem"
|
|
210
|
+
href={subItem.href}
|
|
211
|
+
aria-label={subItem.label}
|
|
212
|
+
aria-disabled={subItem.disabled ? 'true' : undefined}
|
|
213
|
+
onClick={(e) => {
|
|
214
|
+
if (subItem.disabled) e.preventDefault();
|
|
215
|
+
else closeMenu();
|
|
216
|
+
}}
|
|
217
|
+
>
|
|
218
|
+
<span>{subItem.label}</span>
|
|
219
|
+
</a>
|
|
220
|
+
) : (
|
|
221
|
+
<div
|
|
222
|
+
key={i}
|
|
223
|
+
className={`dropdown__item dropdown__submenu-item ${subItem.disabled ? 'dropdown__item--disabled' : ''}`.trim()}
|
|
224
|
+
role="menuitem"
|
|
225
|
+
aria-disabled={subItem.disabled ? 'true' : undefined}
|
|
226
|
+
tabIndex={open ? 0 : -1}
|
|
227
|
+
onClick={(e) => {
|
|
228
|
+
if (!subItem.disabled) {
|
|
229
|
+
subItem.onClick?.(subItem.value ?? subItem.label);
|
|
230
|
+
closeMenu();
|
|
231
|
+
}
|
|
232
|
+
}}
|
|
233
|
+
onKeyDown={(e) => {
|
|
234
|
+
if (subItem.disabled) return;
|
|
235
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
236
|
+
e.preventDefault();
|
|
237
|
+
subItem.onClick?.(subItem.value ?? subItem.label);
|
|
238
|
+
closeMenu();
|
|
239
|
+
}
|
|
240
|
+
}}
|
|
241
|
+
>
|
|
242
|
+
<span>{subItem.label}</span>
|
|
243
|
+
</div>
|
|
244
|
+
)
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
})}
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export default Dropdown;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface EmptyProps extends HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
title?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
icon?: ReactNode;
|
|
7
|
+
action?: ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Empty({
|
|
12
|
+
title = 'No results',
|
|
13
|
+
description,
|
|
14
|
+
icon,
|
|
15
|
+
action,
|
|
16
|
+
className = '',
|
|
17
|
+
...rest
|
|
18
|
+
}: EmptyProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div className={`empty ${className}`.trim()} {...rest}>
|
|
21
|
+
{icon && <div className="empty__icon">{icon}</div>}
|
|
22
|
+
<h3 className="empty__title">{title}</h3>
|
|
23
|
+
{description && <p className="empty__description">{description}</p>}
|
|
24
|
+
{action && <div className="empty__action">{action}</div>}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default Empty;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
|
|
4
|
+
export interface FontPair {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface FontSwitcherProps extends HTMLAttributes<HTMLDivElement> {
|
|
10
|
+
id?: string;
|
|
11
|
+
fonts?: FontPair[];
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DEFAULT_FONTS: FontPair[] = [
|
|
16
|
+
{ id: 'system', label: 'System' },
|
|
17
|
+
{ id: 'sans-mono', label: 'Sans + Mono' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export function FontSwitcher({
|
|
21
|
+
id: idProp,
|
|
22
|
+
fonts = DEFAULT_FONTS,
|
|
23
|
+
className = '',
|
|
24
|
+
...rest
|
|
25
|
+
}: FontSwitcherProps) {
|
|
26
|
+
const id = idProp ?? `font-switcher-${Math.random().toString(36).slice(2, 9)}`;
|
|
27
|
+
const [open, setOpen] = useState(false);
|
|
28
|
+
const [current, setCurrent] = useState(fonts[0]?.id ?? 'system');
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className={`font-switcher ${className}`.trim()} data-font-switcher id={id} {...rest}>
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
className="font-switcher__trigger"
|
|
35
|
+
aria-expanded={open}
|
|
36
|
+
aria-haspopup="true"
|
|
37
|
+
aria-controls={`${id}-menu`}
|
|
38
|
+
onClick={() => setOpen((o) => !o)}
|
|
39
|
+
>
|
|
40
|
+
{fonts.find((f) => f.id === current)?.label ?? 'Font'}
|
|
41
|
+
</button>
|
|
42
|
+
<div
|
|
43
|
+
className={`font-switcher__menu ${open ? 'font-switcher__menu--open' : ''}`.trim()}
|
|
44
|
+
id={`${id}-menu`}
|
|
45
|
+
role="menu"
|
|
46
|
+
aria-hidden={!open}
|
|
47
|
+
hidden={!open}
|
|
48
|
+
>
|
|
49
|
+
{fonts.map((f) => (
|
|
50
|
+
<button
|
|
51
|
+
key={f.id}
|
|
52
|
+
type="button"
|
|
53
|
+
role="menuitem"
|
|
54
|
+
className={`font-switcher__item ${current === f.id ? 'font-switcher__item--active' : ''}`.trim()}
|
|
55
|
+
onClick={() => {
|
|
56
|
+
setCurrent(f.id);
|
|
57
|
+
setOpen(false);
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
{f.label}
|
|
61
|
+
</button>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default FontSwitcher;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface FooterLink {
|
|
4
|
+
href: string;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface FooterProps extends HTMLAttributes<HTMLElement> {
|
|
9
|
+
siteName?: string;
|
|
10
|
+
year?: number;
|
|
11
|
+
links?: FooterLink[];
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Footer({
|
|
16
|
+
siteName = '',
|
|
17
|
+
year = new Date().getFullYear(),
|
|
18
|
+
links = [],
|
|
19
|
+
className = '',
|
|
20
|
+
...rest
|
|
21
|
+
}: FooterProps) {
|
|
22
|
+
const classes = ['footer', className].filter(Boolean).join(' ').trim();
|
|
23
|
+
return (
|
|
24
|
+
<footer className={classes} {...rest}>
|
|
25
|
+
<div className="footer__container">
|
|
26
|
+
<div className="footer__inner">
|
|
27
|
+
<p className="footer__copyright">
|
|
28
|
+
{siteName && (
|
|
29
|
+
<>
|
|
30
|
+
<span className="footer__site-name">{siteName}</span>
|
|
31
|
+
{' · '}
|
|
32
|
+
</>
|
|
33
|
+
)}
|
|
34
|
+
<span className="footer__year">© {year}</span>
|
|
35
|
+
</p>
|
|
36
|
+
{links && links.length > 0 && (
|
|
37
|
+
<nav className="footer__nav" aria-label="Footer">
|
|
38
|
+
<ul className="footer__links">
|
|
39
|
+
{links.map((link, i) => (
|
|
40
|
+
<li key={i} className="footer__link-item">
|
|
41
|
+
<a className="footer__link" href={link.href}>
|
|
42
|
+
{link.label}
|
|
43
|
+
</a>
|
|
44
|
+
</li>
|
|
45
|
+
))}
|
|
46
|
+
</ul>
|
|
47
|
+
</nav>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</footer>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default Footer;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface FormGroupProps {
|
|
4
|
+
label?: string;
|
|
5
|
+
labelFor?: string;
|
|
6
|
+
required?: boolean;
|
|
7
|
+
help?: string;
|
|
8
|
+
error?: string;
|
|
9
|
+
success?: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
children?: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function FormGroup({
|
|
15
|
+
label,
|
|
16
|
+
labelFor,
|
|
17
|
+
required = false,
|
|
18
|
+
help,
|
|
19
|
+
error,
|
|
20
|
+
success,
|
|
21
|
+
className = '',
|
|
22
|
+
children,
|
|
23
|
+
}: FormGroupProps) {
|
|
24
|
+
const errorId = labelFor && error ? `${labelFor}-error` : undefined;
|
|
25
|
+
const helpId = labelFor && help ? `${labelFor}-help` : undefined;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className={`form-group ${className}`.trim()}>
|
|
29
|
+
{label &&
|
|
30
|
+
(labelFor ? (
|
|
31
|
+
<label htmlFor={labelFor} className={`form-group__label ${required ? 'required' : ''}`.trim()}>
|
|
32
|
+
{label}
|
|
33
|
+
</label>
|
|
34
|
+
) : (
|
|
35
|
+
<span className={`form-group__label ${required ? 'required' : ''}`.trim()}>{label}</span>
|
|
36
|
+
))}
|
|
37
|
+
{children}
|
|
38
|
+
{help && (
|
|
39
|
+
<span id={helpId} className="form-group__help">
|
|
40
|
+
{help}
|
|
41
|
+
</span>
|
|
42
|
+
)}
|
|
43
|
+
{error && (
|
|
44
|
+
<span id={errorId} className="form-error" role="alert">
|
|
45
|
+
{error}
|
|
46
|
+
</span>
|
|
47
|
+
)}
|
|
48
|
+
{success && (
|
|
49
|
+
<span className="form-success" role="status">
|
|
50
|
+
{success}
|
|
51
|
+
</span>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default FormGroup;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
import { useState, useRef } from 'react';
|
|
3
|
+
|
|
4
|
+
export interface HoverCardProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
id?: string;
|
|
6
|
+
openDelay?: number;
|
|
7
|
+
closeDelay?: number;
|
|
8
|
+
open?: boolean;
|
|
9
|
+
onOpenChange?: (open: boolean) => void;
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
trigger?: ReactNode;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function HoverCard({
|
|
16
|
+
id: idProp,
|
|
17
|
+
openDelay = 200,
|
|
18
|
+
closeDelay = 100,
|
|
19
|
+
open = false,
|
|
20
|
+
onOpenChange,
|
|
21
|
+
children,
|
|
22
|
+
trigger,
|
|
23
|
+
className = '',
|
|
24
|
+
...rest
|
|
25
|
+
}: HoverCardProps) {
|
|
26
|
+
const id = idProp ?? `hover-card-${Math.random().toString(36).slice(2, 9)}`;
|
|
27
|
+
const openTRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
28
|
+
const closeTRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
29
|
+
|
|
30
|
+
const openContent = () => {
|
|
31
|
+
if (closeTRef.current) clearTimeout(closeTRef.current);
|
|
32
|
+
openTRef.current = setTimeout(() => onOpenChange?.(true), openDelay);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const closeContent = () => {
|
|
36
|
+
if (openTRef.current) clearTimeout(openTRef.current);
|
|
37
|
+
closeTRef.current = setTimeout(() => onOpenChange?.(false), closeDelay);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className={`hover-card ${className}`.trim()} data-hover-card id={id} {...rest}>
|
|
42
|
+
<div data-hover-card-trigger onMouseEnter={openContent} onMouseLeave={closeContent}>
|
|
43
|
+
{trigger}
|
|
44
|
+
</div>
|
|
45
|
+
<div
|
|
46
|
+
className={`hover-card__content ${open ? 'hover-card__content--open' : ''}`.trim()}
|
|
47
|
+
role="dialog"
|
|
48
|
+
aria-hidden={!open}
|
|
49
|
+
hidden={!open}
|
|
50
|
+
data-hover-card-content
|
|
51
|
+
id={`${id}-content`}
|
|
52
|
+
onMouseEnter={openContent}
|
|
53
|
+
onMouseLeave={closeContent}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default HoverCard;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface IconsProps extends HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
children?: ReactNode;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Icons({ children, className = '', ...rest }: IconsProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className={`icons-grid ${className}`.trim()} data-icons-grid {...rest}>
|
|
11
|
+
{children ?? (
|
|
12
|
+
<>
|
|
13
|
+
<div className="icons-grid__item"><span className="kbd">Icon 1</span></div>
|
|
14
|
+
<div className="icons-grid__item"><span className="kbd">Icon 2</span></div>
|
|
15
|
+
<div className="icons-grid__item"><span className="kbd">Icon 3</span></div>
|
|
16
|
+
</>
|
|
17
|
+
)}
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default Icons;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { InputHTMLAttributes } from 'react';
|
|
2
|
+
|
|
3
|
+
export type InputSize = 'sm' | 'md' | 'lg';
|
|
4
|
+
|
|
5
|
+
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
|
6
|
+
size?: InputSize;
|
|
7
|
+
error?: boolean;
|
|
8
|
+
success?: boolean;
|
|
9
|
+
ariaDescribedby?: string;
|
|
10
|
+
ariaInvalid?: boolean | 'true' | 'false';
|
|
11
|
+
onValueChange?: (value: string) => void;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Input({
|
|
16
|
+
type = 'text',
|
|
17
|
+
id,
|
|
18
|
+
name,
|
|
19
|
+
value = '',
|
|
20
|
+
placeholder,
|
|
21
|
+
required = false,
|
|
22
|
+
disabled = false,
|
|
23
|
+
readOnly = false,
|
|
24
|
+
autoComplete,
|
|
25
|
+
size = 'md',
|
|
26
|
+
error = false,
|
|
27
|
+
success = false,
|
|
28
|
+
className = '',
|
|
29
|
+
ariaDescribedby,
|
|
30
|
+
ariaInvalid,
|
|
31
|
+
onChange,
|
|
32
|
+
onValueChange,
|
|
33
|
+
...rest
|
|
34
|
+
}: InputProps) {
|
|
35
|
+
const sizeClass = size !== 'md' ? `form-input--${size}` : '';
|
|
36
|
+
const errorClass = error ? 'form-input--error' : '';
|
|
37
|
+
const successClass = success ? 'form-input--success' : '';
|
|
38
|
+
const classes = ['form-input', sizeClass, errorClass, successClass, className]
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.join(' ')
|
|
41
|
+
.trim();
|
|
42
|
+
const invalid = error || ariaInvalid === true || ariaInvalid === 'true';
|
|
43
|
+
|
|
44
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
45
|
+
onChange?.(e);
|
|
46
|
+
onValueChange?.(e.target.value);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<input
|
|
51
|
+
type={type}
|
|
52
|
+
id={id}
|
|
53
|
+
name={name}
|
|
54
|
+
value={value}
|
|
55
|
+
placeholder={placeholder}
|
|
56
|
+
required={required}
|
|
57
|
+
disabled={disabled}
|
|
58
|
+
readOnly={readOnly}
|
|
59
|
+
autoComplete={autoComplete}
|
|
60
|
+
className={classes}
|
|
61
|
+
aria-invalid={invalid ? 'true' : 'false'}
|
|
62
|
+
aria-describedby={ariaDescribedby}
|
|
63
|
+
onChange={handleChange}
|
|
64
|
+
{...rest}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default Input;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface KbdProps extends HTMLAttributes<HTMLElement> {
|
|
4
|
+
children?: ReactNode;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Kbd({ className = '', children, ...rest }: KbdProps) {
|
|
9
|
+
return (
|
|
10
|
+
<kbd className={`kbd ${className}`.trim()} {...rest}>
|
|
11
|
+
{children}
|
|
12
|
+
</kbd>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default Kbd;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { LabelHTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface LabelProps extends LabelHTMLAttributes<HTMLLabelElement> {
|
|
4
|
+
children?: ReactNode;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Label({ className = '', children, ...rest }: LabelProps) {
|
|
9
|
+
return (
|
|
10
|
+
<label className={`label ${className}`.trim()} {...rest}>
|
|
11
|
+
{children}
|
|
12
|
+
</label>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default Label;
|