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 { useEffect, useState } from 'react';
3
+
4
+ export interface BackToTopProps extends HTMLAttributes<HTMLDivElement> {
5
+ threshold?: number;
6
+ label?: string;
7
+ className?: string;
8
+ }
9
+
10
+ export function BackToTop({
11
+ threshold = 400,
12
+ label = 'Back to top',
13
+ className = '',
14
+ ...rest
15
+ }: BackToTopProps) {
16
+ const [visible, setVisible] = useState(false);
17
+
18
+ const scrollToTop = () => {
19
+ window.scrollTo({ top: 0, behavior: 'smooth' });
20
+ };
21
+
22
+ useEffect(() => {
23
+ const handler = () => setVisible(window.scrollY > threshold);
24
+ window.addEventListener('scroll', handler, { passive: true });
25
+ handler();
26
+ return () => window.removeEventListener('scroll', handler);
27
+ }, [threshold]);
28
+
29
+ const wrapperClass = ['back-to-top', className].filter(Boolean).join(' ').trim();
30
+
31
+ return (
32
+ <div
33
+ className={wrapperClass}
34
+ data-back-to-top
35
+ data-visible={visible ? 'true' : 'false'}
36
+ aria-hidden={visible ? 'false' : 'true'}
37
+ {...rest}
38
+ >
39
+ <button type="button" className="back-to-top__btn" aria-label={label} onClick={scrollToTop}>
40
+ <span className="back-to-top__icon" aria-hidden="true">
41
+ <svg
42
+ xmlns="http://www.w3.org/2000/svg"
43
+ width="24"
44
+ height="24"
45
+ viewBox="0 0 24 24"
46
+ fill="none"
47
+ stroke="currentColor"
48
+ strokeWidth="2"
49
+ strokeLinecap="round"
50
+ strokeLinejoin="round"
51
+ className="icon"
52
+ aria-hidden="true"
53
+ >
54
+ <path d="m18 15-6-6-6 6" />
55
+ </svg>
56
+ </span>
57
+ </button>
58
+ </div>
59
+ );
60
+ }
61
+
62
+ export default BackToTop;
@@ -0,0 +1,39 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+
3
+ export type BadgeVariant = 'primary' | 'success' | 'warning' | 'error' | 'info';
4
+ export type BadgeSize = 'sm' | 'md' | 'lg';
5
+
6
+ export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
7
+ variant?: BadgeVariant;
8
+ size?: BadgeSize;
9
+ pill?: boolean;
10
+ children?: ReactNode;
11
+ className?: string;
12
+ }
13
+
14
+ export function Badge({
15
+ variant = 'primary',
16
+ size = 'md',
17
+ pill = false,
18
+ className = '',
19
+ children,
20
+ ...rest
21
+ }: BadgeProps) {
22
+ const classes = [
23
+ 'badge',
24
+ `badge--${variant}`,
25
+ `badge--${size}`,
26
+ pill ? 'badge--pill' : '',
27
+ className,
28
+ ]
29
+ .filter(Boolean)
30
+ .join(' ')
31
+ .trim();
32
+ return (
33
+ <span className={classes} {...rest}>
34
+ {children}
35
+ </span>
36
+ );
37
+ }
38
+
39
+ export default Badge;
@@ -0,0 +1,81 @@
1
+ import type { HTMLAttributes } from 'react';
2
+
3
+ export interface BreadcrumbItem {
4
+ label: string;
5
+ href?: string;
6
+ }
7
+
8
+ export type BreadcrumbSeparator = 'chevron' | 'slash' | 'arrow' | string;
9
+
10
+ export interface BreadcrumbProps extends HTMLAttributes<HTMLElement> {
11
+ items: BreadcrumbItem[];
12
+ separator?: BreadcrumbSeparator;
13
+ className?: string;
14
+ }
15
+
16
+ export function Breadcrumb({
17
+ items,
18
+ separator = 'chevron',
19
+ className = '',
20
+ ...rest
21
+ }: BreadcrumbProps) {
22
+ const separatorVariant =
23
+ separator === 'slash'
24
+ ? 'breadcrumb--slash'
25
+ : separator === 'arrow'
26
+ ? 'breadcrumb--arrow'
27
+ : 'breadcrumb--chevron';
28
+ const classes = ['breadcrumb', separatorVariant, className].filter(Boolean).join(' ').trim();
29
+ const separatorChar =
30
+ separator === 'slash' ? '/' : separator === 'arrow' ? '›' : typeof separator === 'string' ? separator : '›';
31
+ const useIcon = separator === 'chevron';
32
+
33
+ return (
34
+ <nav className={classes} aria-label="Breadcrumb" {...rest}>
35
+ <ol className="breadcrumb__list">
36
+ {items.map((item, index) => {
37
+ const isLast = index === items.length - 1;
38
+ const isCurrent = isLast || !item.href;
39
+ return (
40
+ <li
41
+ key={index}
42
+ className={`breadcrumb__item ${isCurrent ? 'breadcrumb__item--current' : ''}`.trim()}
43
+ >
44
+ {isCurrent ? (
45
+ <span className="breadcrumb__current" aria-current="page">
46
+ {item.label}
47
+ </span>
48
+ ) : (
49
+ <a className="breadcrumb__link" href={item.href}>
50
+ {item.label}
51
+ </a>
52
+ )}
53
+ {!isLast && (
54
+ <span className="breadcrumb__separator" aria-hidden="true">
55
+ {useIcon ? (
56
+ <svg
57
+ className="breadcrumb__separator-icon"
58
+ width="14"
59
+ height="14"
60
+ viewBox="0 0 24 24"
61
+ fill="none"
62
+ stroke="currentColor"
63
+ strokeWidth="2"
64
+ aria-hidden="true"
65
+ >
66
+ <path d="M6 9l6 6 6-6" />
67
+ </svg>
68
+ ) : (
69
+ separatorChar
70
+ )}
71
+ </span>
72
+ )}
73
+ </li>
74
+ );
75
+ })}
76
+ </ol>
77
+ </nav>
78
+ );
79
+ }
80
+
81
+ export default Breadcrumb;
@@ -0,0 +1,40 @@
1
+ import type { ButtonHTMLAttributes, ReactNode } from 'react';
2
+
3
+ export type ButtonVariant =
4
+ | 'default'
5
+ | 'primary'
6
+ | 'success'
7
+ | 'warning'
8
+ | 'error'
9
+ | 'info'
10
+ | 'outline';
11
+
12
+ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
13
+ variant?: ButtonVariant;
14
+ children?: ReactNode;
15
+ className?: string;
16
+ }
17
+
18
+ /**
19
+ * Button component using Rizzo CSS BEM classes (btn, btn-primary, etc.).
20
+ * Same API and markup as Astro/Svelte Button for design system parity.
21
+ */
22
+ export function Button({
23
+ variant = 'default',
24
+ type = 'button',
25
+ disabled = false,
26
+ className = '',
27
+ children,
28
+ ...rest
29
+ }: ButtonProps) {
30
+ const variantClass = variant === 'default' ? '' : `btn-${variant}`;
31
+ const classes = ['btn', variantClass, className].filter(Boolean).join(' ');
32
+
33
+ return (
34
+ <button type={type} disabled={disabled} className={classes} {...rest}>
35
+ {children}
36
+ </button>
37
+ );
38
+ }
39
+
40
+ export default Button;
@@ -0,0 +1,24 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+
3
+ export interface ButtonGroupProps extends HTMLAttributes<HTMLDivElement> {
4
+ orientation?: 'horizontal' | 'vertical';
5
+ children?: ReactNode;
6
+ className?: string;
7
+ }
8
+
9
+ export function ButtonGroup({
10
+ orientation = 'horizontal',
11
+ className = '',
12
+ children,
13
+ ...rest
14
+ }: ButtonGroupProps) {
15
+ const orientationClass = orientation === 'vertical' ? 'button-group--vertical' : '';
16
+ const classes = ['button-group', orientationClass, className].filter(Boolean).join(' ').trim();
17
+ return (
18
+ <div className={classes} role="group" {...rest}>
19
+ {children}
20
+ </div>
21
+ );
22
+ }
23
+
24
+ export default ButtonGroup;
@@ -0,0 +1,26 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+
3
+ export type CardVariant = 'default' | 'elevated' | 'outlined' | 'filled';
4
+
5
+ export interface CardProps extends HTMLAttributes<HTMLDivElement> {
6
+ variant?: CardVariant;
7
+ children?: ReactNode;
8
+ className?: string;
9
+ }
10
+
11
+ export function Card({
12
+ variant = 'default',
13
+ className = '',
14
+ children,
15
+ ...rest
16
+ }: CardProps) {
17
+ const variantClass = variant !== 'default' ? `card--${variant}` : '';
18
+ const classes = ['card', variantClass, className].filter(Boolean).join(' ').trim();
19
+ return (
20
+ <div className={classes} {...rest}>
21
+ {children}
22
+ </div>
23
+ );
24
+ }
25
+
26
+ export default Card;
@@ -0,0 +1,40 @@
1
+ import type { InputHTMLAttributes } from 'react';
2
+
3
+ export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
4
+ ariaDescribedby?: string;
5
+ ariaLabel?: string;
6
+ className?: string;
7
+ }
8
+
9
+ export function Checkbox({
10
+ id,
11
+ name,
12
+ value,
13
+ checked = false,
14
+ required = false,
15
+ disabled = false,
16
+ className = '',
17
+ ariaDescribedby,
18
+ ariaLabel,
19
+ onChange,
20
+ ...rest
21
+ }: CheckboxProps) {
22
+ return (
23
+ <input
24
+ type="checkbox"
25
+ id={id}
26
+ name={name}
27
+ value={value}
28
+ checked={checked}
29
+ required={required}
30
+ disabled={disabled}
31
+ className={className}
32
+ aria-describedby={ariaDescribedby}
33
+ aria-label={ariaLabel}
34
+ onChange={onChange}
35
+ {...rest}
36
+ />
37
+ );
38
+ }
39
+
40
+ export default Checkbox;
@@ -0,0 +1,58 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+ import { useState } from 'react';
3
+
4
+ export interface CollapsibleProps extends HTMLAttributes<HTMLDivElement> {
5
+ id?: string;
6
+ defaultOpen?: boolean;
7
+ triggerLabel?: string;
8
+ children?: ReactNode;
9
+ className?: string;
10
+ }
11
+
12
+ export function Collapsible({
13
+ id: idProp,
14
+ defaultOpen = false,
15
+ triggerLabel = 'Toggle',
16
+ children,
17
+ className = '',
18
+ ...rest
19
+ }: CollapsibleProps) {
20
+ const id = idProp ?? `collapsible-${Math.random().toString(36).slice(2, 9)}`;
21
+ const triggerId = `${id}-trigger`;
22
+ const panelId = `${id}-panel`;
23
+ const [open, setOpen] = useState(defaultOpen);
24
+
25
+ const triggerClasses = ['collapsible__trigger', open ? 'collapsible__trigger--open' : ''].filter(Boolean).join(' ');
26
+ const panelClasses = ['collapsible__panel', open ? 'collapsible__panel--open' : ''].filter(Boolean).join(' ');
27
+
28
+ return (
29
+ <div className={`collapsible ${className}`.trim()} data-collapsible id={id} {...rest}>
30
+ <button
31
+ type="button"
32
+ className={triggerClasses}
33
+ id={triggerId}
34
+ aria-expanded={open}
35
+ aria-controls={panelId}
36
+ onClick={() => setOpen((o) => !o)}
37
+ >
38
+ <span className="collapsible__trigger-label">{triggerLabel}</span>
39
+ <span className="collapsible__icon" aria-hidden="true">
40
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
41
+ <path d="m6 9 6 6 6-6" />
42
+ </svg>
43
+ </span>
44
+ </button>
45
+ <div
46
+ className={panelClasses}
47
+ id={panelId}
48
+ role="region"
49
+ aria-labelledby={triggerId}
50
+ hidden={!open}
51
+ >
52
+ <div className="collapsible__panel-inner">{children}</div>
53
+ </div>
54
+ </div>
55
+ );
56
+ }
57
+
58
+ export default Collapsible;
@@ -0,0 +1,67 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+ import { useState, useRef, useEffect } from 'react';
3
+
4
+ export interface ContextMenuProps extends HTMLAttributes<HTMLDivElement> {
5
+ id?: string;
6
+ children?: ReactNode;
7
+ trigger?: ReactNode;
8
+ className?: string;
9
+ }
10
+
11
+ export function ContextMenu({
12
+ id: idProp,
13
+ children,
14
+ trigger,
15
+ className = '',
16
+ ...rest
17
+ }: ContextMenuProps) {
18
+ const id = idProp ?? `context-menu-${Math.random().toString(36).slice(2, 9)}`;
19
+ const [open, setOpen] = useState(false);
20
+ const [position, setPosition] = useState({ x: 0, y: 0 });
21
+ const contentRef = useRef<HTMLDivElement>(null);
22
+
23
+ const handleContextMenu = (e: React.MouseEvent) => {
24
+ e.preventDefault();
25
+ setPosition({ x: e.clientX, y: e.clientY });
26
+ setOpen(true);
27
+ requestAnimationFrame(() => document.addEventListener('click', closeMenu));
28
+ };
29
+
30
+ const closeMenu = () => {
31
+ setOpen(false);
32
+ document.removeEventListener('click', closeMenu);
33
+ };
34
+
35
+ const handleKeydown = (e: React.KeyboardEvent) => {
36
+ if (e.key === 'Escape') closeMenu();
37
+ };
38
+
39
+ useEffect(() => {
40
+ if (open && contentRef.current) {
41
+ contentRef.current.style.left = `${position.x}px`;
42
+ contentRef.current.style.top = `${position.y}px`;
43
+ }
44
+ }, [open, position]);
45
+
46
+ return (
47
+ <div className={`context-menu ${className}`.trim()} data-context-menu id={id} {...rest}>
48
+ <div data-context-menu-trigger onContextMenu={handleContextMenu}>
49
+ {trigger}
50
+ </div>
51
+ <div
52
+ ref={contentRef}
53
+ className={`context-menu__content ${open ? 'context-menu__content--open' : ''}`.trim()}
54
+ role="menu"
55
+ aria-hidden={!open}
56
+ hidden={!open}
57
+ data-context-menu-content
58
+ id={`${id}-content`}
59
+ onKeyDown={handleKeydown}
60
+ >
61
+ {children}
62
+ </div>
63
+ </div>
64
+ );
65
+ }
66
+
67
+ export default ContextMenu;
@@ -0,0 +1,128 @@
1
+ import type { ButtonHTMLAttributes } from 'react';
2
+ import { useState } from 'react';
3
+
4
+ export interface CopyToClipboardProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
5
+ value: string;
6
+ label?: string;
7
+ format?: string;
8
+ iconOnly?: boolean;
9
+ buttonLabel?: string;
10
+ className?: string;
11
+ id?: string;
12
+ }
13
+
14
+ export function CopyToClipboard({
15
+ value,
16
+ label,
17
+ format,
18
+ iconOnly = false,
19
+ buttonLabel,
20
+ className = '',
21
+ id: idProp,
22
+ onClick,
23
+ onKeyDown,
24
+ ...rest
25
+ }: CopyToClipboardProps) {
26
+ const [copied, setCopied] = useState(false);
27
+ const [feedbackText, setFeedbackText] = useState('');
28
+ const buttonId = idProp ?? `copy-btn-${Math.random().toString(36).slice(2, 11)}`;
29
+ const displayText = iconOnly ? (buttonLabel ?? 'Copy') : value;
30
+ const classes = [
31
+ 'copy-to-clipboard',
32
+ iconOnly ? 'copy-to-clipboard--icon-only' : '',
33
+ className,
34
+ ]
35
+ .filter(Boolean)
36
+ .join(' ')
37
+ .trim();
38
+ const ariaLabel = copied
39
+ ? format ? `Copied ${format}!` : 'Copied!'
40
+ : label ?? (iconOnly ? (buttonLabel ?? 'Copy to clipboard') : `Copy ${value} to clipboard`);
41
+ const tooltipText = copied
42
+ ? format ? `Copied ${format}!` : 'Copied!'
43
+ : label ?? (iconOnly ? (buttonLabel ?? 'Copy') : 'Copy to clipboard');
44
+
45
+ const copy = async () => {
46
+ if (!value) return;
47
+ try {
48
+ await navigator.clipboard.writeText(value);
49
+ setCopied(true);
50
+ setFeedbackText(format ? `Copied ${format}!` : 'Copied!');
51
+ setTimeout(() => {
52
+ setCopied(false);
53
+ setFeedbackText('');
54
+ }, 2000);
55
+ } catch {
56
+ const textArea = document.createElement('textarea');
57
+ textArea.value = value;
58
+ textArea.style.position = 'fixed';
59
+ textArea.style.left = '-999999px';
60
+ document.body.appendChild(textArea);
61
+ textArea.focus();
62
+ textArea.select();
63
+ try {
64
+ document.execCommand('copy');
65
+ setCopied(true);
66
+ setFeedbackText(format ? `Copied ${format}!` : 'Copied!');
67
+ setTimeout(() => {
68
+ setCopied(false);
69
+ setFeedbackText('');
70
+ }, 2000);
71
+ } catch {
72
+ setFeedbackText('Failed to copy');
73
+ }
74
+ document.body.removeChild(textArea);
75
+ }
76
+ };
77
+
78
+ const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
79
+ onClick?.(e);
80
+ copy();
81
+ };
82
+
83
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
84
+ onKeyDown?.(e);
85
+ if (e.key === 'Enter' || e.key === ' ') {
86
+ e.preventDefault();
87
+ copy();
88
+ }
89
+ };
90
+
91
+ return (
92
+ <span className="tooltip-host" data-tooltip={tooltipText}>
93
+ <button
94
+ type="button"
95
+ className={classes}
96
+ aria-label={ariaLabel}
97
+ id={buttonId}
98
+ onClick={handleClick}
99
+ onKeyDown={handleKeyDown}
100
+ {...rest}
101
+ >
102
+ <span className="copy-to-clipboard__text">{displayText}</span>
103
+ <span
104
+ className={`copy-to-clipboard__icon copy-to-clipboard__icon--copy ${copied ? 'copy-to-clipboard__icon--hidden' : ''}`.trim()}
105
+ aria-hidden="true"
106
+ >
107
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
108
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
109
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
110
+ </svg>
111
+ </span>
112
+ <span
113
+ className={`copy-to-clipboard__icon copy-to-clipboard__icon--check ${!copied ? 'copy-to-clipboard__icon--hidden' : ''}`.trim()}
114
+ aria-hidden="true"
115
+ >
116
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
117
+ <polyline points="20 6 9 17 4 12" />
118
+ </svg>
119
+ </span>
120
+ <span className="copy-to-clipboard__feedback" aria-live="polite">
121
+ {feedbackText}
122
+ </span>
123
+ </button>
124
+ </span>
125
+ );
126
+ }
127
+
128
+ export default CopyToClipboard;
@@ -0,0 +1,23 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+
3
+ export interface DashboardProps extends HTMLAttributes<HTMLDivElement> {
4
+ sidebar?: ReactNode;
5
+ children?: ReactNode;
6
+ className?: string;
7
+ }
8
+
9
+ export function Dashboard({
10
+ sidebar,
11
+ children,
12
+ className = '',
13
+ ...rest
14
+ }: DashboardProps) {
15
+ return (
16
+ <div className={`dashboard ${className}`.trim()} data-dashboard {...rest}>
17
+ {sidebar && <div className="dashboard__sidebar">{sidebar}</div>}
18
+ <main className="dashboard__main">{children}</main>
19
+ </div>
20
+ );
21
+ }
22
+
23
+ export default Dashboard;
@@ -0,0 +1,47 @@
1
+ import type { HTMLAttributes } from 'react';
2
+
3
+ export interface DividerProps extends HTMLAttributes<HTMLDivElement> {
4
+ orientation?: 'horizontal' | 'vertical';
5
+ label?: string;
6
+ className?: string;
7
+ }
8
+
9
+ export function Divider({
10
+ orientation = 'horizontal',
11
+ label,
12
+ className = '',
13
+ ...rest
14
+ }: DividerProps) {
15
+ const hasLabel = typeof label === 'string' && label.trim().length > 0;
16
+ const labelText = label?.trim() ?? '';
17
+ const classes = [
18
+ 'divider',
19
+ `divider--${orientation}`,
20
+ hasLabel ? 'divider--labeled' : '',
21
+ className,
22
+ ]
23
+ .filter(Boolean)
24
+ .join(' ')
25
+ .trim();
26
+ return (
27
+ <div
28
+ className={classes}
29
+ role="separator"
30
+ aria-orientation={orientation}
31
+ aria-label={labelText || undefined}
32
+ {...rest}
33
+ >
34
+ {hasLabel && orientation === 'horizontal' ? (
35
+ <>
36
+ <span className="divider__line" aria-hidden="true" />
37
+ <span className="divider__label">{labelText}</span>
38
+ <span className="divider__line" aria-hidden="true" />
39
+ </>
40
+ ) : (
41
+ <span className="divider__line" aria-hidden="true" />
42
+ )}
43
+ </div>
44
+ );
45
+ }
46
+
47
+ export default Divider;
@@ -0,0 +1,48 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+
3
+ export interface DocsSidebarLink {
4
+ href: string;
5
+ label: string;
6
+ active?: boolean;
7
+ }
8
+
9
+ export interface DocsSidebarProps extends HTMLAttributes<HTMLElement> {
10
+ links?: DocsSidebarLink[];
11
+ children?: ReactNode;
12
+ className?: string;
13
+ }
14
+
15
+ const DEFAULT_LINKS: DocsSidebarLink[] = [
16
+ { href: '/docs', label: 'Overview' },
17
+ { href: '/docs/getting-started', label: 'Getting started' },
18
+ { href: '/docs/components', label: 'Components', active: true },
19
+ ];
20
+
21
+ export function DocsSidebar({
22
+ links = DEFAULT_LINKS,
23
+ children,
24
+ className = '',
25
+ ...rest
26
+ }: DocsSidebarProps) {
27
+ return (
28
+ <aside className={`docs-sidebar ${className}`.trim()} {...rest}>
29
+ <nav className="docs-sidebar__nav" aria-label="Docs">
30
+ <ul className="docs-sidebar__list">
31
+ {children ??
32
+ links.map((link, i) => (
33
+ <li key={i} className="docs-sidebar__item">
34
+ <a
35
+ href={link.href}
36
+ className={`docs-sidebar__link ${link.active ? 'docs-sidebar__link--active' : ''}`.trim()}
37
+ >
38
+ {link.label}
39
+ </a>
40
+ </li>
41
+ ))}
42
+ </ul>
43
+ </nav>
44
+ </aside>
45
+ );
46
+ }
47
+
48
+ export default DocsSidebar;