@hellboy/ds 0.1.2

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 (137) hide show
  1. package/README.md +111 -0
  2. package/dist/index.css +3699 -0
  3. package/dist/index.css.map +1 -0
  4. package/dist/index.d.mts +1087 -0
  5. package/dist/index.d.ts +1087 -0
  6. package/dist/index.js +3391 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/index.mjs +3287 -0
  9. package/dist/index.mjs.map +1 -0
  10. package/dist/theme.css +55 -0
  11. package/hellboy-ds-0.1.2.tgz +0 -0
  12. package/package.json +42 -0
  13. package/src/components/badge/Badge.tsx +29 -0
  14. package/src/components/badge/index.ts +1 -0
  15. package/src/components/banner/Banner.tsx +48 -0
  16. package/src/components/banner/banner.css +44 -0
  17. package/src/components/banner/index.ts +1 -0
  18. package/src/components/button/button.tsx +127 -0
  19. package/src/components/button/index.ts +1 -0
  20. package/src/components/card/card.tsx +57 -0
  21. package/src/components/card/index.ts +1 -0
  22. package/src/components/checkbox/Checkbox.tsx +98 -0
  23. package/src/components/checkbox/index.ts +1 -0
  24. package/src/components/code-block/code-block.tsx +44 -0
  25. package/src/components/code-block/index.ts +1 -0
  26. package/src/components/color-control/color-control.tsx +322 -0
  27. package/src/components/color-control/index.ts +1 -0
  28. package/src/components/drag-handle/DragHandle.tsx +78 -0
  29. package/src/components/drag-handle/index.ts +1 -0
  30. package/src/components/drawer/drawer.tsx +82 -0
  31. package/src/components/drawer/index.ts +1 -0
  32. package/src/components/floating-bar/floating-bar.tsx +52 -0
  33. package/src/components/floating-bar/index.ts +2 -0
  34. package/src/components/footer/footer.tsx +28 -0
  35. package/src/components/footer/index.ts +1 -0
  36. package/src/components/grid/Grid.tsx +53 -0
  37. package/src/components/grid/index.ts +1 -0
  38. package/src/components/header/header.tsx +57 -0
  39. package/src/components/header/index.ts +1 -0
  40. package/src/components/icons/icons.tsx +44 -0
  41. package/src/components/icons/index.ts +1 -0
  42. package/src/components/index.ts +29 -0
  43. package/src/components/input/DatePicker.tsx +133 -0
  44. package/src/components/input/Input.tsx +220 -0
  45. package/src/components/input/InputDate.tsx +10 -0
  46. package/src/components/input/InputDateTime.tsx +10 -0
  47. package/src/components/input/InputEmail.tsx +10 -0
  48. package/src/components/input/InputField.tsx +137 -0
  49. package/src/components/input/InputNumber.tsx +10 -0
  50. package/src/components/input/InputPassword.tsx +10 -0
  51. package/src/components/input/InputSearch.tsx +10 -0
  52. package/src/components/input/InputTel.tsx +10 -0
  53. package/src/components/input/InputText.tsx +10 -0
  54. package/src/components/input/InputTime.tsx +10 -0
  55. package/src/components/input/InputUrl.tsx +10 -0
  56. package/src/components/input/TimePicker.tsx +151 -0
  57. package/src/components/input/index.ts +11 -0
  58. package/src/components/layout/Layout.tsx +244 -0
  59. package/src/components/layout/index.ts +1 -0
  60. package/src/components/list/List.tsx +159 -0
  61. package/src/components/list/index.ts +1 -0
  62. package/src/components/navbar/MenuCategory.tsx +20 -0
  63. package/src/components/navbar/MenuGroup.tsx +288 -0
  64. package/src/components/navbar/MenuItem.tsx +65 -0
  65. package/src/components/navbar/Navbar.tsx +23 -0
  66. package/src/components/navbar/index.ts +4 -0
  67. package/src/components/page/index.ts +1 -0
  68. package/src/components/page/page.tsx +46 -0
  69. package/src/components/page-index/PageIndex.tsx +275 -0
  70. package/src/components/page-index/index.ts +1 -0
  71. package/src/components/popover/index.ts +1 -0
  72. package/src/components/popover/popover.tsx +199 -0
  73. package/src/components/radio/Radio.tsx +176 -0
  74. package/src/components/radio/index.ts +1 -0
  75. package/src/components/section/index.ts +1 -0
  76. package/src/components/section/section.tsx +66 -0
  77. package/src/components/select/Select.tsx +212 -0
  78. package/src/components/select/index.ts +1 -0
  79. package/src/components/slider/Slider.tsx +267 -0
  80. package/src/components/slider/index.ts +1 -0
  81. package/src/components/switch/index.ts +1 -0
  82. package/src/components/switch/switch.tsx +99 -0
  83. package/src/components/table/Table.tsx +147 -0
  84. package/src/components/table/index.ts +1 -0
  85. package/src/components/theme-control/index.ts +1 -0
  86. package/src/components/theme-control/theme-control.tsx +78 -0
  87. package/src/components/tooltip/index.ts +1 -0
  88. package/src/components/tooltip/tooltip.tsx +207 -0
  89. package/src/contexts/NavbarTooltipContext.tsx +48 -0
  90. package/src/contexts/index.ts +1 -0
  91. package/src/foundations/motion.md +136 -0
  92. package/src/index.ts +40 -0
  93. package/src/style/_shared/field.css +69 -0
  94. package/src/style/components/badge/badge.css +74 -0
  95. package/src/style/components/button/button.css +244 -0
  96. package/src/style/components/card/card.css +69 -0
  97. package/src/style/components/checkbox.css +142 -0
  98. package/src/style/components/code-block/code-block.css +34 -0
  99. package/src/style/components/color-control/color-control.css +126 -0
  100. package/src/style/components/drag-handle/drag-handle.css +68 -0
  101. package/src/style/components/drawer/drawer.css +210 -0
  102. package/src/style/components/floating-bar/floating-bar.css +39 -0
  103. package/src/style/components/footer/footer.css +108 -0
  104. package/src/style/components/grid/grid.css +33 -0
  105. package/src/style/components/header/header.css +44 -0
  106. package/src/style/components/icons/icons.css +44 -0
  107. package/src/style/components/input/input.css +393 -0
  108. package/src/style/components/layout/layout.css +205 -0
  109. package/src/style/components/list/list.css +140 -0
  110. package/src/style/components/navbar/navbar.css +342 -0
  111. package/src/style/components/page/page.css +46 -0
  112. package/src/style/components/page-index/page-index.css +158 -0
  113. package/src/style/components/popover/popover.css +44 -0
  114. package/src/style/components/radio.css +178 -0
  115. package/src/style/components/section/section.css +67 -0
  116. package/src/style/components/select/select.css +143 -0
  117. package/src/style/components/slider/slider.css +159 -0
  118. package/src/style/components/switch/switch.css +267 -0
  119. package/src/style/components/table/table.css +108 -0
  120. package/src/style/components/theme-control/theme-control.css +35 -0
  121. package/src/style/components/tooltip/tooltip.css +52 -0
  122. package/src/style/foundations/global.css +316 -0
  123. package/src/style/foundations/motion.css +164 -0
  124. package/src/style/foundations/spacing.css +51 -0
  125. package/src/style/foundations/typography.css +39 -0
  126. package/src/style/foundations/z-index.css +81 -0
  127. package/src/style/modes/dark.css +146 -0
  128. package/src/style/modes/light.css +147 -0
  129. package/src/style/semantic.css +52 -0
  130. package/src/style/styles.css +51 -0
  131. package/src/style/themes/theme.json +37 -0
  132. package/src/utils/README.md +305 -0
  133. package/src/utils/USER_PREFERENCES.md +558 -0
  134. package/src/utils/theme.ts +127 -0
  135. package/src/utils/user-preferences.ts +577 -0
  136. package/tsconfig.json +25 -0
  137. package/tsup.config.ts +52 -0
@@ -0,0 +1,275 @@
1
+ import * as React from "react";
2
+ import "../../style/components/page-index/page-index.css";
3
+
4
+ export interface PageIndexItem {
5
+ id: string;
6
+ title: string;
7
+ level: number;
8
+ element: HTMLElement;
9
+ }
10
+
11
+ export interface PageIndexProps extends React.HTMLAttributes<HTMLDivElement> {
12
+ /**
13
+ * Custom title for the index
14
+ */
15
+ title?: string;
16
+ /**
17
+ * Whether the sidebar is collapsed
18
+ */
19
+ collapsed?: boolean;
20
+ }
21
+
22
+ export const PageIndex: React.FC<PageIndexProps> = ({
23
+ title = "On this page",
24
+ collapsed = false,
25
+ className = "",
26
+ ...props
27
+ }) => {
28
+ const [items, setItems] = React.useState<PageIndexItem[]>([]);
29
+ const [activeId, setActiveId] = React.useState<string>("");
30
+ const [manualNavigation, setManualNavigation] = React.useState<boolean>(false);
31
+
32
+ // Function to extract text content from element
33
+ const getTextContent = React.useCallback((element: HTMLElement): string => {
34
+ // For headings, get text content
35
+ if (element.tagName.match(/^H[1-6]$/)) {
36
+ return element.textContent || "";
37
+ }
38
+ // For other elements, try to find a title attribute or data attribute
39
+ return element.getAttribute("data-page-index-title") ||
40
+ element.getAttribute("title") ||
41
+ element.textContent ||
42
+ "";
43
+ }, []);
44
+
45
+ // Function to generate ID from text
46
+ const generateId = React.useCallback((text: string): string => {
47
+ return text
48
+ .toLowerCase()
49
+ .replace(/[^\w\s-]/g, "") // Remove special characters
50
+ .replace(/\s+/g, "-") // Replace spaces with hyphens
51
+ .replace(/-+/g, "-") // Replace multiple hyphens with single
52
+ .trim();
53
+ }, []);
54
+
55
+ // Scan the page for headings that are direct children of sections within pages
56
+ const scanPage = React.useCallback(() => {
57
+ const newItems: PageIndexItem[] = [];
58
+ const seenIds = new Set<string>(); // Track seen IDs to prevent duplicates
59
+
60
+ // Find all pages (main elements) and their sections
61
+ const pages = document.querySelectorAll('main');
62
+ pages.forEach((page) => {
63
+ const pageElement = page as HTMLElement;
64
+
65
+ // Skip pages that are inside the page-index component
66
+ if (pageElement.closest('.page-index')) {
67
+ return;
68
+ }
69
+
70
+ // Find all sections within this page
71
+ const sections = pageElement.querySelectorAll('section');
72
+ sections.forEach((section) => {
73
+ const sectionElement = section as HTMLElement;
74
+
75
+ // Skip sections that are inside the page-index component
76
+ if (sectionElement.closest('.page-index')) {
77
+ return;
78
+ }
79
+
80
+ // Find direct child headings (h1, h2, h3, h4, h5, h6) of this section
81
+ // Using Array.from and filtering for better browser compatibility
82
+ const childElements = Array.from(sectionElement.children);
83
+ const headings = childElements.filter(child =>
84
+ child.tagName.match(/^H[1-6]$/)
85
+ ) as HTMLElement[];
86
+
87
+ headings.forEach((element) => {
88
+ const text = getTextContent(element);
89
+ if (text.trim()) {
90
+ // Skip headings inside component previews, demo containers, or dialogs/drawers
91
+ if (element.closest('.component-preview, [data-demo], .demo, .example, .drawer, [role="dialog"]')) {
92
+ return;
93
+ }
94
+
95
+ const id = element.id || generateId(text);
96
+
97
+ // Skip if we've already seen this ID (prevent duplicates)
98
+ if (seenIds.has(id)) {
99
+ return;
100
+ }
101
+ seenIds.add(id);
102
+
103
+ // Ensure the element has an ID
104
+ if (!element.id) {
105
+ element.id = id;
106
+ }
107
+
108
+ // Determine level based on heading tag
109
+ const tagName = element.tagName.toLowerCase();
110
+ const level = parseInt(tagName.substring(1)); // h1 -> 1, h2 -> 2, etc.
111
+
112
+ newItems.push({
113
+ id,
114
+ title: text.trim(),
115
+ level,
116
+ element,
117
+ });
118
+ }
119
+ });
120
+ });
121
+ });
122
+
123
+ // Sort items by their position in the document
124
+ newItems.sort((a, b) => {
125
+ const aRect = a.element.getBoundingClientRect();
126
+ const bRect = b.element.getBoundingClientRect();
127
+ return aRect.top - bRect.top;
128
+ });
129
+
130
+ setItems(newItems);
131
+ }, [getTextContent, generateId]);
132
+
133
+ // Set up intersection observer to track active section
134
+ React.useEffect(() => {
135
+ const observer = new IntersectionObserver(
136
+ (entries) => {
137
+ // Skip intersection updates during manual navigation
138
+ if (manualNavigation) return;
139
+
140
+ entries.forEach((entry) => {
141
+ if (entry.isIntersecting) {
142
+ setActiveId(entry.target.id);
143
+ }
144
+ });
145
+ },
146
+ {
147
+ rootMargin: "-80px 0px -80% 0px",
148
+ threshold: 0,
149
+ }
150
+ );
151
+
152
+ items.forEach((item) => {
153
+ observer.observe(item.element);
154
+ });
155
+
156
+ return () => {
157
+ items.forEach((item) => {
158
+ observer.unobserve(item.element);
159
+ });
160
+ };
161
+ }, [items, manualNavigation]);
162
+
163
+ // Scan page on mount and when dependencies change
164
+ React.useEffect(() => {
165
+ // Small delay to ensure DOM is ready
166
+ const timer = setTimeout(() => {
167
+ scanPage();
168
+ }, 100);
169
+
170
+ // Also re-scan when URL changes (for SPA navigation)
171
+ const handleLocationChange = () => {
172
+ setTimeout(() => {
173
+ scanPage();
174
+ }, 150); // Slightly longer delay for route transitions
175
+ };
176
+
177
+ // Listen for popstate (browser back/forward)
178
+ window.addEventListener('popstate', handleLocationChange);
179
+
180
+ // Listen for custom navigation events
181
+ window.addEventListener('locationchange', handleLocationChange);
182
+
183
+ // MutationObserver to detect DOM changes
184
+ const observer = new MutationObserver(() => {
185
+ // Debounce the scan to avoid too many re-scans
186
+ clearTimeout(timer);
187
+ setTimeout(() => {
188
+ scanPage();
189
+ }, 200);
190
+ });
191
+
192
+ // Observe changes to the main content area
193
+ const mainContent = document.querySelector('main') || document.body;
194
+ observer.observe(mainContent, {
195
+ childList: true,
196
+ subtree: true,
197
+ attributes: true,
198
+ attributeFilter: ['data-section-title', 'id'],
199
+ });
200
+
201
+ return () => {
202
+ clearTimeout(timer);
203
+ window.removeEventListener('popstate', handleLocationChange);
204
+ window.removeEventListener('locationchange', handleLocationChange);
205
+ observer.disconnect();
206
+ };
207
+ }, [scanPage]);
208
+
209
+ // Handle click on index item
210
+ const handleItemClick = React.useCallback((id: string) => {
211
+ const element = document.getElementById(id);
212
+ if (element) {
213
+ // Set manual navigation flag to prevent intersection observer interference
214
+ setManualNavigation(true);
215
+ setActiveId(id);
216
+
217
+ element.scrollIntoView({
218
+ behavior: "smooth",
219
+ block: "start",
220
+ });
221
+
222
+ // Reset manual navigation flag after scroll animation completes
223
+ setTimeout(() => {
224
+ setManualNavigation(false);
225
+ }, 1000); // 1 second should be enough for smooth scroll
226
+ }
227
+ }, []);
228
+
229
+ const classes = [
230
+ "page-index",
231
+ collapsed && "page-index--collapsed",
232
+ className,
233
+ ].filter(Boolean).join(" ");
234
+
235
+ if (items.length === 0) {
236
+ return null;
237
+ }
238
+
239
+ // Remove custom props that shouldn't be passed to DOM
240
+ const { sidebarWidth, setSidebarWidth, ...domProps } = props as any;
241
+
242
+ return (
243
+ <div className={classes} {...domProps}>
244
+ {!collapsed && (
245
+ <h3 className="page-index__title">{title}</h3>
246
+ )}
247
+ <nav className="page-index__nav" aria-label="Page contents">
248
+ <ul className="page-index__list">
249
+ {items.map((item) => (
250
+ <li
251
+ key={item.id}
252
+ className={`page-index__item page-index__item--level-${item.level}`}
253
+ >
254
+ <button
255
+ className={`page-index__link ${
256
+ activeId === item.id ? "page-index__link--active" : ""
257
+ }`}
258
+ onClick={() => handleItemClick(item.id)}
259
+ aria-current={activeId === item.id ? "location" : undefined}
260
+ >
261
+ {collapsed ? (
262
+ <span className="page-index__link-text--collapsed">
263
+ {item.title.charAt(0)}
264
+ </span>
265
+ ) : (
266
+ item.title
267
+ )}
268
+ </button>
269
+ </li>
270
+ ))}
271
+ </ul>
272
+ </nav>
273
+ </div>
274
+ );
275
+ };
@@ -0,0 +1 @@
1
+ export * from "./PageIndex";
@@ -0,0 +1 @@
1
+ export { Popover, type PopoverProps } from './popover';
@@ -0,0 +1,199 @@
1
+ import * as React from "react";
2
+ import * as ReactDOM from "react-dom";
3
+ import "../../style/components/popover/popover.css";
4
+
5
+ export interface PopoverProps {
6
+ trigger: React.ReactNode;
7
+ children: React.ReactNode;
8
+ isOpen?: boolean;
9
+ onToggle?: () => void;
10
+ placement?: 'top' | 'bottom' | 'left' | 'right';
11
+ }
12
+
13
+ type ResolvedPlacement = 'top' | 'bottom' | 'left' | 'right';
14
+
15
+ export const Popover: React.FC<PopoverProps> = ({
16
+ trigger,
17
+ children,
18
+ isOpen = false,
19
+ onToggle,
20
+ placement = 'bottom'
21
+ }) => {
22
+ const [internalOpen, setInternalOpen] = React.useState(false);
23
+ const triggerRef = React.useRef<HTMLDivElement>(null);
24
+ const popoverRef = React.useRef<HTMLDivElement>(null);
25
+ const [popoverStyle, setPopoverStyle] = React.useState<React.CSSProperties>({});
26
+ const [resolvedPlacement, setResolvedPlacement] = React.useState<ResolvedPlacement>(placement);
27
+
28
+ const open = isOpen !== undefined ? isOpen : internalOpen;
29
+ const toggle = onToggle || (() => setInternalOpen(!internalOpen));
30
+
31
+ // Fecha ao clicar fora
32
+ React.useEffect(() => {
33
+ const handleClickOutside = (event: MouseEvent) => {
34
+ if (
35
+ triggerRef.current &&
36
+ !triggerRef.current.contains(event.target as Node) &&
37
+ popoverRef.current &&
38
+ !popoverRef.current.contains(event.target as Node)
39
+ ) {
40
+ if (onToggle) {
41
+ onToggle();
42
+ } else {
43
+ setInternalOpen(false);
44
+ }
45
+ }
46
+ };
47
+
48
+ if (open) {
49
+ document.addEventListener('mousedown', handleClickOutside);
50
+ }
51
+
52
+ return () => {
53
+ document.removeEventListener('mousedown', handleClickOutside);
54
+ };
55
+ }, [open, onToggle]);
56
+
57
+ // Calcula posição com ajuste automático baseado no espaço disponível
58
+ React.useEffect(() => {
59
+ if (!open || !triggerRef.current || !popoverRef.current) {
60
+ return;
61
+ }
62
+
63
+ const updatePosition = () => {
64
+ if (!triggerRef.current || !popoverRef.current) {
65
+ return;
66
+ }
67
+
68
+ const triggerRect = triggerRef.current.getBoundingClientRect();
69
+ const popoverRect = popoverRef.current.getBoundingClientRect();
70
+ const gap = 8;
71
+ const viewportWidth = window.innerWidth;
72
+ const viewportHeight = window.innerHeight;
73
+
74
+ // Calcula espaço disponível em cada direção
75
+ const spaceTop = triggerRect.top;
76
+ const spaceBottom = viewportHeight - triggerRect.bottom;
77
+ const spaceLeft = triggerRect.left;
78
+ const spaceRight = viewportWidth - triggerRect.right;
79
+
80
+ // Determina o melhor placement baseado no espaço disponível
81
+ let finalPlacement: ResolvedPlacement = placement as ResolvedPlacement;
82
+
83
+ if (placement === 'right' && spaceRight < popoverRect.width + gap) {
84
+ finalPlacement = spaceLeft >= popoverRect.width + gap ? 'left' : 'bottom';
85
+ } else if (placement === 'left' && spaceLeft < popoverRect.width + gap) {
86
+ finalPlacement = spaceRight >= popoverRect.width + gap ? 'right' : 'bottom';
87
+ } else if (placement === 'bottom' && spaceBottom < popoverRect.height + gap) {
88
+ finalPlacement = spaceTop >= popoverRect.height + gap ? 'top' : 'bottom';
89
+ } else if (placement === 'top' && spaceTop < popoverRect.height + gap) {
90
+ finalPlacement = spaceBottom >= popoverRect.height + gap ? 'bottom' : 'top';
91
+ }
92
+
93
+ setResolvedPlacement(finalPlacement);
94
+
95
+ const newStyle: React.CSSProperties = {
96
+ position: 'fixed',
97
+ zIndex: 9999,
98
+ opacity: 1,
99
+ visibility: 'visible',
100
+ pointerEvents: 'auto',
101
+ };
102
+
103
+ // Calcula posição baseada no placement final
104
+ switch (finalPlacement) {
105
+ case 'right':
106
+ newStyle.top = triggerRect.top;
107
+ newStyle.left = triggerRect.right + gap;
108
+ break;
109
+ case 'left':
110
+ newStyle.top = triggerRect.top;
111
+ newStyle.left = triggerRect.left - popoverRect.width - gap;
112
+ break;
113
+ case 'top':
114
+ newStyle.top = triggerRect.top - popoverRect.height - gap;
115
+ newStyle.left = triggerRect.left;
116
+ break;
117
+ case 'bottom':
118
+ default:
119
+ newStyle.top = triggerRect.bottom + gap;
120
+ newStyle.left = triggerRect.left;
121
+ }
122
+
123
+ // Ajuste para não sair da viewport horizontalmente
124
+ if (typeof newStyle.left === 'number') {
125
+ if (newStyle.left + popoverRect.width > viewportWidth - gap) {
126
+ newStyle.left = viewportWidth - popoverRect.width - gap;
127
+ }
128
+ if (newStyle.left < gap) {
129
+ newStyle.left = gap;
130
+ }
131
+ }
132
+
133
+ // Ajuste para não sair da viewport verticalmente
134
+ if (typeof newStyle.top === 'number') {
135
+ if (newStyle.top + popoverRect.height > viewportHeight - gap) {
136
+ newStyle.top = viewportHeight - popoverRect.height - gap;
137
+ }
138
+ if (newStyle.top < gap) {
139
+ newStyle.top = gap;
140
+ }
141
+ }
142
+
143
+ setPopoverStyle(newStyle);
144
+ };
145
+
146
+ // Usa requestAnimationFrame para garantir que o popover foi renderizado
147
+ requestAnimationFrame(updatePosition);
148
+
149
+ window.addEventListener('resize', updatePosition);
150
+ window.addEventListener('scroll', updatePosition, true);
151
+
152
+ return () => {
153
+ window.removeEventListener('resize', updatePosition);
154
+ window.removeEventListener('scroll', updatePosition, true);
155
+ };
156
+ }, [open, placement]);
157
+
158
+ const handleTriggerClick = () => {
159
+ toggle();
160
+ };
161
+
162
+ return (
163
+ <>
164
+ <div
165
+ ref={triggerRef}
166
+ onClick={handleTriggerClick}
167
+ style={{ display: 'inline-block', position: 'relative' }}
168
+ >
169
+ {React.isValidElement(trigger)
170
+ ? React.cloneElement(trigger as React.ReactElement, {
171
+ onClick: (e: React.MouseEvent) => {
172
+ e.stopPropagation();
173
+ handleTriggerClick();
174
+ (trigger as React.ReactElement).props?.onClick?.(e);
175
+ }
176
+ })
177
+ : trigger}
178
+ </div>
179
+ {open &&
180
+ ReactDOM.createPortal(
181
+ <div
182
+ ref={popoverRef}
183
+ className={`popover popover--${resolvedPlacement}`}
184
+ style={{
185
+ ...popoverStyle,
186
+ // Start with opacity 0 and visibility hidden to prevent initial flash
187
+ opacity: popoverStyle.top !== undefined ? 1 : 0,
188
+ visibility: popoverStyle.top !== undefined ? 'visible' : 'hidden',
189
+ }}
190
+ >
191
+ <div className="popover__content">
192
+ {children}
193
+ </div>
194
+ </div>,
195
+ document.body
196
+ )}
197
+ </>
198
+ );
199
+ };
@@ -0,0 +1,176 @@
1
+ import React from 'react';
2
+ import { Radio as AriakitRadio, RadioProps as AriakitRadioProps, RadioGroup as AriakitRadioGroup, RadioGroupProps as AriakitRadioGroupProps, useRadioStore } from '@ariakit/react';
3
+
4
+ export type RadioSize = 'sm' | 'md' | 'lg';
5
+
6
+ export interface RadioProps extends Omit<AriakitRadioProps, 'children' | 'size'> {
7
+ /**
8
+ * Size of the radio button
9
+ * @default 'md'
10
+ */
11
+ size?: RadioSize;
12
+
13
+ /**
14
+ * Label text for the radio button
15
+ */
16
+ label?: React.ReactNode;
17
+
18
+ /**
19
+ * Error state
20
+ */
21
+ error?: boolean;
22
+ }
23
+
24
+ export interface RadioGroupProps extends Omit<AriakitRadioGroupProps, 'onChange'> {
25
+ /**
26
+ * Size of all radio buttons in the group
27
+ * @default 'md'
28
+ */
29
+ size?: RadioSize;
30
+
31
+ /**
32
+ * Label for the radio group
33
+ */
34
+ label?: string;
35
+
36
+ /**
37
+ * Error message to display below the radio group
38
+ */
39
+ error?: string;
40
+
41
+ /**
42
+ * Helper text to display below the radio group
43
+ */
44
+ helperText?: string;
45
+
46
+ /**
47
+ * Children (Radio components)
48
+ */
49
+ children: React.ReactNode;
50
+
51
+ /**
52
+ * Orientation of the radio group
53
+ * @default 'vertical'
54
+ */
55
+ orientation?: 'horizontal' | 'vertical';
56
+
57
+ /**
58
+ * The value of the selected radio button
59
+ */
60
+ value?: string | number | null;
61
+
62
+ /**
63
+ * Callback when the selected value changes
64
+ */
65
+ onChange?: (value: string | number | null) => void;
66
+ }
67
+
68
+ export const Radio = React.forwardRef<HTMLInputElement, RadioProps>(
69
+ (
70
+ {
71
+ size = 'md',
72
+ label,
73
+ error = false,
74
+ disabled = false,
75
+ className,
76
+ ...props
77
+ },
78
+ ref
79
+ ) => {
80
+ const radioClasses = [
81
+ 'radio',
82
+ `radio--${size}`,
83
+ error && 'radio--error',
84
+ disabled && 'radio--disabled',
85
+ className,
86
+ ]
87
+ .filter(Boolean)
88
+ .join(' ');
89
+
90
+ return (
91
+ <label className={radioClasses}>
92
+ <AriakitRadio
93
+ ref={ref}
94
+ disabled={disabled}
95
+ className="radio__input"
96
+ {...props}
97
+ />
98
+ <span className="radio__box">
99
+ <span className="radio__dot" />
100
+ </span>
101
+ {label && <span className="radio__label">{label}</span>}
102
+ </label>
103
+ );
104
+ }
105
+ );
106
+
107
+ Radio.displayName = 'Radio';
108
+
109
+ export const RadioGroup = React.forwardRef<HTMLDivElement, RadioGroupProps>(
110
+ (
111
+ {
112
+ size = 'md',
113
+ label,
114
+ error,
115
+ helperText,
116
+ children,
117
+ orientation = 'vertical',
118
+ disabled = false,
119
+ className,
120
+ value,
121
+ onChange,
122
+ ...props
123
+ },
124
+ ref
125
+ ) => {
126
+ const groupClasses = [
127
+ 'radio-group',
128
+ `radio-group--${orientation}`,
129
+ error && 'radio-group--error',
130
+ disabled && 'radio-group--disabled',
131
+ className,
132
+ ]
133
+ .filter(Boolean)
134
+ .join(' ');
135
+
136
+ // Create radio group store
137
+ const radioGroupStore = useRadioStore({
138
+ value,
139
+ setValue: onChange,
140
+ });
141
+
142
+ // Pass size down to children
143
+ const childrenWithProps = React.Children.map(children, (child) => {
144
+ if (React.isValidElement(child) && child.type === Radio) {
145
+ return React.cloneElement(child, {
146
+ size: child.props.size || size,
147
+ error: error ? true : child.props.error,
148
+ disabled: disabled || child.props.disabled,
149
+ } as any);
150
+ }
151
+ return child;
152
+ });
153
+
154
+ return (
155
+ <div className={groupClasses}>
156
+ {label && <div className="radio-group__label">{label}</div>}
157
+ <AriakitRadioGroup
158
+ ref={ref}
159
+ store={radioGroupStore}
160
+ className="radio-group__container"
161
+ {...props}
162
+ >
163
+ {childrenWithProps}
164
+ </AriakitRadioGroup>
165
+ {(error || helperText) && (
166
+ <div className="radio-group__feedback">
167
+ {error && <span className="radio-group__error-text">{error}</span>}
168
+ {!error && helperText && <span className="radio-group__helper-text">{helperText}</span>}
169
+ </div>
170
+ )}
171
+ </div>
172
+ );
173
+ }
174
+ );
175
+
176
+ RadioGroup.displayName = 'RadioGroup';
@@ -0,0 +1 @@
1
+ export * from './Radio';
@@ -0,0 +1 @@
1
+ export { Section, type SectionProps, type SectionSize } from './section';