@proyecto-viviana/ui 0.3.1 → 0.3.3

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 (74) 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
@@ -0,0 +1,196 @@
1
+ /**
2
+ * TimeField component for proyecto-viviana-ui
3
+ *
4
+ * Styled time field component with segment-based editing.
5
+ */
6
+
7
+ import { type JSX, splitProps } from 'solid-js';
8
+ import {
9
+ TimeField as HeadlessTimeField,
10
+ TimeInput,
11
+ TimeSegment,
12
+ type TimeFieldProps as HeadlessTimeFieldProps,
13
+ type TimeValue,
14
+ } from '@proyecto-viviana/solidaria-components';
15
+
16
+ // ============================================
17
+ // TYPES
18
+ // ============================================
19
+
20
+ export type TimeFieldSize = 'sm' | 'md' | 'lg';
21
+
22
+ export interface TimeFieldProps<T extends TimeValue = TimeValue>
23
+ extends Omit<HeadlessTimeFieldProps<T>, 'class' | 'style' | 'children'> {
24
+ /** The size of the field. @default 'md' */
25
+ size?: TimeFieldSize;
26
+ /** Additional CSS class name. */
27
+ class?: string;
28
+ /** Label for the field. */
29
+ label?: string;
30
+ /** Description text. */
31
+ description?: string;
32
+ /** Error message. */
33
+ errorMessage?: string;
34
+ }
35
+
36
+ // ============================================
37
+ // STYLES
38
+ // ============================================
39
+
40
+ const sizeStyles = {
41
+ sm: {
42
+ container: 'text-sm',
43
+ input: 'px-2 py-1 gap-0.5',
44
+ segment: 'px-0.5',
45
+ label: 'text-xs',
46
+ },
47
+ md: {
48
+ container: 'text-base',
49
+ input: 'px-3 py-2 gap-1',
50
+ segment: 'px-1',
51
+ label: 'text-sm',
52
+ },
53
+ lg: {
54
+ container: 'text-lg',
55
+ input: 'px-4 py-3 gap-1.5',
56
+ segment: 'px-1.5',
57
+ label: 'text-base',
58
+ },
59
+ };
60
+
61
+ // ============================================
62
+ // TIME FIELD COMPONENT
63
+ // ============================================
64
+
65
+ /**
66
+ * A time field allows users to enter and edit time values using a keyboard.
67
+ *
68
+ * @example
69
+ * ```tsx
70
+ * // Basic usage
71
+ * <TimeField label="Start time" />
72
+ *
73
+ * // With 24-hour format
74
+ * <TimeField
75
+ * label="Meeting time"
76
+ * hourCycle={24}
77
+ * />
78
+ *
79
+ * // With seconds
80
+ * <TimeField
81
+ * label="Precise time"
82
+ * granularity="second"
83
+ * />
84
+ * ```
85
+ */
86
+ export function TimeField<T extends TimeValue = TimeValue>(
87
+ props: TimeFieldProps<T>
88
+ ): JSX.Element {
89
+ const [local, rest] = splitProps(props, [
90
+ 'size',
91
+ 'class',
92
+ 'label',
93
+ 'description',
94
+ 'errorMessage',
95
+ 'isInvalid',
96
+ ]);
97
+
98
+ const size = () => local.size ?? 'md';
99
+ const sizeConfig = () => sizeStyles[size()];
100
+ const isInvalid = () => local.isInvalid || !!local.errorMessage;
101
+
102
+ return (
103
+ <HeadlessTimeField
104
+ {...rest}
105
+ isInvalid={isInvalid()}
106
+ class={`
107
+ flex flex-col gap-1
108
+ ${sizeConfig().container}
109
+ ${local.class ?? ''}
110
+ `}
111
+ >
112
+ {/* Label */}
113
+ {local.label && (
114
+ <label class={`font-medium text-primary-200 ${sizeConfig().label}`}>
115
+ {local.label}
116
+ {rest.isRequired && <span class="text-red-500 ml-0.5">*</span>}
117
+ </label>
118
+ )}
119
+
120
+ {/* Input container */}
121
+ <TimeInput
122
+ class={({ isFocused, isDisabled }) => {
123
+ const base = `
124
+ inline-flex items-center
125
+ ${sizeConfig().input}
126
+ bg-bg-400 rounded-md border
127
+ transition-colors duration-150
128
+ `;
129
+
130
+ let borderClass = 'border-primary-600';
131
+ if (isInvalid()) {
132
+ borderClass = 'border-red-500';
133
+ } else if (isFocused) {
134
+ borderClass = 'border-accent';
135
+ }
136
+
137
+ const disabledClass = isDisabled
138
+ ? 'opacity-50 cursor-not-allowed'
139
+ : '';
140
+
141
+ const focusClass = isFocused
142
+ ? 'ring-2 ring-accent/30'
143
+ : '';
144
+
145
+ return `${base} ${borderClass} ${disabledClass} ${focusClass}`.trim();
146
+ }}
147
+ >
148
+ {(segment) => (
149
+ <TimeSegment
150
+ segment={segment}
151
+ class={({ isFocused, isPlaceholder, isEditable }) => {
152
+ const base = `
153
+ ${sizeConfig().segment}
154
+ rounded
155
+ outline-none
156
+ tabular-nums
157
+ `;
158
+
159
+ let stateClass = '';
160
+ if (segment.type === 'literal') {
161
+ stateClass = 'text-primary-400';
162
+ } else if (isPlaceholder) {
163
+ stateClass = 'text-primary-500 italic';
164
+ } else {
165
+ stateClass = 'text-primary-100';
166
+ }
167
+
168
+ const focusClass = isFocused && isEditable
169
+ ? 'bg-accent text-white'
170
+ : '';
171
+
172
+ return `${base} ${stateClass} ${focusClass}`.trim();
173
+ }}
174
+ />
175
+ )}
176
+ </TimeInput>
177
+
178
+ {/* Description */}
179
+ {local.description && !isInvalid() && (
180
+ <p class={`text-primary-400 ${sizeConfig().label}`}>
181
+ {local.description}
182
+ </p>
183
+ )}
184
+
185
+ {/* Error message */}
186
+ {isInvalid() && local.errorMessage && (
187
+ <p class={`text-red-500 ${sizeConfig().label}`}>
188
+ {local.errorMessage}
189
+ </p>
190
+ )}
191
+ </HeadlessTimeField>
192
+ );
193
+ }
194
+
195
+ // Re-export types
196
+ export type { TimeValue };
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Calendar component for proyecto-viviana-ui
3
+ *
4
+ * Styled calendar component built on top of solidaria-components.
5
+ * A calendar displays a grid of days and allows users to select dates.
6
+ */
7
+
8
+ import { type JSX, splitProps } from 'solid-js';
9
+ import {
10
+ Calendar as HeadlessCalendar,
11
+ CalendarHeading,
12
+ CalendarButton,
13
+ CalendarGrid,
14
+ CalendarCell,
15
+ type CalendarDate,
16
+ type DateValue,
17
+ } from '@proyecto-viviana/solidaria-components';
18
+ import type { CalendarStateProps } from '@proyecto-viviana/solid-stately';
19
+
20
+ // ============================================
21
+ // TYPES
22
+ // ============================================
23
+
24
+ export type CalendarSize = 'sm' | 'md' | 'lg';
25
+
26
+ export interface CalendarProps<T extends DateValue = DateValue>
27
+ extends Omit<CalendarStateProps<T>, 'locale'> {
28
+ /** The size of the calendar. @default 'md' */
29
+ size?: CalendarSize;
30
+ /** Additional CSS class name. */
31
+ class?: string;
32
+ /** Whether to show week numbers. */
33
+ showWeekNumbers?: boolean;
34
+ /** The locale to use for formatting. */
35
+ locale?: string;
36
+ /** Custom aria label. */
37
+ 'aria-label'?: string;
38
+ }
39
+
40
+ // ============================================
41
+ // STYLES
42
+ // ============================================
43
+
44
+ const sizeStyles = {
45
+ sm: {
46
+ container: 'w-64',
47
+ header: 'text-sm',
48
+ cell: 'w-8 h-8 text-xs',
49
+ button: 'w-6 h-6',
50
+ },
51
+ md: {
52
+ container: 'w-80',
53
+ header: 'text-base',
54
+ cell: 'w-10 h-10 text-sm',
55
+ button: 'w-8 h-8',
56
+ },
57
+ lg: {
58
+ container: 'w-96',
59
+ header: 'text-lg',
60
+ cell: 'w-12 h-12 text-base',
61
+ button: 'w-10 h-10',
62
+ },
63
+ };
64
+
65
+ // ============================================
66
+ // CALENDAR COMPONENT
67
+ // ============================================
68
+
69
+ /**
70
+ * A calendar displays a grid of days and allows users to select a date.
71
+ *
72
+ * @example
73
+ * ```tsx
74
+ * // Basic usage
75
+ * <Calendar
76
+ * aria-label="Event date"
77
+ * onChange={(date) => console.log(date)}
78
+ * />
79
+ *
80
+ * // Controlled
81
+ * const [date, setDate] = createSignal<CalendarDate | null>(null);
82
+ * <Calendar
83
+ * value={date()}
84
+ * onChange={setDate}
85
+ * />
86
+ *
87
+ * // With min/max dates
88
+ * <Calendar
89
+ * minValue={today(getLocalTimeZone())}
90
+ * maxValue={today(getLocalTimeZone()).add({ months: 3 })}
91
+ * />
92
+ * ```
93
+ */
94
+ export function Calendar<T extends DateValue = CalendarDate>(
95
+ props: CalendarProps<T>
96
+ ): JSX.Element {
97
+ const [local, rest] = splitProps(props, [
98
+ 'size',
99
+ 'class',
100
+ 'showWeekNumbers',
101
+ 'aria-label',
102
+ ]);
103
+
104
+ const size = () => local.size ?? 'md';
105
+ const sizeConfig = () => sizeStyles[size()];
106
+
107
+ return (
108
+ <HeadlessCalendar
109
+ {...rest}
110
+ aria-label={local['aria-label']}
111
+ class={`
112
+ ${sizeConfig().container}
113
+ bg-bg-500 rounded-lg border border-primary-700 p-4
114
+ ${local.class ?? ''}
115
+ `}
116
+ >
117
+ {/* Header with navigation */}
118
+ <header class="flex items-center justify-between mb-4">
119
+ <CalendarButton
120
+ slot="previous"
121
+ class={`
122
+ ${sizeConfig().button}
123
+ flex items-center justify-center
124
+ rounded-md text-primary-200
125
+ hover:bg-bg-400 transition-colors
126
+ disabled:opacity-50 disabled:cursor-not-allowed
127
+ focus:outline-none focus:ring-2 focus:ring-accent/50
128
+ `}
129
+ >
130
+ <svg
131
+ viewBox="0 0 24 24"
132
+ fill="none"
133
+ stroke="currentColor"
134
+ stroke-width="2"
135
+ stroke-linecap="round"
136
+ stroke-linejoin="round"
137
+ class="w-4 h-4"
138
+ >
139
+ <polyline points="15 18 9 12 15 6" />
140
+ </svg>
141
+ </CalendarButton>
142
+
143
+ <CalendarHeading
144
+ class={`
145
+ font-semibold text-primary-100
146
+ ${sizeConfig().header}
147
+ `}
148
+ />
149
+
150
+ <CalendarButton
151
+ slot="next"
152
+ class={`
153
+ ${sizeConfig().button}
154
+ flex items-center justify-center
155
+ rounded-md text-primary-200
156
+ hover:bg-bg-400 transition-colors
157
+ disabled:opacity-50 disabled:cursor-not-allowed
158
+ focus:outline-none focus:ring-2 focus:ring-accent/50
159
+ `}
160
+ >
161
+ <svg
162
+ viewBox="0 0 24 24"
163
+ fill="none"
164
+ stroke="currentColor"
165
+ stroke-width="2"
166
+ stroke-linecap="round"
167
+ stroke-linejoin="round"
168
+ class="w-4 h-4"
169
+ >
170
+ <polyline points="9 18 15 12 9 6" />
171
+ </svg>
172
+ </CalendarButton>
173
+ </header>
174
+
175
+ {/* Calendar grid */}
176
+ <CalendarGrid
177
+ class="w-full border-collapse"
178
+ >
179
+ {(date) => (
180
+ <CalendarCell
181
+ date={date}
182
+ class={({ isSelected, isFocused, isDisabled, isOutsideMonth, isToday, isPressed }) => {
183
+ const base = `
184
+ ${sizeConfig().cell}
185
+ flex items-center justify-center
186
+ rounded-md cursor-pointer
187
+ transition-colors duration-150
188
+ focus:outline-none
189
+ `;
190
+
191
+ let stateClass = '';
192
+
193
+ if (isDisabled) {
194
+ stateClass = 'text-primary-600 cursor-not-allowed';
195
+ } else if (isSelected) {
196
+ stateClass = 'bg-accent text-white font-medium';
197
+ } else if (isOutsideMonth) {
198
+ stateClass = 'text-primary-600';
199
+ } else if (isToday) {
200
+ stateClass = 'ring-1 ring-accent text-primary-100';
201
+ } else {
202
+ stateClass = 'text-primary-200 hover:bg-bg-400';
203
+ }
204
+
205
+ const focusClass = isFocused && !isSelected
206
+ ? 'ring-2 ring-accent/50'
207
+ : '';
208
+
209
+ const pressedClass = isPressed && !isDisabled
210
+ ? 'scale-95'
211
+ : '';
212
+
213
+ return `${base} ${stateClass} ${focusClass} ${pressedClass}`.trim();
214
+ }}
215
+ />
216
+ )}
217
+ </CalendarGrid>
218
+ </HeadlessCalendar>
219
+ );
220
+ }
221
+
222
+ // Re-export types
223
+ export type { CalendarDate, DateValue };
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Checkbox component for proyecto-viviana-ui
3
+ *
4
+ * A styled checkbox component built on top of solidaria-components.
5
+ * This component only handles styling - all behavior and accessibility
6
+ * is provided by the headless Checkbox from solidaria-components.
7
+ */
8
+
9
+ import { type JSX, splitProps, mergeProps as solidMergeProps, Show } from 'solid-js'
10
+ import {
11
+ Checkbox as HeadlessCheckbox,
12
+ CheckboxGroup as HeadlessCheckboxGroup,
13
+ type CheckboxProps as HeadlessCheckboxProps,
14
+ type CheckboxGroupProps as HeadlessCheckboxGroupProps,
15
+ type CheckboxRenderProps,
16
+ type CheckboxGroupRenderProps,
17
+ } from '@proyecto-viviana/solidaria-components'
18
+
19
+ // ============================================
20
+ // TYPES
21
+ // ============================================
22
+
23
+ export type CheckboxSize = 'sm' | 'md' | 'lg'
24
+
25
+ export interface CheckboxProps extends Omit<HeadlessCheckboxProps, 'class' | 'children' | 'style'> {
26
+ /** The size of the checkbox. */
27
+ size?: CheckboxSize
28
+ /** Additional CSS class name. */
29
+ class?: string
30
+ /** Label text for the checkbox. */
31
+ children?: JSX.Element
32
+ }
33
+
34
+ export interface CheckboxGroupProps extends Omit<HeadlessCheckboxGroupProps, 'class' | 'children' | 'style'> {
35
+ /** Additional CSS class name. */
36
+ class?: string
37
+ /** Children checkboxes. */
38
+ children?: JSX.Element
39
+ /** Label for the group. */
40
+ label?: string
41
+ /** Description for the group. */
42
+ description?: string
43
+ /** Error message when invalid. */
44
+ errorMessage?: string
45
+ }
46
+
47
+ // ============================================
48
+ // STYLES
49
+ // ============================================
50
+
51
+ const sizeStyles = {
52
+ sm: {
53
+ box: 'h-4 w-4',
54
+ icon: 'h-3 w-3',
55
+ label: 'text-sm',
56
+ },
57
+ md: {
58
+ box: 'h-5 w-5',
59
+ icon: 'h-3.5 w-3.5',
60
+ label: 'text-base',
61
+ },
62
+ lg: {
63
+ box: 'h-6 w-6',
64
+ icon: 'h-4 w-4',
65
+ label: 'text-lg',
66
+ },
67
+ }
68
+
69
+ // ============================================
70
+ // ICONS
71
+ // ============================================
72
+
73
+ function CheckIcon(props: { class?: string }) {
74
+ return (
75
+ <svg
76
+ class={props.class}
77
+ viewBox="0 0 12 10"
78
+ fill="none"
79
+ xmlns="http://www.w3.org/2000/svg"
80
+ >
81
+ <path
82
+ d="M1 5L4.5 8.5L11 1"
83
+ stroke="currentColor"
84
+ stroke-width="2"
85
+ stroke-linecap="round"
86
+ stroke-linejoin="round"
87
+ />
88
+ </svg>
89
+ )
90
+ }
91
+
92
+ function IndeterminateIcon(props: { class?: string }) {
93
+ return (
94
+ <svg
95
+ class={props.class}
96
+ viewBox="0 0 12 2"
97
+ fill="none"
98
+ xmlns="http://www.w3.org/2000/svg"
99
+ >
100
+ <path
101
+ d="M1 1H11"
102
+ stroke="currentColor"
103
+ stroke-width="2"
104
+ stroke-linecap="round"
105
+ />
106
+ </svg>
107
+ )
108
+ }
109
+
110
+ // ============================================
111
+ // CHECKBOX COMPONENT
112
+ // ============================================
113
+
114
+ /**
115
+ * A checkbox allows users to select one or more items from a set.
116
+ *
117
+ * Built on solidaria-components Checkbox for full accessibility support.
118
+ */
119
+ export function Checkbox(props: CheckboxProps): JSX.Element {
120
+ const defaultProps: Partial<CheckboxProps> = {
121
+ size: 'md',
122
+ }
123
+
124
+ const merged = solidMergeProps(defaultProps, props)
125
+
126
+ const [local, headlessProps] = splitProps(merged, [
127
+ 'size',
128
+ 'class',
129
+ 'children',
130
+ ])
131
+
132
+ const size = () => sizeStyles[local.size!]
133
+
134
+ // Generate class based on render props
135
+ const getClassName = (renderProps: CheckboxRenderProps): string => {
136
+ const base = 'inline-flex items-center gap-2 cursor-pointer'
137
+ const disabledClass = renderProps.isDisabled ? 'cursor-not-allowed opacity-50' : ''
138
+ const custom = local.class || ''
139
+ return [base, disabledClass, custom].filter(Boolean).join(' ')
140
+ }
141
+
142
+ return (
143
+ <HeadlessCheckbox
144
+ {...headlessProps}
145
+ class={getClassName}
146
+ >
147
+ {(renderProps: CheckboxRenderProps) => {
148
+ const boxClasses = () => {
149
+ const base = 'relative flex items-center justify-center rounded border-2 transition-all duration-200'
150
+ const sizeClass = size().box
151
+
152
+ let colorClass: string
153
+ if (renderProps.isDisabled) {
154
+ colorClass = 'border-bg-300 bg-bg-200'
155
+ } else if (renderProps.isSelected || renderProps.isIndeterminate) {
156
+ colorClass = 'border-accent bg-accent'
157
+ } else {
158
+ colorClass = 'border-primary-600 bg-transparent hover:border-accent-300'
159
+ }
160
+
161
+ const focusClass = renderProps.isFocusVisible
162
+ ? 'ring-2 ring-accent-300 ring-offset-2 ring-offset-bg-400'
163
+ : ''
164
+ const cursorClass = renderProps.isDisabled ? 'cursor-not-allowed' : 'cursor-pointer'
165
+
166
+ return [base, sizeClass, colorClass, focusClass, cursorClass].filter(Boolean).join(' ')
167
+ }
168
+
169
+ const iconClasses = () => {
170
+ const base = 'text-white transition-opacity duration-200'
171
+ const sizeClass = size().icon
172
+ const visibilityClass = (renderProps.isSelected || renderProps.isIndeterminate)
173
+ ? 'opacity-100'
174
+ : 'opacity-0'
175
+
176
+ return [base, sizeClass, visibilityClass].filter(Boolean).join(' ')
177
+ }
178
+
179
+ const labelClasses = () => {
180
+ const base = 'text-primary-200'
181
+ const sizeClass = size().label
182
+ const disabledClass = renderProps.isDisabled ? 'opacity-50' : ''
183
+
184
+ return [base, sizeClass, disabledClass].filter(Boolean).join(' ')
185
+ }
186
+
187
+ return (
188
+ <>
189
+ <span class={boxClasses()}>
190
+ <Show
191
+ when={!renderProps.isIndeterminate}
192
+ fallback={<IndeterminateIcon class={iconClasses()} />}
193
+ >
194
+ <CheckIcon class={iconClasses()} />
195
+ </Show>
196
+ </span>
197
+ <Show when={props.children}>
198
+ <span class={labelClasses()}>{props.children}</span>
199
+ </Show>
200
+ </>
201
+ )
202
+ }}
203
+ </HeadlessCheckbox>
204
+ )
205
+ }
206
+
207
+ // ============================================
208
+ // CHECKBOX GROUP COMPONENT
209
+ // ============================================
210
+
211
+ /**
212
+ * A checkbox group allows users to select multiple items from a list.
213
+ *
214
+ * Built on solidaria-components CheckboxGroup for full accessibility support.
215
+ */
216
+ export function CheckboxGroup(props: CheckboxGroupProps): JSX.Element {
217
+ const [local, headlessProps] = splitProps(props, [
218
+ 'class',
219
+ 'label',
220
+ 'description',
221
+ 'errorMessage',
222
+ ])
223
+
224
+ // Generate class based on render props
225
+ const getClassName = (renderProps: CheckboxGroupRenderProps): string => {
226
+ const base = 'flex flex-col gap-2'
227
+ const disabledClass = renderProps.isDisabled ? 'opacity-50' : ''
228
+ const custom = local.class || ''
229
+ return [base, disabledClass, custom].filter(Boolean).join(' ')
230
+ }
231
+
232
+ // Render children function for the headless component
233
+ const renderChildren = (renderProps: CheckboxGroupRenderProps) => (
234
+ <>
235
+ <Show when={local.label}>
236
+ <span class="text-sm font-medium text-primary-200">{local.label}</span>
237
+ </Show>
238
+ <div class="flex flex-col gap-2">
239
+ {props.children}
240
+ </div>
241
+ <Show when={local.description && !renderProps.isInvalid}>
242
+ <span class="text-sm text-primary-400">{local.description}</span>
243
+ </Show>
244
+ <Show when={local.errorMessage && renderProps.isInvalid}>
245
+ <span class="text-sm text-danger-400">{local.errorMessage}</span>
246
+ </Show>
247
+ </>
248
+ )
249
+
250
+ return (
251
+ <HeadlessCheckboxGroup
252
+ {...headlessProps}
253
+ class={getClassName}
254
+ children={renderChildren as any}
255
+ />
256
+ )
257
+ }