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,62 @@
1
+ import type { HTMLAttributes } from 'react';
2
+ import { useState } from 'react';
3
+
4
+ export interface SearchProps extends HTMLAttributes<HTMLDivElement> {
5
+ id?: string;
6
+ placeholder?: string;
7
+ className?: string;
8
+ }
9
+
10
+ export function Search({
11
+ id: idProp,
12
+ placeholder = 'Search…',
13
+ className = '',
14
+ ...rest
15
+ }: SearchProps) {
16
+ const id = idProp ?? `search-${Math.random().toString(36).slice(2, 9)}`;
17
+ const [open, setOpen] = useState(false);
18
+
19
+ return (
20
+ <div className={`search ${className}`.trim()} data-search id={id} {...rest}>
21
+ <button
22
+ type="button"
23
+ className="search__trigger"
24
+ aria-label="Open search"
25
+ aria-expanded={open}
26
+ aria-controls={`${id}-overlay`}
27
+ onClick={() => setOpen(true)}
28
+ >
29
+ <span className="search__trigger-icon" aria-hidden="true">⌘</span>
30
+ <span className="search__trigger-label">Search</span>
31
+ </button>
32
+ <div
33
+ className={`search__overlay ${open ? 'search__overlay--open' : ''}`.trim()}
34
+ id={`${id}-overlay`}
35
+ role="dialog"
36
+ aria-label="Search"
37
+ aria-hidden={!open}
38
+ hidden={!open}
39
+ >
40
+ <div className="search__overlay-inner">
41
+ <input
42
+ type="search"
43
+ className="search__input"
44
+ placeholder={placeholder}
45
+ autoFocus
46
+ aria-label="Search"
47
+ />
48
+ <button
49
+ type="button"
50
+ className="search__close"
51
+ aria-label="Close search"
52
+ onClick={() => setOpen(false)}
53
+ >
54
+ ×
55
+ </button>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ );
60
+ }
61
+
62
+ export default Search;
@@ -0,0 +1,65 @@
1
+ import type { SelectHTMLAttributes, ReactNode } from 'react';
2
+
3
+ export type SelectSize = 'sm' | 'md' | 'lg';
4
+
5
+ export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
6
+ size?: SelectSize;
7
+ error?: boolean;
8
+ success?: boolean;
9
+ ariaDescribedby?: string;
10
+ ariaInvalid?: boolean | 'true' | 'false';
11
+ onValueChange?: (value: string) => void;
12
+ className?: string;
13
+ children?: ReactNode;
14
+ }
15
+
16
+ export function Select({
17
+ id,
18
+ name,
19
+ value = '',
20
+ required = false,
21
+ disabled = false,
22
+ size = 'md',
23
+ error = false,
24
+ success = false,
25
+ className = '',
26
+ ariaDescribedby,
27
+ ariaInvalid,
28
+ children,
29
+ onChange,
30
+ onValueChange,
31
+ ...rest
32
+ }: SelectProps) {
33
+ const sizeClass = size !== 'md' ? `form-input--${size}` : '';
34
+ const errorClass = error ? 'form-input--error' : '';
35
+ const successClass = success ? 'form-input--success' : '';
36
+ const classes = ['form-input', sizeClass, errorClass, successClass, className]
37
+ .filter(Boolean)
38
+ .join(' ')
39
+ .trim();
40
+ const invalid = error || ariaInvalid === true || ariaInvalid === 'true';
41
+
42
+ const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
43
+ onChange?.(e);
44
+ onValueChange?.(e.target.value);
45
+ };
46
+
47
+ return (
48
+ <select
49
+ id={id}
50
+ name={name}
51
+ value={value}
52
+ required={required}
53
+ disabled={disabled}
54
+ className={classes}
55
+ aria-invalid={invalid ? 'true' : 'false'}
56
+ aria-describedby={ariaDescribedby}
57
+ onChange={handleChange}
58
+ {...rest}
59
+ >
60
+ {children}
61
+ </select>
62
+ );
63
+ }
64
+
65
+ export default Select;
@@ -0,0 +1,33 @@
1
+ import type { HTMLAttributes } from 'react';
2
+
3
+ export interface SeparatorProps extends HTMLAttributes<HTMLDivElement> {
4
+ orientation?: 'horizontal' | 'vertical';
5
+ decorative?: boolean;
6
+ className?: string;
7
+ }
8
+
9
+ export function Separator({
10
+ orientation = 'horizontal',
11
+ decorative = true,
12
+ className = '',
13
+ ...rest
14
+ }: SeparatorProps) {
15
+ const classes = [
16
+ 'separator',
17
+ `separator--${orientation}`,
18
+ className,
19
+ ]
20
+ .filter(Boolean)
21
+ .join(' ')
22
+ .trim();
23
+ return (
24
+ <div
25
+ className={classes}
26
+ role={decorative ? 'none' : 'separator'}
27
+ aria-orientation={decorative ? undefined : orientation}
28
+ {...rest}
29
+ />
30
+ );
31
+ }
32
+
33
+ export default Separator;
@@ -0,0 +1,60 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+ import { useEffect } from 'react';
3
+
4
+ export interface SettingsProps extends HTMLAttributes<HTMLDivElement> {
5
+ id?: string;
6
+ title?: string;
7
+ open?: boolean;
8
+ onOpenChange?: (open: boolean) => void;
9
+ children?: ReactNode;
10
+ className?: string;
11
+ }
12
+
13
+ export function Settings({
14
+ id: idProp,
15
+ title = 'Settings',
16
+ open = false,
17
+ onOpenChange,
18
+ children,
19
+ className = '',
20
+ ...rest
21
+ }: SettingsProps) {
22
+ const id = idProp ?? `settings-${Math.random().toString(36).slice(2, 9)}`;
23
+
24
+ useEffect(() => {
25
+ if (!open) return;
26
+ const handleKey = (e: KeyboardEvent) => {
27
+ if (e.key === 'Escape') {
28
+ e.preventDefault();
29
+ onOpenChange?.(false);
30
+ }
31
+ };
32
+ document.addEventListener('keydown', handleKey);
33
+ return () => document.removeEventListener('keydown', handleKey);
34
+ }, [open, onOpenChange]);
35
+
36
+ return (
37
+ <div
38
+ className={`settings ${open ? 'settings--open' : ''} ${className}`.trim()}
39
+ id={id}
40
+ data-settings
41
+ role="dialog"
42
+ aria-modal="true"
43
+ aria-labelledby={`${id}-title`}
44
+ aria-hidden={!open}
45
+ hidden={!open}
46
+ {...rest}
47
+ >
48
+ <div className="settings__backdrop" onClick={() => onOpenChange?.(false)} aria-hidden="true" />
49
+ <div className="settings__panel">
50
+ <div className="settings__header">
51
+ <h2 id={`${id}-title`} className="settings__title">{title}</h2>
52
+ <button type="button" className="settings__close" aria-label="Close settings" onClick={() => onOpenChange?.(false)}>×</button>
53
+ </div>
54
+ <div className="settings__body">{children}</div>
55
+ </div>
56
+ </div>
57
+ );
58
+ }
59
+
60
+ export default Settings;
@@ -0,0 +1,86 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+ import { useEffect } from 'react';
3
+
4
+ export type SheetSide = 'top' | 'right' | 'bottom' | 'left';
5
+
6
+ export interface SheetProps extends HTMLAttributes<HTMLDivElement> {
7
+ id?: string;
8
+ title?: string;
9
+ side?: SheetSide;
10
+ open?: boolean;
11
+ onOpenChange?: (open: boolean) => void;
12
+ children?: ReactNode;
13
+ className?: string;
14
+ }
15
+
16
+ export function Sheet({
17
+ id: idProp,
18
+ title,
19
+ side = 'right',
20
+ open = false,
21
+ onOpenChange,
22
+ children,
23
+ className = '',
24
+ onKeyDown,
25
+ ...rest
26
+ }: SheetProps) {
27
+ const id = idProp ?? `sheet-${Math.random().toString(36).slice(2, 9)}`;
28
+
29
+ const close = () => onOpenChange?.(false);
30
+
31
+ useEffect(() => {
32
+ const handleKey = (e: KeyboardEvent) => {
33
+ if (e.key === 'Escape') close();
34
+ };
35
+ if (open) document.addEventListener('keydown', handleKey);
36
+ return () => document.removeEventListener('keydown', handleKey);
37
+ }, [open]);
38
+
39
+ const handleKeyDown = (e: React.KeyboardEvent) => {
40
+ onKeyDown?.(e);
41
+ if (e.key === 'Escape') close();
42
+ };
43
+
44
+ return (
45
+ <>
46
+ <div
47
+ className={`sheet__overlay ${open ? 'sheet__overlay--open' : ''}`.trim()}
48
+ data-sheet-overlay
49
+ aria-hidden={!open}
50
+ id={`${id}-overlay`}
51
+ onClick={close}
52
+ role="presentation"
53
+ />
54
+ <div
55
+ className={`sheet sheet--${side} ${open ? 'sheet--open' : ''} ${className}`.trim()}
56
+ role="dialog"
57
+ aria-modal="true"
58
+ aria-labelledby={title ? `${id}-title` : undefined}
59
+ aria-hidden={!open}
60
+ id={id}
61
+ data-sheet
62
+ hidden={!open}
63
+ onKeyDown={handleKeyDown}
64
+ {...rest}
65
+ >
66
+ <div className="sheet__content">
67
+ {title && (
68
+ <div className="sheet__header">
69
+ <h2 id={`${id}-title`} className="sheet__title">
70
+ {title}
71
+ </h2>
72
+ <button type="button" className="sheet__close" aria-label="Close" onClick={close}>
73
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
74
+ <path d="M18 6L6 18M6 6l12 12" />
75
+ </svg>
76
+ </button>
77
+ </div>
78
+ )}
79
+ <div className="sheet__body">{children}</div>
80
+ </div>
81
+ </div>
82
+ </>
83
+ );
84
+ }
85
+
86
+ export default Sheet;
@@ -0,0 +1,32 @@
1
+ import type { HTMLAttributes } from 'react';
2
+
3
+ export type SkeletonVariant = 'text' | 'circle' | 'rect' | 'default';
4
+
5
+ export interface SkeletonProps extends HTMLAttributes<HTMLSpanElement> {
6
+ variant?: SkeletonVariant;
7
+ label?: string;
8
+ className?: string;
9
+ }
10
+
11
+ export function Skeleton({
12
+ variant = 'default',
13
+ label = 'Loading',
14
+ className = '',
15
+ ...rest
16
+ }: SkeletonProps) {
17
+ const variantClass = variant !== 'default' ? `skeleton--${variant}` : '';
18
+ const classes = ['skeleton', variantClass, className].filter(Boolean).join(' ').trim();
19
+ const isDecorative = label === '';
20
+ return (
21
+ <span
22
+ className={classes}
23
+ role={isDecorative ? undefined : 'status'}
24
+ aria-label={isDecorative ? undefined : label}
25
+ aria-busy={isDecorative ? undefined : 'true'}
26
+ aria-hidden={isDecorative ? 'true' : undefined}
27
+ {...rest}
28
+ />
29
+ );
30
+ }
31
+
32
+ export default Skeleton;
@@ -0,0 +1,66 @@
1
+ import type { HTMLAttributes, InputHTMLAttributes } from 'react';
2
+
3
+ export interface SliderProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
4
+ id?: string;
5
+ name?: string;
6
+ min?: number;
7
+ max?: number;
8
+ step?: number;
9
+ value?: number;
10
+ disabled?: boolean;
11
+ ariaLabel?: string;
12
+ onValueChange?: (value: number) => void;
13
+ className?: string;
14
+ }
15
+
16
+ export function Slider({
17
+ id: idProp,
18
+ name,
19
+ min = 0,
20
+ max = 100,
21
+ step = 1,
22
+ value = min,
23
+ disabled = false,
24
+ ariaLabel,
25
+ onValueChange,
26
+ onChange,
27
+ className = '',
28
+ ...rest
29
+ }: SliderProps) {
30
+ const id = idProp ?? `slider-${Math.random().toString(36).slice(2, 9)}`;
31
+ const pct = Math.min(100, Math.max(0, ((value - min) / (max - min)) * 100));
32
+
33
+ const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
34
+ onChange?.(e);
35
+ const v = parseFloat(e.target.value);
36
+ if (!Number.isNaN(v)) onValueChange?.(v);
37
+ };
38
+
39
+ return (
40
+ <div className={`slider ${className}`.trim()} data-slider>
41
+ <input
42
+ type="range"
43
+ id={id}
44
+ name={name}
45
+ className="slider__input"
46
+ min={min}
47
+ max={max}
48
+ step={step}
49
+ value={value}
50
+ disabled={disabled}
51
+ aria-valuemin={min}
52
+ aria-valuemax={max}
53
+ aria-valuenow={value}
54
+ aria-label={ariaLabel}
55
+ data-slider-input
56
+ onChange={handleInput}
57
+ {...rest}
58
+ />
59
+ <div className="slider__track" aria-hidden="true">
60
+ <div className="slider__fill" data-slider-fill style={{ width: `${pct}%` }} />
61
+ </div>
62
+ </div>
63
+ );
64
+ }
65
+
66
+ export default Slider;
@@ -0,0 +1,15 @@
1
+ import { useState } from 'react';
2
+ import { Switch } from './Switch';
3
+
4
+ export function SoundEffects() {
5
+ const [on, setOn] = useState(false);
6
+ return (
7
+ <div className="sound-effects" data-sound-effects>
8
+ <Switch
9
+ label="Play sound on click"
10
+ checked={on}
11
+ onCheckedChange={setOn}
12
+ />
13
+ </div>
14
+ );
15
+ }
@@ -0,0 +1,36 @@
1
+ import type { HTMLAttributes } from 'react';
2
+
3
+ export type SpinnerVariant = 'primary' | 'success' | 'warning' | 'error' | 'info';
4
+ export type SpinnerSize = 'sm' | 'md' | 'lg';
5
+
6
+ export interface SpinnerProps extends HTMLAttributes<HTMLSpanElement> {
7
+ size?: SpinnerSize;
8
+ variant?: SpinnerVariant;
9
+ label?: string;
10
+ className?: string;
11
+ }
12
+
13
+ export function Spinner({
14
+ size = 'md',
15
+ variant = 'primary',
16
+ label = 'Loading',
17
+ className = '',
18
+ ...rest
19
+ }: SpinnerProps) {
20
+ const classes = [
21
+ 'spinner',
22
+ `spinner--${size}`,
23
+ `spinner--${variant}`,
24
+ className,
25
+ ]
26
+ .filter(Boolean)
27
+ .join(' ')
28
+ .trim();
29
+ return (
30
+ <span className={classes} role="status" aria-label={label} {...rest}>
31
+ <span className="spinner__ring" aria-hidden="true" />
32
+ </span>
33
+ );
34
+ }
35
+
36
+ export default Spinner;
@@ -0,0 +1,52 @@
1
+ import type { InputHTMLAttributes, LabelHTMLAttributes } from 'react';
2
+
3
+ export interface SwitchProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
4
+ id?: string;
5
+ name?: string;
6
+ checked?: boolean;
7
+ disabled?: boolean;
8
+ label?: string;
9
+ onCheckedChange?: (checked: boolean) => void;
10
+ className?: string;
11
+ }
12
+
13
+ export function Switch({
14
+ id: idProp,
15
+ name,
16
+ checked = false,
17
+ disabled = false,
18
+ label,
19
+ onCheckedChange,
20
+ onChange,
21
+ className = '',
22
+ ...rest
23
+ }: SwitchProps) {
24
+ const id = idProp ?? `switch-${Math.random().toString(36).slice(2, 11)}`;
25
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
26
+ onChange?.(e);
27
+ onCheckedChange?.(e.target.checked);
28
+ };
29
+ const labelProps: LabelHTMLAttributes<HTMLLabelElement> = { htmlFor: id, className: `switch ${className}`.trim() };
30
+ return (
31
+ <label {...labelProps}>
32
+ <input
33
+ type="checkbox"
34
+ id={id}
35
+ name={name}
36
+ checked={checked}
37
+ disabled={disabled}
38
+ className="switch__input"
39
+ role="switch"
40
+ aria-checked={checked}
41
+ onChange={handleChange}
42
+ {...rest}
43
+ />
44
+ <span className="switch__track" aria-hidden="true">
45
+ <span className="switch__thumb" />
46
+ </span>
47
+ {label && <span className="switch__label">{label}</span>}
48
+ </label>
49
+ );
50
+ }
51
+
52
+ export default Switch;
@@ -0,0 +1,178 @@
1
+ import type { HTMLAttributes } from 'react';
2
+ import { useState, useMemo } from 'react';
3
+
4
+ export interface TableColumn {
5
+ key: string;
6
+ label: string;
7
+ sortable?: boolean;
8
+ type?: 'string' | 'number';
9
+ }
10
+
11
+ export interface TableProps extends HTMLAttributes<HTMLDivElement> {
12
+ columns: TableColumn[];
13
+ data: Record<string, string | number>[];
14
+ caption?: string;
15
+ sortable?: boolean;
16
+ filterable?: boolean;
17
+ filterPlaceholder?: string;
18
+ striped?: boolean;
19
+ className?: string;
20
+ }
21
+
22
+ export function Table({
23
+ columns,
24
+ data,
25
+ caption,
26
+ sortable = true,
27
+ filterable = false,
28
+ filterPlaceholder = 'Filter table…',
29
+ striped = true,
30
+ className = '',
31
+ ...rest
32
+ }: TableProps) {
33
+ const tableId = `table-${Math.random().toString(36).slice(2, 11)}`;
34
+ const [sortColumnIndex, setSortColumnIndex] = useState<number | null>(null);
35
+ const [sortDirection, setSortDirection] = useState<'ascending' | 'descending'>('ascending');
36
+ const [filterQuery, setFilterQuery] = useState('');
37
+
38
+ const filteredData = useMemo(() => {
39
+ if (!filterable || !filterQuery.trim()) return data;
40
+ const q = filterQuery.trim().toLowerCase();
41
+ return data.filter((row) =>
42
+ columns.some((col) => String(row[col.key] ?? '').toLowerCase().includes(q))
43
+ );
44
+ }, [data, columns, filterable, filterQuery]);
45
+
46
+ const sortedData = useMemo(() => {
47
+ if (!sortable || sortColumnIndex === null) return filteredData;
48
+ const col = columns[sortColumnIndex];
49
+ const type = col?.type ?? 'string';
50
+ const dir = sortDirection === 'ascending' ? 1 : -1;
51
+ return [...filteredData].sort((a, b) => {
52
+ const aVal = a[col.key];
53
+ const bVal = b[col.key];
54
+ if (type === 'number') {
55
+ const aNum = parseFloat(String(aVal).replace(/[^0-9.-]/g, '')) || 0;
56
+ const bNum = parseFloat(String(bVal).replace(/[^0-9.-]/g, '')) || 0;
57
+ return dir * (aNum - bNum);
58
+ }
59
+ const cmp = String(aVal).localeCompare(String(bVal), undefined, { numeric: true });
60
+ return dir * cmp;
61
+ });
62
+ }, [filteredData, columns, sortable, sortColumnIndex, sortDirection]);
63
+
64
+ function getSortState(colIndex: number): 'none' | 'ascending' | 'descending' {
65
+ if (sortColumnIndex !== colIndex) return 'none';
66
+ return sortDirection;
67
+ }
68
+
69
+ function handleSort(colIndex: number) {
70
+ if (sortColumnIndex === colIndex) {
71
+ setSortDirection((d) => (d === 'ascending' ? 'descending' : 'ascending'));
72
+ } else {
73
+ setSortColumnIndex(colIndex);
74
+ setSortDirection('ascending');
75
+ }
76
+ }
77
+
78
+ const classes = [
79
+ 'table',
80
+ striped ? 'table--striped' : '',
81
+ sortable ? 'table--sortable' : '',
82
+ filterable ? 'table--filterable' : '',
83
+ className,
84
+ ]
85
+ .filter(Boolean)
86
+ .join(' ')
87
+ .trim();
88
+
89
+ return (
90
+ <div
91
+ className={classes}
92
+ data-table-id={tableId}
93
+ data-table-sortable={sortable ? 'true' : undefined}
94
+ data-table-filterable={filterable ? 'true' : undefined}
95
+ {...rest}
96
+ >
97
+ {filterable && (
98
+ <div className="table__filter-wrap">
99
+ <label htmlFor={`${tableId}-filter`} className="sr-only">
100
+ Filter table
101
+ </label>
102
+ <span className="table__filter-icon" aria-hidden="true">
103
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="table__filter-icon-svg icon" aria-hidden="true">
104
+ <path d="M22 3H2l8 9.46V19l4 2v-8.54L22 3z" />
105
+ </svg>
106
+ </span>
107
+ <input
108
+ type="search"
109
+ id={`${tableId}-filter`}
110
+ className="table__filter"
111
+ placeholder={filterPlaceholder}
112
+ aria-controls={tableId}
113
+ data-table-filter
114
+ autoComplete="off"
115
+ value={filterQuery}
116
+ onChange={(e) => setFilterQuery(e.target.value)}
117
+ />
118
+ </div>
119
+ )}
120
+ <div className="table__wrapper">
121
+ <table className="table__table" id={tableId}>
122
+ {caption && <caption className="table__caption">{caption}</caption>}
123
+ <thead className="table__head">
124
+ <tr className="table__row">
125
+ {columns.map((col, i) => {
126
+ const canSort = sortable && col.sortable !== false;
127
+ return (
128
+ <th
129
+ key={col.key}
130
+ className="table__cell table__cell--head"
131
+ scope="col"
132
+ data-column-index={i}
133
+ data-sortable={canSort ? 'true' : undefined}
134
+ data-type={col.type ?? 'string'}
135
+ aria-sort={canSort ? getSortState(i) : undefined}
136
+ >
137
+ {canSort ? (
138
+ <button
139
+ type="button"
140
+ className="table__sort-trigger"
141
+ data-column-index={i}
142
+ aria-label={`Sort by ${col.label}`}
143
+ onClick={() => handleSort(i)}
144
+ >
145
+ <span className="table__cell-content">{col.label}</span>
146
+ <span className="table__sort-icon" aria-hidden="true">
147
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="table__sort-icon-svg icon" aria-hidden="true">
148
+ <path d="m7 15 5 5 5-5" />
149
+ <path d="m7 9 5-5 5 5" />
150
+ </svg>
151
+ </span>
152
+ </button>
153
+ ) : (
154
+ <span className="table__cell-content">{col.label}</span>
155
+ )}
156
+ </th>
157
+ );
158
+ })}
159
+ </tr>
160
+ </thead>
161
+ <tbody className="table__body">
162
+ {sortedData.map((row, rowIndex) => (
163
+ <tr key={rowIndex} className="table__row" data-row-index={rowIndex}>
164
+ {columns.map((col) => (
165
+ <td key={col.key} className="table__cell" data-column-key={col.key}>
166
+ {row[col.key] ?? ''}
167
+ </td>
168
+ ))}
169
+ </tr>
170
+ ))}
171
+ </tbody>
172
+ </table>
173
+ </div>
174
+ </div>
175
+ );
176
+ }
177
+
178
+ export default Table;