@proyecto-viviana/ui 0.3.2 → 0.3.4

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 (76) hide show
  1. package/dist/components.css +1077 -1077
  2. package/dist/index.js +236 -249
  3. package/dist/index.js.map +3 -3
  4. package/dist/index.ssr.js +78 -81
  5. package/dist/index.ssr.js.map +3 -3
  6. package/dist/radio/index.d.ts +12 -27
  7. package/dist/radio/index.d.ts.map +1 -1
  8. package/dist/test-utils/index.d.ts +2 -2
  9. package/dist/test-utils/index.d.ts.map +1 -1
  10. package/package.json +13 -12
  11. package/src/alert/index.tsx +48 -0
  12. package/src/assets/favicon.png +0 -0
  13. package/src/assets/fire.gif +0 -0
  14. package/src/autocomplete/index.tsx +313 -0
  15. package/src/avatar/index.tsx +75 -0
  16. package/src/badge/index.tsx +43 -0
  17. package/src/breadcrumbs/index.tsx +207 -0
  18. package/src/button/Button.tsx +74 -0
  19. package/src/button/index.ts +2 -0
  20. package/src/button/types.ts +24 -0
  21. package/src/calendar/DateField.tsx +200 -0
  22. package/src/calendar/DatePicker.tsx +298 -0
  23. package/src/calendar/RangeCalendar.tsx +236 -0
  24. package/src/calendar/TimeField.tsx +196 -0
  25. package/src/calendar/index.tsx +223 -0
  26. package/src/checkbox/index.tsx +257 -0
  27. package/src/color/index.tsx +687 -0
  28. package/src/combobox/index.tsx +383 -0
  29. package/src/components.css +1077 -0
  30. package/src/custom/calendar-card/index.tsx +66 -0
  31. package/src/custom/chip/index.tsx +46 -0
  32. package/src/custom/conversation/index.tsx +105 -0
  33. package/src/custom/event-card/index.tsx +132 -0
  34. package/src/custom/header/index.tsx +33 -0
  35. package/src/custom/lateral-nav/index.tsx +88 -0
  36. package/src/custom/logo/index.tsx +58 -0
  37. package/src/custom/nav-header/index.tsx +42 -0
  38. package/src/custom/page-layout/index.tsx +29 -0
  39. package/src/custom/profile-card/index.tsx +64 -0
  40. package/src/custom/project-card/index.tsx +59 -0
  41. package/src/custom/timeline-item/index.tsx +105 -0
  42. package/src/dialog/Dialog.tsx +260 -0
  43. package/src/dialog/index.tsx +3 -0
  44. package/src/disclosure/index.tsx +307 -0
  45. package/src/gridlist/index.tsx +403 -0
  46. package/src/icon/icons/GitHubIcon.tsx +20 -0
  47. package/src/icon/index.tsx +48 -0
  48. package/src/index.ts +322 -0
  49. package/src/landmark/index.tsx +231 -0
  50. package/src/link/index.tsx +130 -0
  51. package/src/listbox/index.tsx +231 -0
  52. package/src/menu/index.tsx +297 -0
  53. package/src/meter/index.tsx +163 -0
  54. package/src/numberfield/index.tsx +482 -0
  55. package/src/popover/index.tsx +260 -0
  56. package/src/progress-bar/index.tsx +169 -0
  57. package/src/radio/index.tsx +173 -0
  58. package/src/searchfield/index.tsx +453 -0
  59. package/src/select/index.tsx +349 -0
  60. package/src/separator/index.tsx +141 -0
  61. package/src/slider/index.tsx +382 -0
  62. package/src/styles.css +450 -0
  63. package/src/switch/ToggleSwitch.tsx +112 -0
  64. package/src/switch/index.tsx +90 -0
  65. package/src/table/index.tsx +531 -0
  66. package/src/tabs/index.tsx +273 -0
  67. package/src/tag-group/index.tsx +240 -0
  68. package/src/test-utils/index.ts +40 -0
  69. package/src/textfield/index.tsx +211 -0
  70. package/src/theme.css +101 -0
  71. package/src/toast/index.tsx +324 -0
  72. package/src/toolbar/index.tsx +108 -0
  73. package/src/tooltip/index.tsx +197 -0
  74. package/src/tree/index.tsx +494 -0
  75. package/dist/index.jsx +0 -6658
  76. package/dist/index.jsx.map +0 -7
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Popover component for proyecto-viviana-ui
3
+ *
4
+ * A popover displays content in an overlay positioned relative to a trigger.
5
+ * Built on top of solidaria-components for accessibility.
6
+ * Follows Spectrum 2 design patterns.
7
+ */
8
+
9
+ import { type JSX, Show, splitProps } from 'solid-js'
10
+ import {
11
+ Popover as HeadlessPopover,
12
+ PopoverTrigger as HeadlessPopoverTrigger,
13
+ OverlayArrow as HeadlessOverlayArrow,
14
+ type PopoverProps as HeadlessPopoverProps,
15
+ type PopoverTriggerProps as HeadlessPopoverTriggerProps,
16
+ type PopoverRenderProps,
17
+ } from '@proyecto-viviana/solidaria-components'
18
+ import type { Placement, PlacementAxis } from '@proyecto-viviana/solidaria'
19
+
20
+ // ============================================
21
+ // TYPES
22
+ // ============================================
23
+
24
+ export type PopoverPlacement = Placement
25
+ export type PopoverSize = 'sm' | 'md' | 'lg'
26
+
27
+ export interface PopoverTriggerProps extends HeadlessPopoverTriggerProps {
28
+ /** The children of the popover trigger (trigger element and popover). */
29
+ children: JSX.Element
30
+ }
31
+
32
+ export interface PopoverProps extends Omit<HeadlessPopoverProps, 'class' | 'style' | 'children'> {
33
+ /** The content of the popover. */
34
+ children: JSX.Element
35
+ /** The position of the popover relative to the trigger. */
36
+ placement?: PopoverPlacement
37
+ /** Size variant of the popover. */
38
+ size?: PopoverSize
39
+ /** Additional CSS class name. */
40
+ class?: string
41
+ /** Whether to show an arrow pointing to the trigger. */
42
+ showArrow?: boolean
43
+ /** Custom padding inside the popover. */
44
+ padding?: 'none' | 'sm' | 'md' | 'lg'
45
+ }
46
+
47
+ // ============================================
48
+ // STYLES
49
+ // ============================================
50
+
51
+ const baseStyles = [
52
+ 'bg-bg-300',
53
+ 'rounded-lg',
54
+ 'shadow-xl',
55
+ 'border border-primary-700',
56
+ 'text-primary-200',
57
+ 'outline-none',
58
+ // Animation
59
+ 'animate-in fade-in-0 zoom-in-95',
60
+ 'data-[placement=top]:slide-in-from-bottom-2',
61
+ 'data-[placement=bottom]:slide-in-from-top-2',
62
+ 'data-[placement=left]:slide-in-from-right-2',
63
+ 'data-[placement=right]:slide-in-from-left-2',
64
+ 'data-[exiting]:animate-out data-[exiting]:fade-out-0 data-[exiting]:zoom-out-95',
65
+ ].join(' ')
66
+
67
+ const sizeStyles: Record<PopoverSize, string> = {
68
+ sm: 'max-w-xs',
69
+ md: 'max-w-sm',
70
+ lg: 'max-w-lg',
71
+ }
72
+
73
+ const paddingStyles: Record<string, string> = {
74
+ none: '',
75
+ sm: 'p-2',
76
+ md: 'p-4',
77
+ lg: 'p-6',
78
+ }
79
+
80
+ // Arrow styles based on placement
81
+ const arrowBaseStyles = [
82
+ 'fill-bg-300',
83
+ 'stroke-primary-700',
84
+ 'stroke-1',
85
+ ].join(' ')
86
+
87
+ // Arrow positioning for each placement axis
88
+ const getArrowRotation = (placement: PlacementAxis | null): string => {
89
+ switch (placement) {
90
+ case 'top':
91
+ return 'rotate-180'
92
+ case 'bottom':
93
+ return ''
94
+ case 'left':
95
+ return 'rotate-90'
96
+ case 'right':
97
+ return '-rotate-90'
98
+ default:
99
+ return ''
100
+ }
101
+ }
102
+
103
+ // ============================================
104
+ // COMPONENTS
105
+ // ============================================
106
+
107
+ /**
108
+ * PopoverTrigger wraps around a trigger element and a Popover.
109
+ * It handles opening and closing the Popover when the user interacts
110
+ * with the trigger.
111
+ *
112
+ * @example
113
+ * ```tsx
114
+ * <PopoverTrigger>
115
+ * <Button>Open Popover</Button>
116
+ * <Popover>
117
+ * <p>Popover content here!</p>
118
+ * </Popover>
119
+ * </PopoverTrigger>
120
+ * ```
121
+ */
122
+ export function PopoverTrigger(props: PopoverTriggerProps): JSX.Element {
123
+ return <HeadlessPopoverTrigger {...props} />
124
+ }
125
+
126
+ /**
127
+ * Styled popover component that displays content in an overlay.
128
+ *
129
+ * @example
130
+ * ```tsx
131
+ * <PopoverTrigger>
132
+ * <Button>Settings</Button>
133
+ * <Popover placement="bottom" size="md">
134
+ * <h3>Settings</h3>
135
+ * <p>Configure your preferences here.</p>
136
+ * </Popover>
137
+ * </PopoverTrigger>
138
+ * ```
139
+ */
140
+ export function Popover(props: PopoverProps): JSX.Element {
141
+ const [local, rest] = splitProps(props, [
142
+ 'placement',
143
+ 'size',
144
+ 'class',
145
+ 'showArrow',
146
+ 'padding',
147
+ ])
148
+
149
+ const placement = () => local.placement ?? 'bottom'
150
+ const size = () => local.size ?? 'md'
151
+ const padding = () => local.padding ?? 'md'
152
+
153
+ return (
154
+ <HeadlessPopover
155
+ {...rest}
156
+ placement={placement()}
157
+ class={(_renderProps: PopoverRenderProps) => {
158
+ const classes = [
159
+ baseStyles,
160
+ sizeStyles[size()],
161
+ paddingStyles[padding()],
162
+ local.class ?? '',
163
+ ].filter(Boolean).join(' ')
164
+ return classes
165
+ }}
166
+ >
167
+ {(renderProps: PopoverRenderProps) => (
168
+ <>
169
+ <Show when={local.showArrow}>
170
+ <PopoverArrow placement={renderProps.placement} />
171
+ </Show>
172
+ {props.children}
173
+ </>
174
+ )}
175
+ </HeadlessPopover>
176
+ )
177
+ }
178
+
179
+ /**
180
+ * Arrow component for the popover.
181
+ * Automatically positions itself based on the popover placement.
182
+ */
183
+ interface PopoverArrowProps {
184
+ /** The current placement axis. */
185
+ placement: PlacementAxis | null
186
+ /** Additional CSS class. */
187
+ class?: string
188
+ }
189
+
190
+ function PopoverArrow(props: PopoverArrowProps): JSX.Element {
191
+ return (
192
+ <HeadlessOverlayArrow
193
+ class="absolute block"
194
+ style={{
195
+ // Position based on placement
196
+ ...(props.placement === 'top' && { bottom: '100%', left: '50%', transform: 'translateX(-50%)' }),
197
+ ...(props.placement === 'bottom' && { top: '-8px', left: '50%', transform: 'translateX(-50%)' }),
198
+ ...(props.placement === 'left' && { right: '100%', top: '50%', transform: 'translateY(-50%)' }),
199
+ ...(props.placement === 'right' && { left: '-8px', top: '50%', transform: 'translateY(-50%)' }),
200
+ }}
201
+ >
202
+ <svg
203
+ width="12"
204
+ height="12"
205
+ viewBox="0 0 12 12"
206
+ class={`${arrowBaseStyles} ${getArrowRotation(props.placement)} ${props.class ?? ''}`}
207
+ >
208
+ <path d="M0 0 L6 6 L12 0" />
209
+ </svg>
210
+ </HeadlessOverlayArrow>
211
+ )
212
+ }
213
+
214
+ // ============================================
215
+ // POPOVER CONTENT SECTIONS
216
+ // ============================================
217
+
218
+ export interface PopoverHeaderProps {
219
+ /** The title of the popover. */
220
+ title: string
221
+ /** Optional description text. */
222
+ description?: string
223
+ /** Additional CSS class. */
224
+ class?: string
225
+ }
226
+
227
+ /**
228
+ * Header section for popover with title and optional description.
229
+ */
230
+ export function PopoverHeader(props: PopoverHeaderProps): JSX.Element {
231
+ return (
232
+ <div class={`mb-3 ${props.class ?? ''}`}>
233
+ <h3 class="text-lg font-semibold text-primary-100">{props.title}</h3>
234
+ <Show when={props.description}>
235
+ <p class="text-sm text-primary-400 mt-1">{props.description}</p>
236
+ </Show>
237
+ </div>
238
+ )
239
+ }
240
+
241
+ export interface PopoverFooterProps {
242
+ /** Footer content, typically buttons. */
243
+ children: JSX.Element
244
+ /** Additional CSS class. */
245
+ class?: string
246
+ }
247
+
248
+ /**
249
+ * Footer section for popover actions.
250
+ */
251
+ export function PopoverFooter(props: PopoverFooterProps): JSX.Element {
252
+ return (
253
+ <div class={`flex gap-2 justify-end mt-4 pt-3 border-t border-primary-700 ${props.class ?? ''}`}>
254
+ {props.children}
255
+ </div>
256
+ )
257
+ }
258
+
259
+ // Re-export types
260
+ export type { PopoverRenderProps, Placement, PlacementAxis }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * ProgressBar component for proyecto-viviana-ui
3
+ *
4
+ * Styled progress bar component built on top of the solidaria hook directly.
5
+ */
6
+
7
+ import { type JSX, splitProps, Show, createMemo } from 'solid-js';
8
+ import { createProgressBar } from '@proyecto-viviana/solidaria';
9
+
10
+ // ============================================
11
+ // TYPES
12
+ // ============================================
13
+
14
+ export type ProgressBarSize = 'sm' | 'md' | 'lg';
15
+ export type ProgressBarVariant = 'primary' | 'accent' | 'success' | 'warning' | 'danger';
16
+
17
+ export interface ProgressBarProps {
18
+ /** The current value (controlled). @default 0 */
19
+ value?: number;
20
+ /** The smallest value allowed. @default 0 */
21
+ minValue?: number;
22
+ /** The largest value allowed. @default 100 */
23
+ maxValue?: number;
24
+ /** The content to display as the value's label (e.g. "1 of 4"). */
25
+ valueLabel?: string;
26
+ /** Whether presentation is indeterminate when progress isn't known. */
27
+ isIndeterminate?: boolean;
28
+ /** The size of the progress bar. @default 'md' */
29
+ size?: ProgressBarSize;
30
+ /** The visual style variant. @default 'primary' */
31
+ variant?: ProgressBarVariant;
32
+ /** The label to display above the progress bar. */
33
+ label?: string;
34
+ /** Whether to show the value text. @default true for determinate progress */
35
+ showValueLabel?: boolean;
36
+ /** Additional CSS class name. */
37
+ class?: string;
38
+ /** An accessibility label. */
39
+ 'aria-label'?: string;
40
+ }
41
+
42
+ // ============================================
43
+ // STYLES
44
+ // ============================================
45
+
46
+ const sizeStyles = {
47
+ sm: {
48
+ track: 'h-1',
49
+ text: 'text-xs',
50
+ },
51
+ md: {
52
+ track: 'h-2',
53
+ text: 'text-sm',
54
+ },
55
+ lg: {
56
+ track: 'h-3',
57
+ text: 'text-base',
58
+ },
59
+ };
60
+
61
+ const variantStyles = {
62
+ primary: 'bg-primary-500',
63
+ accent: 'bg-accent',
64
+ success: 'bg-green-500',
65
+ warning: 'bg-yellow-500',
66
+ danger: 'bg-red-500',
67
+ };
68
+
69
+ // ============================================
70
+ // UTILITIES
71
+ // ============================================
72
+
73
+ function clamp(value: number, min: number, max: number): number {
74
+ return Math.min(Math.max(value, min), max);
75
+ }
76
+
77
+ // ============================================
78
+ // PROGRESSBAR COMPONENT
79
+ // ============================================
80
+
81
+ /**
82
+ * Progress bars show either determinate or indeterminate progress of an operation
83
+ * over time.
84
+ *
85
+ * @example
86
+ * ```tsx
87
+ * <ProgressBar value={50} label="Loading..." />
88
+ *
89
+ * // Indeterminate
90
+ * <ProgressBar isIndeterminate label="Processing..." />
91
+ *
92
+ * // Different variants
93
+ * <ProgressBar value={75} variant="success" />
94
+ * ```
95
+ */
96
+ export function ProgressBar(props: ProgressBarProps): JSX.Element {
97
+ const [local, ariaProps] = splitProps(props, [
98
+ 'size',
99
+ 'variant',
100
+ 'label',
101
+ 'showValueLabel',
102
+ 'class',
103
+ ]);
104
+
105
+ const size = () => local.size ?? 'md';
106
+ const variant = () => local.variant ?? 'primary';
107
+ const isIndeterminate = () => ariaProps.isIndeterminate ?? false;
108
+ const showValueLabel = () => local.showValueLabel ?? !isIndeterminate();
109
+
110
+ // Create progress bar aria props
111
+ const progressAria = createProgressBar({
112
+ get value() { return ariaProps.value; },
113
+ get minValue() { return ariaProps.minValue; },
114
+ get maxValue() { return ariaProps.maxValue; },
115
+ get valueLabel() { return ariaProps.valueLabel; },
116
+ get isIndeterminate() { return ariaProps.isIndeterminate; },
117
+ get label() { return local.label; },
118
+ get 'aria-label'() { return ariaProps['aria-label']; },
119
+ });
120
+
121
+ // Calculate percentage
122
+ const percentage = createMemo(() => {
123
+ if (isIndeterminate()) {
124
+ return undefined;
125
+ }
126
+ const value = ariaProps.value ?? 0;
127
+ const minValue = ariaProps.minValue ?? 0;
128
+ const maxValue = ariaProps.maxValue ?? 100;
129
+ const clampedValue = clamp(value, minValue, maxValue);
130
+ return ((clampedValue - minValue) / (maxValue - minValue)) * 100;
131
+ });
132
+
133
+ // Get value text from aria props
134
+ const valueText = () => progressAria.progressBarProps['aria-valuetext'] as string | undefined;
135
+
136
+ const sizeConfig = () => sizeStyles[size()];
137
+
138
+ return (
139
+ <div
140
+ {...progressAria.progressBarProps}
141
+ class={`w-full ${local.class ?? ''}`}
142
+ >
143
+ {/* Label and value row */}
144
+ <Show when={local.label || showValueLabel()}>
145
+ <div class={`flex justify-between items-center mb-1 ${sizeConfig().text}`}>
146
+ <Show when={local.label}>
147
+ <span class="text-primary-200 font-medium">{local.label}</span>
148
+ </Show>
149
+ <Show when={showValueLabel() && !isIndeterminate()}>
150
+ <span class="text-primary-300">{valueText()}</span>
151
+ </Show>
152
+ </div>
153
+ </Show>
154
+
155
+ {/* Track */}
156
+ <div class={`w-full ${sizeConfig().track} bg-bg-300 rounded-full overflow-hidden`}>
157
+ {/* Fill */}
158
+ <div
159
+ class={`h-full rounded-full transition-all duration-300 ${variantStyles[variant()]} ${
160
+ isIndeterminate() ? 'animate-progress-indeterminate' : ''
161
+ }`}
162
+ style={{
163
+ width: isIndeterminate() ? '30%' : `${percentage()}%`,
164
+ }}
165
+ />
166
+ </div>
167
+ </div>
168
+ );
169
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * RadioGroup and Radio components for proyecto-viviana-ui
3
+ *
4
+ * Styled radio components built on top of solidaria-components.
5
+ * SSR-compatible - renders children and UI elements directly without render props.
6
+ */
7
+
8
+ import { type JSX, Show, createContext, useContext, splitProps } from 'solid-js'
9
+ import {
10
+ RadioGroup as HeadlessRadioGroup,
11
+ Radio as HeadlessRadio,
12
+ type RadioGroupProps as HeadlessRadioGroupProps,
13
+ type RadioProps as HeadlessRadioProps,
14
+ type RadioGroupRenderProps,
15
+ type RadioRenderProps,
16
+ } from '@proyecto-viviana/solidaria-components'
17
+
18
+ // ============================================
19
+ // SIZE CONTEXT
20
+ // ============================================
21
+
22
+ export type RadioGroupOrientation = 'horizontal' | 'vertical'
23
+ export type RadioGroupSize = 'sm' | 'md' | 'lg'
24
+
25
+ const RadioSizeContext = createContext<RadioGroupSize>('md')
26
+
27
+ // ============================================
28
+ // TYPES
29
+ // ============================================
30
+
31
+ export interface RadioGroupProps extends Omit<HeadlessRadioGroupProps, 'class' | 'style'> {
32
+ /** The size of the radio buttons. */
33
+ size?: RadioGroupSize
34
+ /** Additional CSS class name. */
35
+ class?: string
36
+ /** Label for the group. */
37
+ label?: string
38
+ /** Description for the group. */
39
+ description?: string
40
+ /** Error message when invalid. */
41
+ errorMessage?: string
42
+ }
43
+
44
+ export interface RadioProps extends Omit<HeadlessRadioProps, 'class' | 'style'> {
45
+ /** Additional CSS class name. */
46
+ class?: string
47
+ }
48
+
49
+ // ============================================
50
+ // STYLES
51
+ // ============================================
52
+
53
+ const sizeStyles = {
54
+ sm: {
55
+ circle: 'h-4 w-4',
56
+ dot: 'h-2 w-2',
57
+ label: 'text-sm',
58
+ },
59
+ md: {
60
+ circle: 'h-5 w-5',
61
+ dot: 'h-2.5 w-2.5',
62
+ label: 'text-base',
63
+ },
64
+ lg: {
65
+ circle: 'h-6 w-6',
66
+ dot: 'h-3 w-3',
67
+ label: 'text-lg',
68
+ },
69
+ }
70
+
71
+ // ============================================
72
+ // RADIO GROUP COMPONENT
73
+ // ============================================
74
+
75
+ /**
76
+ * A radio group allows users to select a single option from a list of mutually exclusive options.
77
+ *
78
+ * Built on solidaria-components RadioGroup for full accessibility support.
79
+ */
80
+ export function RadioGroup(props: RadioGroupProps): JSX.Element {
81
+ // Split out our custom styling props from the rest
82
+ const [local, headlessProps] = splitProps(props, [
83
+ 'size',
84
+ 'class',
85
+ 'label',
86
+ 'description',
87
+ 'errorMessage',
88
+ ])
89
+
90
+ const size = local.size ?? 'md'
91
+ const customClass = local.class ?? ''
92
+
93
+ // Generate class based on render props
94
+ const getClassName = (renderProps: RadioGroupRenderProps): string => {
95
+ const base = 'flex gap-2'
96
+ const orientationClass = renderProps.orientation === 'horizontal' ? 'flex-row flex-wrap' : 'flex-col'
97
+ const disabledClass = renderProps.isDisabled ? 'opacity-50' : ''
98
+ return [base, orientationClass, disabledClass, customClass].filter(Boolean).join(' ')
99
+ }
100
+
101
+ // Pass remaining props through to headless component
102
+ // headlessProps maintains reactivity for controlled values like value/onChange
103
+ return (
104
+ <RadioSizeContext.Provider value={size}>
105
+ <HeadlessRadioGroup
106
+ {...headlessProps}
107
+ class={getClassName}
108
+ data-size={size}
109
+ >
110
+ <Show when={local.label}>
111
+ <span class="text-primary-200 font-medium mb-1">{local.label}</span>
112
+ </Show>
113
+ {props.children as JSX.Element}
114
+ <Show when={local.description}>
115
+ <span class="text-primary-400 text-sm [&:has(~[data-invalid])]:hidden">{local.description}</span>
116
+ </Show>
117
+ <Show when={local.errorMessage}>
118
+ <span class="text-danger-400 text-sm hidden [[data-invalid]_&]:block">{local.errorMessage}</span>
119
+ </Show>
120
+ </HeadlessRadioGroup>
121
+ </RadioSizeContext.Provider>
122
+ )
123
+ }
124
+
125
+ // ============================================
126
+ // RADIO COMPONENT
127
+ // ============================================
128
+
129
+ /**
130
+ * A radio button allows users to select a single option from a list.
131
+ * Must be used within a RadioGroup.
132
+ * SSR-compatible - renders static JSX without render prop children.
133
+ *
134
+ * Note: Unlike other styled components, Radio does not use render props for children.
135
+ * Instead, it relies on data attributes set by the headless Radio component for styling.
136
+ * However, since we need dynamic styling based on state, we accept that this component
137
+ * has some limitations compared to the render-props-based original implementation.
138
+ *
139
+ * Built on solidaria-components Radio for full accessibility support.
140
+ */
141
+ export function Radio(props: RadioProps): JSX.Element {
142
+ const [local, headlessProps] = splitProps(props, ['class'])
143
+ const sizeFromContext = useContext(RadioSizeContext)
144
+ const sizeStyle = sizeStyles[sizeFromContext]
145
+ const customClass = local.class ?? ''
146
+
147
+ // Generate class based on render props
148
+ const getClassName = (renderProps: RadioRenderProps): string => {
149
+ const base = 'inline-flex items-center gap-2'
150
+ const cursorClass = renderProps.isDisabled ? 'cursor-not-allowed' : 'cursor-pointer'
151
+ const disabledClass = renderProps.isDisabled ? 'opacity-50' : ''
152
+ return [base, cursorClass, disabledClass, customClass].filter(Boolean).join(' ')
153
+ }
154
+
155
+ // Static classes - will use a simplified visual style since we can't dynamically style based on state without render props
156
+ const circleClass = `relative flex items-center justify-center rounded-full border-2 transition-all duration-200 ${sizeStyle.circle} border-primary-600 bg-transparent hover:border-accent-300`
157
+ const dotClass = `rounded-full bg-accent transition-all duration-200 ${sizeStyle.dot}`
158
+ const labelClass = `text-primary-200 ${sizeStyle.label}`
159
+
160
+ return (
161
+ <HeadlessRadio
162
+ {...headlessProps}
163
+ class={getClassName}
164
+ >
165
+ <span class={circleClass}>
166
+ <span class={dotClass} />
167
+ </span>
168
+ <Show when={props.children}>
169
+ <span class={labelClass}>{props.children as JSX.Element}</span>
170
+ </Show>
171
+ </HeadlessRadio>
172
+ )
173
+ }