@proyecto-viviana/solidaria-components 0.0.1 → 0.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proyecto-viviana/solidaria-components",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Pre-wired headless components for SolidJS - port of react-aria-components",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -8,13 +8,14 @@
8
8
  "types": "./dist/index.d.ts",
9
9
  "exports": {
10
10
  ".": {
11
- "solid": "./dist/index.js",
11
+ "solid": "./src/index.ts",
12
12
  "import": "./dist/index.js",
13
13
  "types": "./dist/index.d.ts"
14
14
  }
15
15
  },
16
16
  "files": [
17
- "dist"
17
+ "dist",
18
+ "src"
18
19
  ],
19
20
  "sideEffects": false,
20
21
  "scripts": {
package/src/Button.tsx ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Button component for solidaria-components
3
+ *
4
+ * A pre-wired headless button that combines state + aria hooks.
5
+ * Port of react-aria-components/src/Button.tsx
6
+ */
7
+
8
+ import {
9
+ type JSX,
10
+ createContext,
11
+ createMemo,
12
+ splitProps,
13
+ } from 'solid-js';
14
+ import {
15
+ createButton,
16
+ createFocusRing,
17
+ createHover,
18
+ type AriaButtonProps,
19
+ } from '@proyecto-viviana/solidaria';
20
+ import {
21
+ type RenderChildren,
22
+ type ClassNameOrFunction,
23
+ type StyleOrFunction,
24
+ type SlotProps,
25
+ useRenderProps,
26
+ filterDOMProps,
27
+ } from './utils';
28
+
29
+ // ============================================
30
+ // TYPES
31
+ // ============================================
32
+
33
+ export interface ButtonRenderProps {
34
+ /** Whether the button is currently hovered with a mouse. */
35
+ isHovered: boolean;
36
+ /** Whether the button is currently in a pressed state. */
37
+ isPressed: boolean;
38
+ /** Whether the button is focused, either via a mouse or keyboard. */
39
+ isFocused: boolean;
40
+ /** Whether the button is keyboard focused. */
41
+ isFocusVisible: boolean;
42
+ /** Whether the button is disabled. */
43
+ isDisabled: boolean;
44
+ }
45
+
46
+ export interface ButtonProps
47
+ extends Omit<AriaButtonProps, 'children'>,
48
+ SlotProps {
49
+ /** The children of the component. A function may be provided to receive render props. */
50
+ children?: RenderChildren<ButtonRenderProps>;
51
+ /** The CSS className for the element. */
52
+ class?: ClassNameOrFunction<ButtonRenderProps>;
53
+ /** The inline style for the element. */
54
+ style?: StyleOrFunction<ButtonRenderProps>;
55
+ }
56
+
57
+ // ============================================
58
+ // CONTEXT
59
+ // ============================================
60
+
61
+ export const ButtonContext = createContext<ButtonProps | null>(null);
62
+
63
+ // ============================================
64
+ // COMPONENT
65
+ // ============================================
66
+
67
+ /**
68
+ * A button allows a user to perform an action.
69
+ *
70
+ * This is a headless component that provides accessibility and state management.
71
+ * Style it using the render props pattern or data attributes.
72
+ *
73
+ * @example
74
+ * ```tsx
75
+ * <Button onPress={() => alert('Pressed!')}>
76
+ * {({ isPressed, isHovered }) => (
77
+ * <span class={isPressed ? 'bg-blue-700' : isHovered ? 'bg-blue-600' : 'bg-blue-500'}>
78
+ * Click me
79
+ * </span>
80
+ * )}
81
+ * </Button>
82
+ * ```
83
+ */
84
+ export function Button(props: ButtonProps): JSX.Element {
85
+ // Split props
86
+ const [local, ariaProps] = splitProps(props, [
87
+ 'children',
88
+ 'class',
89
+ 'style',
90
+ 'slot',
91
+ ]);
92
+
93
+ // Helper to resolve isDisabled (handles both boolean and Accessor<boolean>)
94
+ const resolveDisabled = (): boolean => {
95
+ const disabled = ariaProps.isDisabled;
96
+ if (typeof disabled === 'function') {
97
+ return disabled();
98
+ }
99
+ return !!disabled;
100
+ };
101
+
102
+ // Create button aria props
103
+ const buttonAria = createButton({
104
+ ...ariaProps,
105
+ get isDisabled() {
106
+ return resolveDisabled();
107
+ },
108
+ });
109
+
110
+ // Create focus ring
111
+ const { isFocused, isFocusVisible, focusProps } = createFocusRing();
112
+
113
+ // Create hover
114
+ const { isHovered, hoverProps } = createHover({
115
+ get isDisabled() {
116
+ return resolveDisabled();
117
+ },
118
+ });
119
+
120
+ // Render props values
121
+ const renderValues = createMemo<ButtonRenderProps>(() => ({
122
+ isHovered: isHovered(),
123
+ isPressed: buttonAria.isPressed(),
124
+ isFocused: isFocused(),
125
+ isFocusVisible: isFocusVisible(),
126
+ isDisabled: resolveDisabled(),
127
+ }));
128
+
129
+ // Resolve render props
130
+ const renderProps = useRenderProps(
131
+ {
132
+ children: local.children,
133
+ class: local.class,
134
+ style: local.style,
135
+ defaultClassName: 'solidaria-Button',
136
+ },
137
+ renderValues
138
+ );
139
+
140
+ // Filter DOM props
141
+ const domProps = createMemo(() => {
142
+ const filtered = filterDOMProps(ariaProps, { global: true });
143
+ return filtered;
144
+ });
145
+
146
+ // Remove ref from spread props to avoid type conflicts
147
+ const cleanButtonProps = () => {
148
+ const { ref: _ref1, ...rest } = buttonAria.buttonProps as Record<string, unknown>;
149
+ return rest;
150
+ };
151
+ const cleanFocusProps = () => {
152
+ const { ref: _ref2, ...rest } = focusProps as Record<string, unknown>;
153
+ return rest;
154
+ };
155
+ const cleanHoverProps = () => {
156
+ const { ref: _ref3, ...rest } = hoverProps as Record<string, unknown>;
157
+ return rest;
158
+ };
159
+
160
+ return (
161
+ <button
162
+ {...domProps()}
163
+ {...cleanButtonProps()}
164
+ {...cleanFocusProps()}
165
+ {...cleanHoverProps()}
166
+ class={renderProps().class}
167
+ style={renderProps().style}
168
+ data-pressed={buttonAria.isPressed() || undefined}
169
+ data-hovered={isHovered() || undefined}
170
+ data-focused={isFocused() || undefined}
171
+ data-focus-visible={isFocusVisible() || undefined}
172
+ data-disabled={resolveDisabled() || undefined}
173
+ >
174
+ {renderProps().children}
175
+ </button>
176
+ );
177
+ }
@@ -0,0 +1,378 @@
1
+ /**
2
+ * Checkbox and CheckboxGroup components for solidaria-components
3
+ *
4
+ * Pre-wired headless checkbox components that combine state + aria hooks.
5
+ * Port of react-aria-components/src/Checkbox.tsx
6
+ */
7
+
8
+ import {
9
+ type JSX,
10
+ type Accessor,
11
+ type ParentProps,
12
+ createContext,
13
+ useContext,
14
+ createMemo,
15
+ splitProps,
16
+ } from 'solid-js';
17
+ import {
18
+ createCheckbox,
19
+ createCheckboxGroup,
20
+ createCheckboxGroupItem,
21
+ createFocusRing,
22
+ createHover,
23
+ type AriaCheckboxProps,
24
+ type AriaCheckboxGroupProps,
25
+ } from '@proyecto-viviana/solidaria';
26
+ import {
27
+ createToggleState,
28
+ createCheckboxGroupState,
29
+ type CheckboxGroupState,
30
+ } from '@proyecto-viviana/solid-stately';
31
+ import { VisuallyHidden } from './VisuallyHidden';
32
+ import {
33
+ type RenderChildren,
34
+ type ClassNameOrFunction,
35
+ type StyleOrFunction,
36
+ type SlotProps,
37
+ useRenderProps,
38
+ filterDOMProps,
39
+ } from './utils';
40
+
41
+ // ============================================
42
+ // TYPES
43
+ // ============================================
44
+
45
+ export interface CheckboxGroupRenderProps {
46
+ /** Whether the checkbox group is disabled. */
47
+ isDisabled: boolean;
48
+ /** Whether the checkbox group is read only. */
49
+ isReadOnly: boolean;
50
+ /** Whether the checkbox group is required. */
51
+ isRequired: boolean;
52
+ /** Whether the checkbox group is invalid. */
53
+ isInvalid: boolean;
54
+ /** State of the checkbox group. */
55
+ state: CheckboxGroupState;
56
+ }
57
+
58
+ export interface CheckboxRenderProps {
59
+ /** Whether the checkbox is selected. */
60
+ isSelected: boolean;
61
+ /** Whether the checkbox is indeterminate. */
62
+ isIndeterminate: boolean;
63
+ /** Whether the checkbox is currently hovered with a mouse. */
64
+ isHovered: boolean;
65
+ /** Whether the checkbox is currently in a pressed state. */
66
+ isPressed: boolean;
67
+ /** Whether the checkbox is focused, either via a mouse or keyboard. */
68
+ isFocused: boolean;
69
+ /** Whether the checkbox is keyboard focused. */
70
+ isFocusVisible: boolean;
71
+ /** Whether the checkbox is disabled. */
72
+ isDisabled: boolean;
73
+ /** Whether the checkbox is read only. */
74
+ isReadOnly: boolean;
75
+ /** Whether the checkbox is invalid. */
76
+ isInvalid: boolean;
77
+ /** Whether the checkbox is required. */
78
+ isRequired: boolean;
79
+ }
80
+
81
+ export interface CheckboxGroupProps
82
+ extends Omit<AriaCheckboxGroupProps, 'children' | 'label' | 'description' | 'errorMessage'>,
83
+ SlotProps {
84
+ /** The children of the component. A function may be provided to receive render props. */
85
+ children?: RenderChildren<CheckboxGroupRenderProps>;
86
+ /** The CSS className for the element. */
87
+ class?: ClassNameOrFunction<CheckboxGroupRenderProps>;
88
+ /** The inline style for the element. */
89
+ style?: StyleOrFunction<CheckboxGroupRenderProps>;
90
+ }
91
+
92
+ export interface CheckboxProps
93
+ extends Omit<AriaCheckboxProps, 'children'>,
94
+ SlotProps {
95
+ /** The children of the component. A function may be provided to receive render props. */
96
+ children?: RenderChildren<CheckboxRenderProps>;
97
+ /** The CSS className for the element. */
98
+ class?: ClassNameOrFunction<CheckboxRenderProps>;
99
+ /** The inline style for the element. */
100
+ style?: StyleOrFunction<CheckboxRenderProps>;
101
+ /** Whether the checkbox is indeterminate. */
102
+ isIndeterminate?: boolean;
103
+ }
104
+
105
+ // ============================================
106
+ // CONTEXT
107
+ // ============================================
108
+
109
+ export const CheckboxGroupContext = createContext<CheckboxGroupProps | null>(null);
110
+ export const CheckboxGroupStateContext = createContext<CheckboxGroupState | null>(null);
111
+ export const CheckboxContext = createContext<CheckboxProps | null>(null);
112
+
113
+ // ============================================
114
+ // CHECKBOX GROUP COMPONENT
115
+ // ============================================
116
+
117
+ /**
118
+ * A checkbox group allows a user to select multiple items from a list of options.
119
+ *
120
+ * @example
121
+ * ```tsx
122
+ * <CheckboxGroup>
123
+ * <Checkbox value="one">Option 1</Checkbox>
124
+ * <Checkbox value="two">Option 2</Checkbox>
125
+ * </CheckboxGroup>
126
+ * ```
127
+ */
128
+ export function CheckboxGroup(props: ParentProps<CheckboxGroupProps>): JSX.Element {
129
+ const [local, ariaProps] = splitProps(props, [
130
+ 'children',
131
+ 'class',
132
+ 'style',
133
+ 'slot',
134
+ ]);
135
+
136
+ // Create checkbox group state
137
+ // Use getters to ensure props are read lazily inside reactive contexts
138
+ const state = createCheckboxGroupState({
139
+ get value() { return ariaProps.value; },
140
+ get defaultValue() { return ariaProps.defaultValue; },
141
+ get onChange() { return ariaProps.onChange; },
142
+ get isDisabled() { return ariaProps.isDisabled; },
143
+ get isReadOnly() { return ariaProps.isReadOnly; },
144
+ get isRequired() { return ariaProps.isRequired; },
145
+ get isInvalid() { return ariaProps.isInvalid; },
146
+ });
147
+
148
+ // Create checkbox group aria props
149
+ const groupAria = createCheckboxGroup(() => ariaProps, state);
150
+
151
+ // Render props values
152
+ const renderValues = createMemo<CheckboxGroupRenderProps>(() => ({
153
+ isDisabled: state.isDisabled,
154
+ isReadOnly: state.isReadOnly,
155
+ isRequired: ariaProps.isRequired ?? false,
156
+ isInvalid: groupAria.isInvalid,
157
+ state,
158
+ }));
159
+
160
+ // Resolve render props
161
+ const renderProps = useRenderProps(
162
+ {
163
+ children: local.children,
164
+ class: local.class,
165
+ style: local.style,
166
+ defaultClassName: 'solidaria-CheckboxGroup',
167
+ },
168
+ renderValues
169
+ );
170
+
171
+ // Filter DOM props
172
+ const domProps = createMemo(() => filterDOMProps(ariaProps, { global: true }));
173
+
174
+ // Remove ref from spread props to avoid type conflicts
175
+ const cleanGroupProps = () => {
176
+ const { ref: _ref, ...rest } = groupAria.groupProps as Record<string, unknown>;
177
+ return rest;
178
+ };
179
+
180
+ return (
181
+ <CheckboxGroupStateContext.Provider value={state}>
182
+ <div
183
+ {...domProps()}
184
+ {...cleanGroupProps()}
185
+ class={renderProps().class}
186
+ style={renderProps().style}
187
+ data-disabled={state.isDisabled || undefined}
188
+ data-readonly={state.isReadOnly || undefined}
189
+ data-required={ariaProps.isRequired || undefined}
190
+ data-invalid={groupAria.isInvalid || undefined}
191
+ >
192
+ {renderProps().children}
193
+ </div>
194
+ </CheckboxGroupStateContext.Provider>
195
+ );
196
+ }
197
+
198
+ // ============================================
199
+ // CHECKBOX COMPONENT
200
+ // ============================================
201
+
202
+ /**
203
+ * A checkbox allows a user to select multiple items from a list of individual items,
204
+ * or to mark one individual item as selected.
205
+ *
206
+ * @example
207
+ * ```tsx
208
+ * <Checkbox>
209
+ * {({ isSelected }) => (
210
+ * <>
211
+ * <span class={`checkbox ${isSelected ? 'checked' : ''}`}>
212
+ * {isSelected && '✓'}
213
+ * </span>
214
+ * <span>Accept terms</span>
215
+ * </>
216
+ * )}
217
+ * </Checkbox>
218
+ * ```
219
+ */
220
+ export function Checkbox(props: CheckboxProps): JSX.Element {
221
+ let inputRef: HTMLInputElement | null = null;
222
+
223
+ const [local, ariaProps] = splitProps(props, [
224
+ 'children',
225
+ 'class',
226
+ 'style',
227
+ 'slot',
228
+ 'isIndeterminate',
229
+ ]);
230
+
231
+ // Check if we're inside a CheckboxGroup
232
+ const groupState = useContext(CheckboxGroupStateContext);
233
+
234
+ // Create appropriate state/aria hooks based on context
235
+ let isSelected: Accessor<boolean>;
236
+ let isPressed: Accessor<boolean>;
237
+ let isDisabled: boolean;
238
+ let isReadOnly: boolean;
239
+ let isInvalid: boolean;
240
+ let labelProps: JSX.LabelHTMLAttributes<HTMLLabelElement>;
241
+ let inputProps: JSX.InputHTMLAttributes<HTMLInputElement>;
242
+
243
+ if (groupState) {
244
+ // Inside a CheckboxGroup - use group item
245
+ const itemAria = createCheckboxGroupItem(
246
+ () => ({
247
+ ...ariaProps,
248
+ value: ariaProps.value ?? '',
249
+ children: typeof local.children === 'function' ? true : local.children,
250
+ }),
251
+ groupState,
252
+ () => inputRef
253
+ );
254
+ isSelected = itemAria.isSelected;
255
+ isPressed = itemAria.isPressed;
256
+ isDisabled = itemAria.isDisabled;
257
+ isReadOnly = itemAria.isReadOnly;
258
+ isInvalid = itemAria.isInvalid;
259
+ labelProps = itemAria.labelProps;
260
+ inputProps = itemAria.inputProps;
261
+ } else {
262
+ // Standalone checkbox
263
+ // Use getters to ensure props are read lazily inside reactive contexts
264
+ const state = createToggleState({
265
+ get isSelected() { return ariaProps.isSelected; },
266
+ get defaultSelected() { return ariaProps.defaultSelected; },
267
+ get onChange() { return ariaProps.onChange; },
268
+ get isReadOnly() { return ariaProps.isReadOnly; },
269
+ });
270
+
271
+ const checkboxAria = createCheckbox(
272
+ () => ({
273
+ ...ariaProps,
274
+ isIndeterminate: local.isIndeterminate,
275
+ children: typeof local.children === 'function' ? true : local.children,
276
+ }),
277
+ state,
278
+ () => inputRef
279
+ );
280
+ isSelected = checkboxAria.isSelected;
281
+ isPressed = checkboxAria.isPressed;
282
+ isDisabled = checkboxAria.isDisabled;
283
+ isReadOnly = checkboxAria.isReadOnly;
284
+ isInvalid = checkboxAria.isInvalid;
285
+ labelProps = checkboxAria.labelProps;
286
+ inputProps = checkboxAria.inputProps;
287
+ }
288
+
289
+ // Create focus ring
290
+ const { isFocused, isFocusVisible, focusProps } = createFocusRing();
291
+
292
+ // Create hover
293
+ const { isHovered, hoverProps } = createHover({
294
+ get isDisabled() {
295
+ return isDisabled || isReadOnly;
296
+ },
297
+ });
298
+
299
+ // Render props values
300
+ const renderValues = createMemo<CheckboxRenderProps>(() => ({
301
+ isSelected: isSelected(),
302
+ isIndeterminate: local.isIndeterminate ?? false,
303
+ isHovered: isHovered(),
304
+ isPressed: isPressed(),
305
+ isFocused: isFocused(),
306
+ isFocusVisible: isFocusVisible(),
307
+ isDisabled,
308
+ isReadOnly,
309
+ isInvalid,
310
+ isRequired: ariaProps.isRequired ?? false,
311
+ }));
312
+
313
+ // Resolve render props
314
+ const renderProps = useRenderProps(
315
+ {
316
+ children: local.children,
317
+ class: local.class,
318
+ style: local.style,
319
+ defaultClassName: 'solidaria-Checkbox',
320
+ },
321
+ renderValues
322
+ );
323
+
324
+ // Filter DOM props
325
+ const domProps = createMemo(() => {
326
+ const filtered = filterDOMProps(ariaProps, { global: true });
327
+ delete (filtered as Record<string, unknown>).id;
328
+ delete (filtered as Record<string, unknown>).onClick;
329
+ return filtered;
330
+ });
331
+
332
+ // Remove ref from spread props to avoid type conflicts
333
+ const cleanLabelProps = () => {
334
+ const { ref: _ref1, ...rest } = labelProps as Record<string, unknown>;
335
+ return rest;
336
+ };
337
+ const cleanHoverProps = () => {
338
+ const { ref: _ref2, ...rest } = hoverProps as Record<string, unknown>;
339
+ return rest;
340
+ };
341
+ const cleanInputProps = () => {
342
+ const { ref: _ref3, ...rest } = inputProps as Record<string, unknown>;
343
+ return rest;
344
+ };
345
+ const cleanFocusProps = () => {
346
+ const { ref: _ref4, ...rest } = focusProps as Record<string, unknown>;
347
+ return rest;
348
+ };
349
+
350
+ return (
351
+ <label
352
+ {...domProps()}
353
+ {...cleanLabelProps()}
354
+ {...cleanHoverProps()}
355
+ class={renderProps().class}
356
+ style={renderProps().style}
357
+ data-selected={isSelected() || undefined}
358
+ data-indeterminate={local.isIndeterminate || undefined}
359
+ data-pressed={isPressed() || undefined}
360
+ data-hovered={isHovered() || undefined}
361
+ data-focused={isFocused() || undefined}
362
+ data-focus-visible={isFocusVisible() || undefined}
363
+ data-disabled={isDisabled || undefined}
364
+ data-readonly={isReadOnly || undefined}
365
+ data-invalid={isInvalid || undefined}
366
+ data-required={ariaProps.isRequired || undefined}
367
+ >
368
+ <VisuallyHidden>
369
+ <input
370
+ ref={(el) => (inputRef = el)}
371
+ {...cleanInputProps()}
372
+ {...cleanFocusProps()}
373
+ />
374
+ </VisuallyHidden>
375
+ {renderProps().children}
376
+ </label>
377
+ );
378
+ }