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,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;
|