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.
Files changed (171) hide show
  1. package/README.md +9 -5
  2. package/bin/rizzo-css.js +247 -27
  3. package/dist/rizzo.min.css +5 -3
  4. package/package.json +14 -7
  5. package/scaffold/astro/Footer.astro +8 -0
  6. package/scaffold/astro/Settings.astro +8 -2
  7. package/scaffold/astro/Tabs.astro +2 -2
  8. package/scaffold/react/Accordion.tsx +143 -0
  9. package/scaffold/react/Alert.tsx +90 -0
  10. package/scaffold/react/AlertDialog.tsx +80 -0
  11. package/scaffold/react/AspectRatio.tsx +32 -0
  12. package/scaffold/react/Avatar.tsx +53 -0
  13. package/scaffold/react/BackToTop.tsx +62 -0
  14. package/scaffold/react/Badge.tsx +39 -0
  15. package/scaffold/react/Breadcrumb.tsx +81 -0
  16. package/scaffold/react/Button.tsx +40 -0
  17. package/scaffold/react/ButtonGroup.tsx +24 -0
  18. package/scaffold/react/Card.tsx +26 -0
  19. package/scaffold/react/Checkbox.tsx +40 -0
  20. package/scaffold/react/Collapsible.tsx +58 -0
  21. package/scaffold/react/ContextMenu.tsx +67 -0
  22. package/scaffold/react/CopyToClipboard.tsx +128 -0
  23. package/scaffold/react/Dashboard.tsx +23 -0
  24. package/scaffold/react/Divider.tsx +47 -0
  25. package/scaffold/react/DocsSidebar.tsx +48 -0
  26. package/scaffold/react/Dropdown.tsx +256 -0
  27. package/scaffold/react/Empty.tsx +29 -0
  28. package/scaffold/react/FontSwitcher.tsx +68 -0
  29. package/scaffold/react/Footer.tsx +55 -0
  30. package/scaffold/react/FormGroup.tsx +57 -0
  31. package/scaffold/react/HoverCard.tsx +61 -0
  32. package/scaffold/react/Icons.tsx +22 -0
  33. package/scaffold/react/Input.tsx +69 -0
  34. package/scaffold/react/Kbd.tsx +16 -0
  35. package/scaffold/react/Label.tsx +16 -0
  36. package/scaffold/react/Modal.tsx +149 -0
  37. package/scaffold/react/Navbar.tsx +72 -0
  38. package/scaffold/react/Pagination.tsx +155 -0
  39. package/scaffold/react/Popover.tsx +66 -0
  40. package/scaffold/react/ProgressBar.tsx +66 -0
  41. package/scaffold/react/Radio.tsx +38 -0
  42. package/scaffold/react/ResizableHandle.tsx +24 -0
  43. package/scaffold/react/ResizablePane.tsx +29 -0
  44. package/scaffold/react/ResizablePaneGroup.tsx +29 -0
  45. package/scaffold/react/ScrollArea.tsx +29 -0
  46. package/scaffold/react/Search.tsx +62 -0
  47. package/scaffold/react/Select.tsx +65 -0
  48. package/scaffold/react/Separator.tsx +33 -0
  49. package/scaffold/react/Settings.tsx +60 -0
  50. package/scaffold/react/Sheet.tsx +86 -0
  51. package/scaffold/react/Skeleton.tsx +32 -0
  52. package/scaffold/react/Slider.tsx +66 -0
  53. package/scaffold/react/SoundEffects.tsx +15 -0
  54. package/scaffold/react/Spinner.tsx +36 -0
  55. package/scaffold/react/Switch.tsx +52 -0
  56. package/scaffold/react/Table.tsx +178 -0
  57. package/scaffold/react/Tabs.tsx +143 -0
  58. package/scaffold/react/Textarea.tsx +69 -0
  59. package/scaffold/react/ThemeSwitcher.tsx +89 -0
  60. package/scaffold/react/Toast.tsx +43 -0
  61. package/scaffold/react/Toggle.tsx +45 -0
  62. package/scaffold/react/ToggleGroup.tsx +34 -0
  63. package/scaffold/react/Tooltip.tsx +40 -0
  64. package/scaffold/vanilla/README-RIZZO.md +1 -1
  65. package/scaffold/vanilla/components/accordion.html +30 -0
  66. package/scaffold/vanilla/components/alert-dialog.html +30 -0
  67. package/scaffold/vanilla/components/alert.html +30 -0
  68. package/scaffold/vanilla/components/aspect-ratio.html +30 -0
  69. package/scaffold/vanilla/components/avatar.html +30 -0
  70. package/scaffold/vanilla/components/back-to-top.html +30 -0
  71. package/scaffold/vanilla/components/badge.html +30 -0
  72. package/scaffold/vanilla/components/breadcrumb.html +30 -0
  73. package/scaffold/vanilla/components/button-group.html +30 -0
  74. package/scaffold/vanilla/components/button.html +30 -0
  75. package/scaffold/vanilla/components/cards.html +30 -0
  76. package/scaffold/vanilla/components/collapsible.html +30 -0
  77. package/scaffold/vanilla/components/context-menu.html +30 -0
  78. package/scaffold/vanilla/components/copy-to-clipboard.html +30 -0
  79. package/scaffold/vanilla/components/dashboard.html +30 -0
  80. package/scaffold/vanilla/components/divider.html +30 -0
  81. package/scaffold/vanilla/components/docs-sidebar.html +30 -0
  82. package/scaffold/vanilla/components/dropdown.html +30 -0
  83. package/scaffold/vanilla/components/empty.html +30 -0
  84. package/scaffold/vanilla/components/font-switcher.html +30 -0
  85. package/scaffold/vanilla/components/footer.html +30 -0
  86. package/scaffold/vanilla/components/forms.html +30 -0
  87. package/scaffold/vanilla/components/hover-card.html +30 -0
  88. package/scaffold/vanilla/components/icons.html +30 -0
  89. package/scaffold/vanilla/components/index.html +30 -0
  90. package/scaffold/vanilla/components/kbd.html +30 -0
  91. package/scaffold/vanilla/components/label.html +30 -0
  92. package/scaffold/vanilla/components/modal.html +30 -0
  93. package/scaffold/vanilla/components/navbar.html +30 -0
  94. package/scaffold/vanilla/components/pagination.html +30 -0
  95. package/scaffold/vanilla/components/popover.html +30 -0
  96. package/scaffold/vanilla/components/progress-bar.html +30 -0
  97. package/scaffold/vanilla/components/resizable.html +30 -0
  98. package/scaffold/vanilla/components/scroll-area.html +30 -0
  99. package/scaffold/vanilla/components/search.html +30 -0
  100. package/scaffold/vanilla/components/separator.html +30 -0
  101. package/scaffold/vanilla/components/settings.html +30 -0
  102. package/scaffold/vanilla/components/sheet.html +30 -0
  103. package/scaffold/vanilla/components/skeleton.html +30 -0
  104. package/scaffold/vanilla/components/slider.html +30 -0
  105. package/scaffold/vanilla/components/sound-effects.html +30 -0
  106. package/scaffold/vanilla/components/spinner.html +30 -0
  107. package/scaffold/vanilla/components/switch.html +30 -0
  108. package/scaffold/vanilla/components/table.html +30 -0
  109. package/scaffold/vanilla/components/tabs.html +30 -0
  110. package/scaffold/vanilla/components/theme-switcher.html +30 -0
  111. package/scaffold/vanilla/components/toast.html +30 -0
  112. package/scaffold/vanilla/components/toggle-group.html +30 -0
  113. package/scaffold/vanilla/components/toggle.html +30 -0
  114. package/scaffold/vanilla/components/tooltip.html +30 -0
  115. package/scaffold/vanilla/index.html +30 -0
  116. package/scaffold/vue/Accordion.vue +9 -0
  117. package/scaffold/vue/Alert.vue +9 -0
  118. package/scaffold/vue/AlertDialog.vue +9 -0
  119. package/scaffold/vue/AspectRatio.vue +9 -0
  120. package/scaffold/vue/Avatar.vue +9 -0
  121. package/scaffold/vue/BackToTop.vue +9 -0
  122. package/scaffold/vue/Badge.vue +28 -0
  123. package/scaffold/vue/Breadcrumb.vue +9 -0
  124. package/scaffold/vue/Button.vue +23 -0
  125. package/scaffold/vue/ButtonGroup.vue +9 -0
  126. package/scaffold/vue/Card.vue +21 -0
  127. package/scaffold/vue/Checkbox.vue +31 -0
  128. package/scaffold/vue/Collapsible.vue +9 -0
  129. package/scaffold/vue/ContextMenu.vue +9 -0
  130. package/scaffold/vue/CopyToClipboard.vue +9 -0
  131. package/scaffold/vue/Dashboard.vue +9 -0
  132. package/scaffold/vue/Divider.vue +23 -0
  133. package/scaffold/vue/DocsSidebar.vue +9 -0
  134. package/scaffold/vue/Dropdown.vue +9 -0
  135. package/scaffold/vue/Empty.vue +9 -0
  136. package/scaffold/vue/FontSwitcher.vue +9 -0
  137. package/scaffold/vue/Footer.vue +9 -0
  138. package/scaffold/vue/FormGroup.vue +45 -0
  139. package/scaffold/vue/HoverCard.vue +9 -0
  140. package/scaffold/vue/Icons.vue +9 -0
  141. package/scaffold/vue/Input.vue +59 -0
  142. package/scaffold/vue/Kbd.vue +9 -0
  143. package/scaffold/vue/Label.vue +23 -0
  144. package/scaffold/vue/Modal.vue +9 -0
  145. package/scaffold/vue/Navbar.vue +9 -0
  146. package/scaffold/vue/Pagination.vue +9 -0
  147. package/scaffold/vue/Popover.vue +9 -0
  148. package/scaffold/vue/ProgressBar.vue +9 -0
  149. package/scaffold/vue/Radio.vue +29 -0
  150. package/scaffold/vue/ResizableHandle.vue +9 -0
  151. package/scaffold/vue/ResizablePane.vue +9 -0
  152. package/scaffold/vue/ResizablePaneGroup.vue +9 -0
  153. package/scaffold/vue/ScrollArea.vue +9 -0
  154. package/scaffold/vue/Search.vue +9 -0
  155. package/scaffold/vue/Select.vue +52 -0
  156. package/scaffold/vue/Separator.vue +9 -0
  157. package/scaffold/vue/Settings.vue +9 -0
  158. package/scaffold/vue/Sheet.vue +9 -0
  159. package/scaffold/vue/Skeleton.vue +9 -0
  160. package/scaffold/vue/Slider.vue +9 -0
  161. package/scaffold/vue/SoundEffects.vue +9 -0
  162. package/scaffold/vue/Spinner.vue +21 -0
  163. package/scaffold/vue/Switch.vue +9 -0
  164. package/scaffold/vue/Table.vue +9 -0
  165. package/scaffold/vue/Tabs.vue +9 -0
  166. package/scaffold/vue/Textarea.vue +60 -0
  167. package/scaffold/vue/ThemeSwitcher.vue +9 -0
  168. package/scaffold/vue/Toast.vue +9 -0
  169. package/scaffold/vue/Toggle.vue +9 -0
  170. package/scaffold/vue/ToggleGroup.vue +9 -0
  171. 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;