@proyecto-viviana/solidaria-components 0.2.2 → 0.2.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 (64) hide show
  1. package/dist/Color.d.ts +2 -6
  2. package/dist/Color.d.ts.map +1 -1
  3. package/dist/ComboBox.d.ts +3 -3
  4. package/dist/ComboBox.d.ts.map +1 -1
  5. package/dist/GridList.d.ts +2 -2
  6. package/dist/GridList.d.ts.map +1 -1
  7. package/dist/ListBox.d.ts +5 -5
  8. package/dist/ListBox.d.ts.map +1 -1
  9. package/dist/Menu.d.ts +3 -3
  10. package/dist/Menu.d.ts.map +1 -1
  11. package/dist/Select.d.ts +3 -3
  12. package/dist/Select.d.ts.map +1 -1
  13. package/dist/Table.d.ts +2 -2
  14. package/dist/Table.d.ts.map +1 -1
  15. package/dist/Tabs.d.ts +1 -1
  16. package/dist/Tabs.d.ts.map +1 -1
  17. package/dist/index.js +56 -56
  18. package/dist/index.js.map +2 -2
  19. package/dist/index.ssr.js +56 -56
  20. package/dist/index.ssr.js.map +2 -2
  21. package/package.json +10 -8
  22. package/src/Autocomplete.tsx +174 -0
  23. package/src/Breadcrumbs.tsx +264 -0
  24. package/src/Button.tsx +238 -0
  25. package/src/Calendar.tsx +471 -0
  26. package/src/Checkbox.tsx +387 -0
  27. package/src/Color.tsx +1370 -0
  28. package/src/ComboBox.tsx +824 -0
  29. package/src/DateField.tsx +337 -0
  30. package/src/DatePicker.tsx +367 -0
  31. package/src/Dialog.tsx +262 -0
  32. package/src/Disclosure.tsx +439 -0
  33. package/src/GridList.tsx +511 -0
  34. package/src/Landmark.tsx +203 -0
  35. package/src/Link.tsx +201 -0
  36. package/src/ListBox.tsx +346 -0
  37. package/src/Menu.tsx +544 -0
  38. package/src/Meter.tsx +157 -0
  39. package/src/Modal.tsx +433 -0
  40. package/src/NumberField.tsx +542 -0
  41. package/src/Popover.tsx +540 -0
  42. package/src/ProgressBar.tsx +162 -0
  43. package/src/RadioGroup.tsx +356 -0
  44. package/src/RangeCalendar.tsx +462 -0
  45. package/src/SearchField.tsx +479 -0
  46. package/src/Select.tsx +734 -0
  47. package/src/Separator.tsx +130 -0
  48. package/src/Slider.tsx +500 -0
  49. package/src/Switch.tsx +213 -0
  50. package/src/Table.tsx +857 -0
  51. package/src/Tabs.tsx +552 -0
  52. package/src/TagGroup.tsx +421 -0
  53. package/src/TextField.tsx +271 -0
  54. package/src/TimeField.tsx +455 -0
  55. package/src/Toast.tsx +503 -0
  56. package/src/Toolbar.tsx +160 -0
  57. package/src/Tooltip.tsx +423 -0
  58. package/src/Tree.tsx +551 -0
  59. package/src/VisuallyHidden.tsx +60 -0
  60. package/src/contexts.ts +74 -0
  61. package/src/index.ts +620 -0
  62. package/src/utils.tsx +329 -0
  63. package/dist/index.jsx +0 -9056
  64. package/dist/index.jsx.map +0 -7
package/src/Tree.tsx ADDED
@@ -0,0 +1,551 @@
1
+ /**
2
+ * Tree component for solidaria-components
3
+ *
4
+ * A pre-wired headless tree that combines state + aria hooks.
5
+ * Based on react-aria-components/src/Tree.tsx
6
+ *
7
+ * Tree displays hierarchical data with expandable/collapsible nodes,
8
+ * supporting keyboard navigation and selection.
9
+ */
10
+
11
+ import {
12
+ type JSX,
13
+ createContext,
14
+ createMemo,
15
+ createSignal,
16
+ splitProps,
17
+ useContext,
18
+ For,
19
+ Show,
20
+ } from 'solid-js';
21
+ import {
22
+ createTree,
23
+ createTreeItem,
24
+ createTreeSelectionCheckbox,
25
+ createFocusRing,
26
+ createHover,
27
+ type AriaTreeProps,
28
+ } from '@proyecto-viviana/solidaria';
29
+ import {
30
+ createTreeState,
31
+ createTreeCollection,
32
+ type TreeState,
33
+ type TreeCollection,
34
+ type TreeNode,
35
+ type TreeItemData,
36
+ type Key,
37
+ } from '@proyecto-viviana/solid-stately';
38
+ import {
39
+ type RenderChildren,
40
+ type ClassNameOrFunction,
41
+ type StyleOrFunction,
42
+ type SlotProps,
43
+ useRenderProps,
44
+ filterDOMProps,
45
+ } from './utils';
46
+
47
+ // ============================================
48
+ // TYPES
49
+ // ============================================
50
+
51
+ export interface TreeRenderProps {
52
+ /** Whether the tree has focus. */
53
+ isFocused: boolean;
54
+ /** Whether the tree has keyboard focus. */
55
+ isFocusVisible: boolean;
56
+ /** Whether the tree is disabled. */
57
+ isDisabled: boolean;
58
+ /** Whether the tree is empty. */
59
+ isEmpty: boolean;
60
+ }
61
+
62
+ export interface TreeProps<T extends object> extends Omit<AriaTreeProps, 'children'>, SlotProps {
63
+ /** The hierarchical items to render in the tree. */
64
+ items: TreeItemData<T>[];
65
+ /** The selection mode. */
66
+ selectionMode?: 'none' | 'single' | 'multiple';
67
+ /** Keys of disabled items. */
68
+ disabledKeys?: Iterable<Key>;
69
+ /** Currently selected keys (controlled). */
70
+ selectedKeys?: 'all' | Iterable<Key>;
71
+ /** Default selected keys (uncontrolled). */
72
+ defaultSelectedKeys?: 'all' | Iterable<Key>;
73
+ /** Handler called when selection changes. */
74
+ onSelectionChange?: (keys: 'all' | Set<Key>) => void;
75
+ /** Currently expanded keys (controlled). */
76
+ expandedKeys?: Iterable<Key>;
77
+ /** Default expanded keys (uncontrolled). */
78
+ defaultExpandedKeys?: Iterable<Key>;
79
+ /** Handler called when expansion changes. */
80
+ onExpandedChange?: (keys: Set<Key>) => void;
81
+ /** The children of the component. A function provided to render each item. */
82
+ children: (item: TreeItemData<T>, state: TreeRenderItemState) => JSX.Element;
83
+ /** The CSS className for the element. */
84
+ class?: ClassNameOrFunction<TreeRenderProps>;
85
+ /** The inline style for the element. */
86
+ style?: StyleOrFunction<TreeRenderProps>;
87
+ /** A function to render when the tree is empty. */
88
+ renderEmptyState?: () => JSX.Element;
89
+ }
90
+
91
+ export interface TreeRenderItemState {
92
+ /** Whether the item is expanded. */
93
+ isExpanded: boolean;
94
+ /** Whether the item is expandable (has children). */
95
+ isExpandable: boolean;
96
+ /** The nesting level (0 = root). */
97
+ level: number;
98
+ }
99
+
100
+ export interface TreeItemRenderProps {
101
+ /** Whether the item is selected. */
102
+ isSelected: boolean;
103
+ /** Whether the item is focused. */
104
+ isFocused: boolean;
105
+ /** Whether the item has keyboard focus. */
106
+ isFocusVisible: boolean;
107
+ /** Whether the item is pressed. */
108
+ isPressed: boolean;
109
+ /** Whether the item is hovered. */
110
+ isHovered: boolean;
111
+ /** Whether the item is disabled. */
112
+ isDisabled: boolean;
113
+ /** Whether the item is expanded. */
114
+ isExpanded: boolean;
115
+ /** Whether the item is expandable (has children). */
116
+ isExpandable: boolean;
117
+ /** The nesting level (0 = root). */
118
+ level: number;
119
+ }
120
+
121
+ export interface TreeItemProps<T extends object> extends SlotProps {
122
+ /** The unique key for the item. */
123
+ id: Key;
124
+ /** The item value. */
125
+ item?: TreeItemData<T>;
126
+ /** The children of the item. A function may be provided to receive render props. */
127
+ children?: RenderChildren<TreeItemRenderProps>;
128
+ /** The CSS className for the element. */
129
+ class?: ClassNameOrFunction<TreeItemRenderProps>;
130
+ /** The inline style for the element. */
131
+ style?: StyleOrFunction<TreeItemRenderProps>;
132
+ /** The text value of the item (for accessibility). */
133
+ textValue?: string;
134
+ /** Handler called when the item is activated. */
135
+ onAction?: () => void;
136
+ }
137
+
138
+ export interface TreeExpandButtonProps {
139
+ /** CSS class name for the button. */
140
+ class?: string;
141
+ /** CSS style for the button. */
142
+ style?: JSX.CSSProperties;
143
+ /** Children to render inside the button. */
144
+ children?: JSX.Element | ((props: { isExpanded: boolean }) => JSX.Element);
145
+ }
146
+
147
+ // ============================================
148
+ // CONTEXT
149
+ // ============================================
150
+
151
+ interface TreeContextValue<T extends object> {
152
+ state: TreeState<T, TreeCollection<T>>;
153
+ collection: TreeCollection<T>;
154
+ isDisabled: boolean;
155
+ renderItem: (item: TreeItemData<T>, state: TreeRenderItemState) => JSX.Element;
156
+ }
157
+
158
+ interface TreeItemContextValue<T extends object> {
159
+ node: TreeNode<T>;
160
+ isExpanded: boolean;
161
+ isExpandable: boolean;
162
+ level: number;
163
+ }
164
+
165
+ export const TreeContext = createContext<TreeContextValue<object> | null>(null);
166
+ export const TreeStateContext = createContext<TreeState<object, TreeCollection<object>> | null>(null);
167
+ export const TreeItemContext = createContext<TreeItemContextValue<object> | null>(null);
168
+
169
+ // ============================================
170
+ // COMPONENTS
171
+ // ============================================
172
+
173
+ /**
174
+ * A tree displays hierarchical data with expandable/collapsible nodes,
175
+ * supporting keyboard navigation and selection.
176
+ */
177
+ export function Tree<T extends object>(props: TreeProps<T>): JSX.Element {
178
+ const [local, stateProps, ariaProps] = splitProps(
179
+ props,
180
+ ['class', 'style', 'slot', 'renderEmptyState'],
181
+ [
182
+ 'items',
183
+ 'disabledKeys',
184
+ 'selectionMode',
185
+ 'selectedKeys',
186
+ 'defaultSelectedKeys',
187
+ 'onSelectionChange',
188
+ 'expandedKeys',
189
+ 'defaultExpandedKeys',
190
+ 'onExpandedChange',
191
+ ]
192
+ );
193
+
194
+ // Create ref signal
195
+ const [ref, setRef] = createSignal<HTMLDivElement | null>(null);
196
+
197
+ // Create tree state
198
+ const state = createTreeState<T, TreeCollection<T>>(() => ({
199
+ collectionFactory: (expandedKeys) =>
200
+ createTreeCollection(stateProps.items, expandedKeys) as TreeCollection<T>,
201
+ disabledKeys: stateProps.disabledKeys,
202
+ selectionMode: stateProps.selectionMode,
203
+ selectedKeys: stateProps.selectedKeys,
204
+ defaultSelectedKeys: stateProps.defaultSelectedKeys,
205
+ onSelectionChange: stateProps.onSelectionChange,
206
+ expandedKeys: stateProps.expandedKeys,
207
+ defaultExpandedKeys: stateProps.defaultExpandedKeys,
208
+ onExpandedChange: stateProps.onExpandedChange,
209
+ }));
210
+
211
+ // Create tree aria props
212
+ const { treeProps } = createTree<T, TreeCollection<T>>(
213
+ () => ({
214
+ id: ariaProps.id,
215
+ 'aria-label': ariaProps['aria-label'],
216
+ 'aria-labelledby': ariaProps['aria-labelledby'],
217
+ 'aria-describedby': ariaProps['aria-describedby'],
218
+ isVirtualized: ariaProps.isVirtualized,
219
+ onAction: ariaProps.onAction,
220
+ isDisabled: ariaProps.isDisabled,
221
+ }),
222
+ () => state,
223
+ ref
224
+ );
225
+
226
+ // Create focus ring
227
+ const { isFocused, isFocusVisible, focusProps } = createFocusRing();
228
+
229
+ // Render props values
230
+ const renderValues = createMemo<TreeRenderProps>(() => ({
231
+ isFocused: state.isFocused || isFocused(),
232
+ isFocusVisible: isFocusVisible(),
233
+ isDisabled: ariaProps.isDisabled ?? false,
234
+ isEmpty: stateProps.items.length === 0,
235
+ }));
236
+
237
+ // Resolve render props
238
+ const renderProps = useRenderProps(
239
+ {
240
+ class: local.class,
241
+ style: local.style,
242
+ defaultClassName: 'solidaria-Tree',
243
+ },
244
+ renderValues
245
+ );
246
+
247
+ // Filter DOM props
248
+ const domProps = createMemo(() => {
249
+ const filtered = filterDOMProps(ariaProps as Record<string, unknown>, { global: true });
250
+ return filtered;
251
+ });
252
+
253
+ // Remove ref from spread props
254
+ const cleanTreeProps = () => {
255
+ const { ref: _ref1, ...rest } = treeProps as Record<string, unknown>;
256
+ return rest;
257
+ };
258
+ const cleanFocusProps = () => {
259
+ const { ref: _ref2, ...rest } = focusProps as Record<string, unknown>;
260
+ return rest;
261
+ };
262
+
263
+ const isEmpty = () => stateProps.items.length === 0;
264
+
265
+ const contextValue = createMemo<TreeContextValue<T>>(() => ({
266
+ state,
267
+ collection: state.collection,
268
+ isDisabled: ariaProps.isDisabled ?? false,
269
+ renderItem: props.children,
270
+ }));
271
+
272
+ // Render visible rows (flat list based on expansion state)
273
+ const visibleRows = createMemo(() => state.collection.rows);
274
+
275
+ return (
276
+ <TreeContext.Provider value={contextValue() as unknown as TreeContextValue<object>}>
277
+ <TreeStateContext.Provider value={state as unknown as TreeState<object, TreeCollection<object>>}>
278
+ <div
279
+ ref={setRef}
280
+ {...domProps()}
281
+ {...cleanTreeProps()}
282
+ {...cleanFocusProps()}
283
+ class={renderProps.class()}
284
+ style={renderProps.style()}
285
+ data-focused={state.isFocused || undefined}
286
+ data-focus-visible={isFocusVisible() || undefined}
287
+ data-disabled={ariaProps.isDisabled || undefined}
288
+ data-empty={isEmpty() || undefined}
289
+ >
290
+ {isEmpty() && local.renderEmptyState ? (
291
+ local.renderEmptyState()
292
+ ) : (
293
+ <For each={visibleRows()}>
294
+ {(node) => {
295
+ // Find the original item data to pass to render function
296
+ const itemData: TreeItemData<T> = {
297
+ key: node.key,
298
+ value: node.value as T,
299
+ textValue: node.textValue,
300
+ children: node.hasChildNodes
301
+ ? node.childNodes.map((child) => ({
302
+ key: child.key,
303
+ value: child.value as T,
304
+ textValue: child.textValue,
305
+ }))
306
+ : undefined,
307
+ };
308
+ const itemState: TreeRenderItemState = {
309
+ isExpanded: node.isExpanded ?? false,
310
+ isExpandable: node.isExpandable ?? false,
311
+ level: node.level,
312
+ };
313
+ return props.children(itemData, itemState);
314
+ }}
315
+ </For>
316
+ )}
317
+ </div>
318
+ </TreeStateContext.Provider>
319
+ </TreeContext.Provider>
320
+ );
321
+ }
322
+
323
+ /**
324
+ * An item in a tree.
325
+ */
326
+ export function TreeItem<T extends object>(props: TreeItemProps<T>): JSX.Element {
327
+ const [local] = splitProps(props, [
328
+ 'class',
329
+ 'style',
330
+ 'slot',
331
+ 'id',
332
+ 'item',
333
+ 'textValue',
334
+ 'onAction',
335
+ ]);
336
+
337
+ // Get state from context
338
+ const context = useContext(TreeStateContext);
339
+ if (!context) {
340
+ throw new Error('TreeItem must be used within a Tree');
341
+ }
342
+ const state = context as TreeState<T, TreeCollection<T>>;
343
+
344
+ // Create ref signal
345
+ const [ref, setRef] = createSignal<HTMLDivElement | null>(null);
346
+
347
+ // Find the item node
348
+ const itemNode = createMemo(() => {
349
+ const node = state.collection.getItem(local.id);
350
+ if (!node) {
351
+ // Create a simple node for the item
352
+ return {
353
+ type: 'item' as const,
354
+ key: local.id,
355
+ value: local.item?.value ?? null,
356
+ textValue: local.textValue ?? String(local.id),
357
+ level: 0,
358
+ index: 0,
359
+ hasChildNodes: false,
360
+ childNodes: [],
361
+ isExpandable: false,
362
+ isExpanded: false,
363
+ } as TreeNode<T>;
364
+ }
365
+ return node;
366
+ });
367
+
368
+ // Create item aria props
369
+ const {
370
+ rowProps,
371
+ gridCellProps,
372
+ expandButtonProps: _expandButtonProps,
373
+ isSelected,
374
+ isDisabled,
375
+ isPressed,
376
+ isExpanded,
377
+ isExpandable,
378
+ level,
379
+ } = createTreeItem<T, TreeCollection<T>>(
380
+ () => ({
381
+ node: itemNode(),
382
+ onAction: local.onAction,
383
+ }),
384
+ () => state,
385
+ ref
386
+ );
387
+
388
+ // Create hover
389
+ const { isHovered, hoverProps } = createHover({
390
+ get isDisabled() {
391
+ return isDisabled;
392
+ },
393
+ });
394
+
395
+ // Create focus ring
396
+ const { isFocusVisible, focusProps } = createFocusRing();
397
+
398
+ // Check if focused
399
+ const isFocused = createMemo(() => state.focusedKey === local.id);
400
+
401
+ // Render props values
402
+ const renderValues = createMemo<TreeItemRenderProps>(() => ({
403
+ isSelected,
404
+ isFocused: isFocused(),
405
+ isFocusVisible: isFocusVisible() && isFocused(),
406
+ isPressed,
407
+ isHovered: isHovered(),
408
+ isDisabled,
409
+ isExpanded,
410
+ isExpandable,
411
+ level,
412
+ }));
413
+
414
+ // Resolve render props
415
+ const renderProps = useRenderProps(
416
+ {
417
+ children: props.children,
418
+ class: local.class,
419
+ style: local.style,
420
+ defaultClassName: 'solidaria-Tree-item',
421
+ },
422
+ renderValues
423
+ );
424
+
425
+ // Remove ref from spread props
426
+ const cleanRowProps = () => {
427
+ const { ref: _ref1, ...rest } = rowProps as Record<string, unknown>;
428
+ return rest;
429
+ };
430
+ const cleanHoverProps = () => {
431
+ const { ref: _ref2, ...rest } = hoverProps as Record<string, unknown>;
432
+ return rest;
433
+ };
434
+ const cleanFocusProps = () => {
435
+ const { ref: _ref3, ...rest } = focusProps as Record<string, unknown>;
436
+ return rest;
437
+ };
438
+
439
+ // Item context for nested components
440
+ const itemContextValue = createMemo<TreeItemContextValue<T>>(() => ({
441
+ node: itemNode(),
442
+ isExpanded,
443
+ isExpandable,
444
+ level,
445
+ }));
446
+
447
+ return (
448
+ <TreeItemContext.Provider value={itemContextValue() as TreeItemContextValue<object>}>
449
+ <div
450
+ ref={setRef}
451
+ {...cleanRowProps()}
452
+ {...cleanHoverProps()}
453
+ {...cleanFocusProps()}
454
+ class={renderProps.class()}
455
+ style={renderProps.style()}
456
+ data-selected={isSelected || undefined}
457
+ data-focused={isFocused() || undefined}
458
+ data-focus-visible={(isFocusVisible() && isFocused()) || undefined}
459
+ data-pressed={isPressed || undefined}
460
+ data-hovered={isHovered() || undefined}
461
+ data-disabled={isDisabled || undefined}
462
+ data-expanded={isExpanded || undefined}
463
+ data-expandable={isExpandable || undefined}
464
+ data-level={level}
465
+ >
466
+ <div {...gridCellProps} class="solidaria-Tree-item-content">
467
+ {renderProps.renderChildren()}
468
+ </div>
469
+ </div>
470
+ </TreeItemContext.Provider>
471
+ );
472
+ }
473
+
474
+ /**
475
+ * A button to expand/collapse a tree item.
476
+ */
477
+ export function TreeExpandButton(props: TreeExpandButtonProps): JSX.Element {
478
+ // Get item context
479
+ const itemContext = useContext(TreeItemContext);
480
+ if (!itemContext) {
481
+ throw new Error('TreeExpandButton must be used within a Tree');
482
+ }
483
+
484
+ // Get state context
485
+ const stateContext = useContext(TreeStateContext);
486
+ if (!stateContext) {
487
+ throw new Error('TreeExpandButton must be used within a Tree');
488
+ }
489
+
490
+ const state = stateContext as TreeState<object, TreeCollection<object>>;
491
+
492
+ // Create expand button props
493
+ const { expandButtonProps } = createTreeItem(
494
+ () => ({ node: itemContext.node }),
495
+ () => state,
496
+ () => null
497
+ );
498
+
499
+ // Remove ref and add custom handling
500
+ const cleanExpandProps = () => {
501
+ const { ref: _ref, ...rest } = expandButtonProps as Record<string, unknown>;
502
+ return rest;
503
+ };
504
+
505
+ const isExpanded = createMemo(() => state.isExpanded(itemContext.node.key));
506
+
507
+ // Render children
508
+ const renderChildren = () => {
509
+ if (typeof props.children === 'function') {
510
+ return props.children({ isExpanded: isExpanded() });
511
+ }
512
+ return props.children;
513
+ };
514
+
515
+ return (
516
+ <Show when={itemContext.isExpandable}>
517
+ <button
518
+ {...cleanExpandProps()}
519
+ class={props.class ?? 'solidaria-Tree-expand-button'}
520
+ style={props.style}
521
+ data-expanded={isExpanded() || undefined}
522
+ >
523
+ {renderChildren()}
524
+ </button>
525
+ </Show>
526
+ );
527
+ }
528
+
529
+ /**
530
+ * A checkbox for item selection in a tree.
531
+ */
532
+ export function TreeSelectionCheckbox(props: { itemKey: Key }): JSX.Element {
533
+ const context = useContext(TreeStateContext);
534
+ if (!context) {
535
+ throw new Error('TreeSelectionCheckbox must be used within a Tree');
536
+ }
537
+
538
+ const state = context as TreeState<object, TreeCollection<object>>;
539
+
540
+ const { checkboxProps } = createTreeSelectionCheckbox<object, TreeCollection<object>>(
541
+ () => ({ key: props.itemKey }),
542
+ () => state
543
+ );
544
+
545
+ return <input {...checkboxProps} class="solidaria-Tree-checkbox" />;
546
+ }
547
+
548
+ // Attach static properties
549
+ Tree.Item = TreeItem;
550
+ Tree.ExpandButton = TreeExpandButton;
551
+ Tree.SelectionCheckbox = TreeSelectionCheckbox;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * VisuallyHidden component for solidaria-components
3
+ *
4
+ * Hides content visually but keeps it accessible to screen readers.
5
+ * Port of react-aria's VisuallyHidden.
6
+ */
7
+
8
+ import { type JSX, type ParentProps, splitProps } from 'solid-js';
9
+ import { Dynamic } from 'solid-js/web';
10
+
11
+ // ============================================
12
+ // TYPES
13
+ // ============================================
14
+
15
+ export interface VisuallyHiddenProps extends ParentProps {
16
+ /** The element type to render. @default 'span' */
17
+ elementType?: keyof JSX.IntrinsicElements;
18
+ /** Whether the element should be focusable when focused. */
19
+ isFocusable?: boolean;
20
+ }
21
+
22
+ // ============================================
23
+ // STYLES
24
+ // ============================================
25
+
26
+ const visuallyHiddenStyles: JSX.CSSProperties = {
27
+ border: '0',
28
+ clip: 'rect(0 0 0 0)',
29
+ 'clip-path': 'inset(50%)',
30
+ height: '1px',
31
+ margin: '-1px',
32
+ overflow: 'hidden',
33
+ padding: '0',
34
+ position: 'absolute',
35
+ width: '1px',
36
+ 'white-space': 'nowrap',
37
+ };
38
+
39
+ // ============================================
40
+ // COMPONENT
41
+ // ============================================
42
+
43
+ /**
44
+ * VisuallyHidden hides its children visually, while keeping content visible to screen readers.
45
+ */
46
+ export function VisuallyHidden(props: VisuallyHiddenProps): JSX.Element {
47
+ const [local, others] = splitProps(props, ['elementType', 'isFocusable']);
48
+
49
+ const elementType = () => local.elementType ?? 'span';
50
+
51
+ return (
52
+ <Dynamic
53
+ component={elementType()}
54
+ style={visuallyHiddenStyles}
55
+ {...others}
56
+ >
57
+ {props.children}
58
+ </Dynamic>
59
+ );
60
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Shared contexts for overlay components.
3
+ *
4
+ * These are separated to avoid circular dependencies between
5
+ * Dialog, Modal, Popover, and Button components.
6
+ */
7
+
8
+ import { createContext, useContext } from 'solid-js'
9
+ import type { OverlayTriggerState as StatelyOverlayTriggerState } from '@proyecto-viviana/solid-stately'
10
+
11
+ // ============================================
12
+ // OVERLAY TRIGGER STATE CONTEXT
13
+ // ============================================
14
+
15
+ export interface OverlayTriggerState {
16
+ isOpen: boolean
17
+ open: () => void
18
+ close: () => void
19
+ toggle: () => void
20
+ }
21
+
22
+ export const OverlayTriggerStateContext = createContext<OverlayTriggerState | null>(null)
23
+
24
+ /**
25
+ * Hook to access the overlay trigger state from context.
26
+ */
27
+ export function useOverlayTriggerState(): OverlayTriggerState | null {
28
+ return useContext(OverlayTriggerStateContext)
29
+ }
30
+
31
+ // ============================================
32
+ // DIALOG TRIGGER CONTEXT
33
+ // ============================================
34
+
35
+ export interface DialogTriggerContextValue {
36
+ state: StatelyOverlayTriggerState
37
+ triggerRef: () => HTMLElement | null
38
+ triggerId: string
39
+ }
40
+
41
+ export const DialogTriggerContext = createContext<DialogTriggerContextValue | null>(null)
42
+
43
+ /**
44
+ * Hook to access the dialog trigger state from context.
45
+ */
46
+ export function useDialogTrigger(): DialogTriggerContextValue | null {
47
+ return useContext(DialogTriggerContext)
48
+ }
49
+
50
+ // ============================================
51
+ // POPOVER TRIGGER CONTEXT
52
+ // ============================================
53
+
54
+ export interface PopoverTriggerContextValue {
55
+ state: {
56
+ isOpen: () => boolean
57
+ open: () => void
58
+ close: () => void
59
+ toggle: () => void
60
+ }
61
+ triggerRef: () => HTMLElement | null
62
+ setTriggerRef: (el: HTMLElement | null) => void
63
+ triggerId: string
64
+ trigger: string
65
+ }
66
+
67
+ export const PopoverTriggerContext = createContext<PopoverTriggerContextValue | null>(null)
68
+
69
+ /**
70
+ * Hook to access the popover trigger state from context.
71
+ */
72
+ export function usePopoverTrigger(): PopoverTriggerContextValue | null {
73
+ return useContext(PopoverTriggerContext)
74
+ }