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,149 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+ import { useEffect, useRef, useCallback } from 'react';
3
+
4
+ const FOCUSABLE_SELECTORS = [
5
+ 'button:not([disabled])',
6
+ 'a[href]',
7
+ 'input:not([disabled])',
8
+ 'select:not([disabled])',
9
+ 'textarea:not([disabled])',
10
+ '[tabindex]:not([tabindex="-1"])',
11
+ ].join(', ');
12
+
13
+ function getFocusableElements(container: HTMLElement): HTMLElement[] {
14
+ return Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTORS));
15
+ }
16
+
17
+ export interface ModalProps extends HTMLAttributes<HTMLDivElement> {
18
+ id?: string;
19
+ title?: string;
20
+ size?: 'sm' | 'md' | 'lg';
21
+ open?: boolean;
22
+ onOpenChange?: (open: boolean) => void;
23
+ closeOnOverlayClick?: boolean;
24
+ closeOnEscape?: boolean;
25
+ children?: ReactNode;
26
+ footer?: ReactNode;
27
+ className?: string;
28
+ }
29
+
30
+ export function Modal({
31
+ id: idProp,
32
+ title = 'Modal',
33
+ size = 'md',
34
+ open = false,
35
+ onOpenChange,
36
+ closeOnOverlayClick = true,
37
+ closeOnEscape = true,
38
+ children,
39
+ footer,
40
+ className = '',
41
+ ...rest
42
+ }: ModalProps) {
43
+ const modalId = idProp ?? `modal-${Math.random().toString(36).slice(2, 11)}`;
44
+ const sizeClass = size !== 'md' ? `modal--${size}` : '';
45
+ const classes = ['modal', sizeClass, className].filter(Boolean).join(' ').trim();
46
+ const overlayRef = useRef<HTMLDivElement>(null);
47
+ const modalRef = useRef<HTMLDivElement>(null);
48
+ const previousActiveRef = useRef<HTMLElement | null>(null);
49
+
50
+ const close = useCallback(() => {
51
+ onOpenChange?.(false);
52
+ previousActiveRef.current?.focus();
53
+ previousActiveRef.current = null;
54
+ }, [onOpenChange]);
55
+
56
+ useEffect(() => {
57
+ if (!open) return;
58
+ previousActiveRef.current = document.activeElement as HTMLElement | null;
59
+ const modalEl = modalRef.current;
60
+ if (!modalEl) return;
61
+ const focusable = getFocusableElements(modalEl);
62
+ const closeBtn = modalEl.querySelector<HTMLElement>('[data-modal-close]');
63
+ const first = focusable[0] ?? closeBtn;
64
+ first?.focus();
65
+
66
+ const keyHandler = (e: KeyboardEvent) => {
67
+ if (e.key === 'Escape' && closeOnEscape) {
68
+ e.preventDefault();
69
+ close();
70
+ return;
71
+ }
72
+ if (e.key === 'Tab') {
73
+ const focusableEls = getFocusableElements(modalEl);
74
+ if (focusableEls.length === 0) return;
75
+ const firstEl = focusableEls[0];
76
+ const lastEl = focusableEls[focusableEls.length - 1];
77
+ const active = document.activeElement as HTMLElement | null;
78
+ if (e.shiftKey) {
79
+ if (active === firstEl || !modalEl.contains(active)) {
80
+ e.preventDefault();
81
+ lastEl.focus();
82
+ }
83
+ } else {
84
+ if (active === lastEl || !modalEl.contains(active)) {
85
+ e.preventDefault();
86
+ firstEl.focus();
87
+ }
88
+ }
89
+ }
90
+ };
91
+ document.addEventListener('keydown', keyHandler);
92
+ return () => document.removeEventListener('keydown', keyHandler);
93
+ }, [open, closeOnEscape, close]);
94
+
95
+ useEffect(() => {
96
+ if (overlayRef.current) (overlayRef.current as any).inert = !open;
97
+ if (modalRef.current) (modalRef.current as any).inert = !open;
98
+ }, [open]);
99
+
100
+ const handleOverlayClick = (e: React.MouseEvent) => {
101
+ if (closeOnOverlayClick && e.target === overlayRef.current) close();
102
+ };
103
+
104
+ return (
105
+ <>
106
+ <div
107
+ ref={overlayRef}
108
+ className="modal__overlay"
109
+ data-modal-overlay
110
+ aria-hidden={!open}
111
+ {...(open ? {} : { inert: true })}
112
+ {...(!open && { hidden: true })}
113
+ id={`${modalId}-overlay`}
114
+ onClick={handleOverlayClick}
115
+ role="presentation"
116
+ />
117
+ <div
118
+ ref={modalRef}
119
+ className={classes}
120
+ role="dialog"
121
+ aria-modal="true"
122
+ aria-labelledby={`${modalId}-title`}
123
+ aria-hidden={!open}
124
+ {...(open ? {} : { inert: true })}
125
+ {...(!open && { hidden: true })}
126
+ id={modalId}
127
+ data-modal
128
+ data-open={open || undefined}
129
+ {...rest}
130
+ >
131
+ <div className="modal__header">
132
+ <h2 id={`${modalId}-title`} className="modal__title">
133
+ {title}
134
+ </h2>
135
+ <button type="button" className="modal__close" aria-label="Close modal" data-modal-close onClick={close}>
136
+ <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" aria-hidden="true">
137
+ <path d="M18 6L6 18" />
138
+ <path d="M6 6l12 12" />
139
+ </svg>
140
+ </button>
141
+ </div>
142
+ <div className="modal__body">{children}</div>
143
+ {footer != null && <div className="modal__footer">{footer}</div>}
144
+ </div>
145
+ </>
146
+ );
147
+ }
148
+
149
+ export default Modal;
@@ -0,0 +1,72 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+ import { useState, useEffect } from 'react';
3
+
4
+ export interface NavbarProps extends HTMLAttributes<HTMLElement> {
5
+ siteName?: string;
6
+ logo?: string;
7
+ children?: ReactNode;
8
+ }
9
+
10
+ export function Navbar({
11
+ siteName = 'Site',
12
+ logo,
13
+ children,
14
+ className = '',
15
+ ...rest
16
+ }: NavbarProps) {
17
+ const [menuOpen, setMenuOpen] = useState(false);
18
+
19
+ useEffect(() => {
20
+ if (!menuOpen) return;
21
+ const onEscape = (e: KeyboardEvent) => {
22
+ if (e.key === 'Escape') setMenuOpen(false);
23
+ };
24
+ const onClick = (e: MouseEvent) => {
25
+ const target = e.target as Node;
26
+ if (target && !(target as Element).closest?.('.navbar')) setMenuOpen(false);
27
+ };
28
+ document.addEventListener('keydown', onEscape);
29
+ const t = setTimeout(() => document.addEventListener('click', onClick), 0);
30
+ return () => {
31
+ document.removeEventListener('keydown', onEscape);
32
+ document.removeEventListener('click', onClick);
33
+ clearTimeout(t);
34
+ };
35
+ }, [menuOpen]);
36
+
37
+ return (
38
+ <nav className={`navbar ${className}`.trim()} role="navigation" aria-label="Main navigation" {...rest}>
39
+ <div className="navbar__container">
40
+ <div className="navbar__brand">
41
+ <a href="/" className="navbar__brand-link">
42
+ {logo ? (
43
+ <img src={logo} alt="" className="navbar__logo" />
44
+ ) : (
45
+ <span className="navbar__logo" aria-hidden="true">◎</span>
46
+ )}
47
+ {siteName}
48
+ </a>
49
+ </div>
50
+ {children && <div className="navbar__actions-desktop">{children}</div>}
51
+ <button
52
+ type="button"
53
+ className="navbar__toggle"
54
+ aria-label="Toggle menu"
55
+ aria-expanded={menuOpen}
56
+ onClick={() => setMenuOpen((o) => !o)}
57
+ >
58
+ <span className="navbar__toggle-icon" aria-hidden="true">
59
+ <span /><span /><span />
60
+ </span>
61
+ </button>
62
+ </div>
63
+ {menuOpen && (
64
+ <div className="navbar__mobile" aria-hidden="false">
65
+ <div className="navbar__mobile-inner">Mobile menu</div>
66
+ </div>
67
+ )}
68
+ </nav>
69
+ );
70
+ }
71
+
72
+ export default Navbar;
@@ -0,0 +1,155 @@
1
+ import type { HTMLAttributes } from 'react';
2
+
3
+ function buildHref(template: string, page: number): string {
4
+ return template.replace(/\{page\}/g, String(page));
5
+ }
6
+
7
+ function getPageItems(
8
+ total: number,
9
+ current: number,
10
+ maxVisible: number
11
+ ): (number | 'ellipsis')[] {
12
+ if (total <= 1) return [];
13
+ if (total <= maxVisible) return Array.from({ length: total }, (_, i) => i + 1);
14
+ const items: (number | 'ellipsis')[] = [1];
15
+ const delta = Math.max(0, Math.floor((maxVisible - 2) / 2));
16
+ const start = Math.max(2, current - delta);
17
+ const end = Math.min(total - 1, current + delta);
18
+ if (start > 2) items.push('ellipsis');
19
+ for (let p = start; p <= end; p++) {
20
+ if (p !== 1 && p !== total) items.push(p);
21
+ }
22
+ if (end < total - 1) items.push('ellipsis');
23
+ if (total > 1) items.push(total);
24
+ return items;
25
+ }
26
+
27
+ export interface PaginationProps extends HTMLAttributes<HTMLElement> {
28
+ currentPage: number;
29
+ totalPages: number;
30
+ hrefTemplate?: string;
31
+ showFirstLast?: boolean;
32
+ maxVisible?: number;
33
+ className?: string;
34
+ }
35
+
36
+ export function Pagination({
37
+ currentPage,
38
+ totalPages,
39
+ hrefTemplate = '?page={page}',
40
+ showFirstLast = true,
41
+ maxVisible = 5,
42
+ className = '',
43
+ ...rest
44
+ }: PaginationProps) {
45
+ const classes = ['pagination', className].filter(Boolean).join(' ').trim();
46
+ const pageItems = getPageItems(totalPages, currentPage, maxVisible);
47
+ const hasPrev = currentPage > 1;
48
+ const hasNext = currentPage < totalPages;
49
+
50
+ return (
51
+ <nav className={classes} aria-label="Pagination" {...rest}>
52
+ <ul className="pagination__list">
53
+ {showFirstLast && totalPages > 1 && (
54
+ <li className="pagination__item">
55
+ {hasPrev ? (
56
+ <a
57
+ className="pagination__link pagination__link--prev"
58
+ href={buildHref(hrefTemplate, 1)}
59
+ aria-label="First page"
60
+ >
61
+ First
62
+ </a>
63
+ ) : (
64
+ <span
65
+ className="pagination__link pagination__link--prev pagination__link--disabled"
66
+ aria-disabled="true"
67
+ >
68
+ First
69
+ </span>
70
+ )}
71
+ </li>
72
+ )}
73
+ <li className="pagination__item">
74
+ {hasPrev ? (
75
+ <a
76
+ className="pagination__link pagination__link--prev"
77
+ href={buildHref(hrefTemplate, currentPage - 1)}
78
+ aria-label="Previous page"
79
+ >
80
+ Previous
81
+ </a>
82
+ ) : (
83
+ <span
84
+ className="pagination__link pagination__link--prev pagination__link--disabled"
85
+ aria-disabled="true"
86
+ >
87
+ Previous
88
+ </span>
89
+ )}
90
+ </li>
91
+ {pageItems.map((item, i) => (
92
+ <li key={i} className="pagination__item">
93
+ {item === 'ellipsis' ? (
94
+ <span className="pagination__ellipsis" aria-hidden="true">
95
+
96
+ </span>
97
+ ) : item === currentPage ? (
98
+ <span className="pagination__link pagination__link--current" aria-current="page">
99
+ {item}
100
+ </span>
101
+ ) : (
102
+ <a
103
+ className="pagination__link"
104
+ href={buildHref(hrefTemplate, item)}
105
+ aria-label={`Page ${item}`}
106
+ >
107
+ {item}
108
+ </a>
109
+ )}
110
+ </li>
111
+ ))}
112
+ <li className="pagination__item">
113
+ {hasNext ? (
114
+ <a
115
+ className="pagination__link pagination__link--next"
116
+ href={buildHref(hrefTemplate, currentPage + 1)}
117
+ aria-label="Next page"
118
+ >
119
+ Next
120
+ </a>
121
+ ) : (
122
+ <span
123
+ className="pagination__link pagination__link--next pagination__link--disabled"
124
+ aria-disabled="true"
125
+ >
126
+ Next
127
+ </span>
128
+ )}
129
+ </li>
130
+ {showFirstLast && totalPages > 1 && (
131
+ <li className="pagination__item">
132
+ {hasNext ? (
133
+ <a
134
+ className="pagination__link pagination__link--next"
135
+ href={buildHref(hrefTemplate, totalPages)}
136
+ aria-label="Last page"
137
+ >
138
+ Last
139
+ </a>
140
+ ) : (
141
+ <span
142
+ className="pagination__link pagination__link--next pagination__link--disabled"
143
+ aria-disabled="true"
144
+ >
145
+ Last
146
+ </span>
147
+ )}
148
+ </li>
149
+ )}
150
+ </ul>
151
+ </nav>
152
+ );
153
+ }
154
+
155
+ export default Pagination;
@@ -0,0 +1,66 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+ import { useState, useEffect } from 'react';
3
+
4
+ export interface PopoverProps extends HTMLAttributes<HTMLDivElement> {
5
+ id?: string;
6
+ open?: boolean;
7
+ onOpenChange?: (open: boolean) => void;
8
+ children?: ReactNode;
9
+ trigger?: ReactNode;
10
+ className?: string;
11
+ }
12
+
13
+ export function Popover({
14
+ id: idProp,
15
+ open = false,
16
+ onOpenChange,
17
+ children,
18
+ trigger,
19
+ className = '',
20
+ ...rest
21
+ }: PopoverProps) {
22
+ const id = idProp ?? `popover-${Math.random().toString(36).slice(2, 9)}`;
23
+
24
+ const toggle = (e: React.MouseEvent) => {
25
+ e.preventDefault();
26
+ onOpenChange?.(!open);
27
+ };
28
+
29
+ useEffect(() => {
30
+ const handleClickOutside = (e: MouseEvent) => {
31
+ const target = e.target as Node;
32
+ if (target && !document.getElementById(id)?.contains(target)) {
33
+ onOpenChange?.(false);
34
+ }
35
+ };
36
+ if (open) {
37
+ const t = setTimeout(() => document.addEventListener('click', handleClickOutside), 0);
38
+ return () => {
39
+ clearTimeout(t);
40
+ document.removeEventListener('click', handleClickOutside);
41
+ };
42
+ }
43
+ }, [open, id, onOpenChange]);
44
+
45
+ return (
46
+ <div className={`popover ${className}`.trim()} data-popover id={id} {...rest}>
47
+ <span data-popover-trigger onClick={toggle}>
48
+ {trigger}
49
+ </span>
50
+ <div
51
+ className={`popover__content ${open ? 'popover__content--open' : ''}`.trim()}
52
+ role="dialog"
53
+ aria-modal="false"
54
+ aria-hidden={!open}
55
+ hidden={!open}
56
+ data-popover-content
57
+ id={`${id}-content`}
58
+ onKeyDown={(e) => e.key === 'Escape' && onOpenChange?.(false)}
59
+ >
60
+ {children}
61
+ </div>
62
+ </div>
63
+ );
64
+ }
65
+
66
+ export default Popover;
@@ -0,0 +1,66 @@
1
+ import type { HTMLAttributes } from 'react';
2
+
3
+ export type ProgressBarVariant = 'primary' | 'success' | 'warning' | 'error' | 'info';
4
+ export type ProgressBarSize = 'sm' | 'md' | 'lg';
5
+
6
+ export interface ProgressBarProps extends HTMLAttributes<HTMLDivElement> {
7
+ value?: number;
8
+ max?: number;
9
+ variant?: ProgressBarVariant;
10
+ size?: ProgressBarSize;
11
+ showLabel?: boolean;
12
+ indeterminate?: boolean;
13
+ label?: string;
14
+ className?: string;
15
+ }
16
+
17
+ export function ProgressBar({
18
+ value = 0,
19
+ max = 100,
20
+ variant = 'primary',
21
+ size = 'md',
22
+ showLabel = false,
23
+ indeterminate = false,
24
+ label,
25
+ className = '',
26
+ ...rest
27
+ }: ProgressBarProps) {
28
+ const safeMax = max <= 0 ? 100 : max;
29
+ const clampedValue = indeterminate ? 0 : Math.max(0, Math.min(value, safeMax));
30
+ const percentage = indeterminate ? 0 : Math.round((clampedValue / safeMax) * 100);
31
+ const classes = [
32
+ 'progress',
33
+ `progress--${variant}`,
34
+ `progress--${size}`,
35
+ indeterminate ? 'progress--indeterminate' : '',
36
+ className,
37
+ ]
38
+ .filter(Boolean)
39
+ .join(' ')
40
+ .trim();
41
+ const barStyle = indeterminate ? {} : { width: `${percentage}%` };
42
+
43
+ return (
44
+ <div
45
+ className={classes}
46
+ role="progressbar"
47
+ aria-valuemin={0}
48
+ aria-valuemax={safeMax}
49
+ aria-label={label ?? (indeterminate ? 'Loading' : 'Progress')}
50
+ aria-valuetext={indeterminate ? 'Loading' : undefined}
51
+ aria-valuenow={indeterminate ? undefined : clampedValue}
52
+ {...rest}
53
+ >
54
+ <div className="progress__track">
55
+ <div className="progress__bar" style={barStyle} />
56
+ </div>
57
+ {showLabel && !indeterminate && (
58
+ <span className="progress__label" aria-hidden="true">
59
+ {percentage}%
60
+ </span>
61
+ )}
62
+ </div>
63
+ );
64
+ }
65
+
66
+ export default ProgressBar;
@@ -0,0 +1,38 @@
1
+ import type { InputHTMLAttributes } from 'react';
2
+
3
+ export interface RadioProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
4
+ /** Selected value for the radio group. Use the same name for all radios in the group. */
5
+ group?: string;
6
+ ariaDescribedby?: string;
7
+ ariaLabel?: string;
8
+ className?: string;
9
+ }
10
+
11
+ export function Radio({
12
+ id,
13
+ name,
14
+ value = '',
15
+ required = false,
16
+ disabled = false,
17
+ className = '',
18
+ ariaDescribedby,
19
+ ariaLabel,
20
+ ...rest
21
+ }: RadioProps) {
22
+ return (
23
+ <input
24
+ type="radio"
25
+ id={id}
26
+ name={name}
27
+ value={value}
28
+ required={required}
29
+ disabled={disabled}
30
+ className={className}
31
+ aria-describedby={ariaDescribedby}
32
+ aria-label={ariaLabel}
33
+ {...rest}
34
+ />
35
+ );
36
+ }
37
+
38
+ export default Radio;
@@ -0,0 +1,24 @@
1
+ import type { HTMLAttributes } from 'react';
2
+
3
+ export interface ResizableHandleProps extends HTMLAttributes<HTMLDivElement> {
4
+ withHandle?: boolean;
5
+ className?: string;
6
+ }
7
+
8
+ export function ResizableHandle({
9
+ withHandle = false,
10
+ className = '',
11
+ ...rest
12
+ }: ResizableHandleProps) {
13
+ const classes = [
14
+ 'resizable__handle',
15
+ withHandle ? 'resizable__handle--with-handle' : '',
16
+ className,
17
+ ]
18
+ .filter(Boolean)
19
+ .join(' ')
20
+ .trim();
21
+ return <div className={classes} data-resizable-handle {...rest} />;
22
+ }
23
+
24
+ export default ResizableHandle;
@@ -0,0 +1,29 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+
3
+ export interface ResizablePaneProps extends HTMLAttributes<HTMLDivElement> {
4
+ defaultSize?: number;
5
+ children?: ReactNode;
6
+ className?: string;
7
+ }
8
+
9
+ export function ResizablePane({
10
+ defaultSize = 50,
11
+ className = '',
12
+ children,
13
+ style,
14
+ ...rest
15
+ }: ResizablePaneProps) {
16
+ const size = Math.min(100, Math.max(0, defaultSize));
17
+ return (
18
+ <div
19
+ className={`resizable__pane ${className}`.trim()}
20
+ data-resizable-pane
21
+ style={{ flex: `1 1 ${size}%`, ...(style as object) }}
22
+ {...rest}
23
+ >
24
+ {children}
25
+ </div>
26
+ );
27
+ }
28
+
29
+ export default ResizablePane;
@@ -0,0 +1,29 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+
3
+ export interface ResizablePaneGroupProps extends HTMLAttributes<HTMLDivElement> {
4
+ id?: string;
5
+ direction?: 'horizontal' | 'vertical';
6
+ children?: ReactNode;
7
+ className?: string;
8
+ }
9
+
10
+ export function ResizablePaneGroup({
11
+ id: idProp,
12
+ direction = 'horizontal',
13
+ className = '',
14
+ children,
15
+ ...rest
16
+ }: ResizablePaneGroupProps) {
17
+ const id = idProp ?? `resizable-${Math.random().toString(36).slice(2, 9)}`;
18
+ const classes = [`resizable__pane-group`, `resizable__pane-group--${direction}`, className]
19
+ .filter(Boolean)
20
+ .join(' ')
21
+ .trim();
22
+ return (
23
+ <div className={classes} id={id} data-resizable-group data-direction={direction} {...rest}>
24
+ {children}
25
+ </div>
26
+ );
27
+ }
28
+
29
+ export default ResizablePaneGroup;
@@ -0,0 +1,29 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+
3
+ export interface ScrollAreaProps extends HTMLAttributes<HTMLDivElement> {
4
+ orientation?: 'vertical' | 'horizontal';
5
+ children?: ReactNode;
6
+ className?: string;
7
+ }
8
+
9
+ export function ScrollArea({
10
+ orientation = 'vertical',
11
+ className = '',
12
+ children,
13
+ ...rest
14
+ }: ScrollAreaProps) {
15
+ const horizontal = orientation === 'horizontal';
16
+ const classes = ['scroll-area', horizontal ? 'scroll-area--horizontal' : '', className]
17
+ .filter(Boolean)
18
+ .join(' ')
19
+ .trim();
20
+ return (
21
+ <div className={classes} {...rest}>
22
+ <div className="scroll-area__viewport" tabIndex={0}>
23
+ {children}
24
+ </div>
25
+ </div>
26
+ );
27
+ }
28
+
29
+ export default ScrollArea;