@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/Dialog.tsx ADDED
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Dialog component for solidaria-components
3
+ *
4
+ * A headless dialog component that combines ARIA hooks.
5
+ * Port of react-aria-components Dialog.
6
+ */
7
+
8
+ import {
9
+ type JSX,
10
+ createContext,
11
+ createMemo,
12
+ createUniqueId,
13
+ splitProps,
14
+ useContext,
15
+ Switch,
16
+ Match,
17
+ } from 'solid-js'
18
+ import {
19
+ createDialog,
20
+ createOverlayTrigger,
21
+ type AriaDialogProps,
22
+ } from '@proyecto-viviana/solidaria'
23
+ import { createOverlayTriggerState } from '@proyecto-viviana/solid-stately'
24
+ import { DialogTriggerContext, useOverlayTriggerState } from './contexts'
25
+ import {
26
+ type RenderChildren,
27
+ type ClassNameOrFunction,
28
+ type StyleOrFunction,
29
+ type SlotProps,
30
+ useRenderProps,
31
+ filterDOMProps,
32
+ } from './utils'
33
+
34
+ // ============================================
35
+ // TYPES
36
+ // ============================================
37
+
38
+ export interface DialogRenderProps {
39
+ /** Function to close the dialog */
40
+ close: () => void
41
+ }
42
+
43
+ export interface DialogProps extends Omit<AriaDialogProps, 'class' | 'style'>, SlotProps {
44
+ /** The children of the component - can be JSX or render function. */
45
+ children?: RenderChildren<DialogRenderProps>
46
+ /** The CSS className for the element. */
47
+ class?: ClassNameOrFunction<DialogRenderProps>
48
+ /** The inline style for the element. */
49
+ style?: StyleOrFunction<DialogRenderProps>
50
+ /** Callback when dialog should close */
51
+ onClose?: () => void
52
+ }
53
+
54
+ export interface DialogTriggerProps {
55
+ /** The children - should include a trigger and modal/popover content. */
56
+ children: JSX.Element
57
+ /** Whether the dialog is open (controlled). */
58
+ isOpen?: boolean
59
+ /** Whether the dialog is open by default (uncontrolled). */
60
+ defaultOpen?: boolean
61
+ /** Callback when open state changes. */
62
+ onOpenChange?: (isOpen: boolean) => void
63
+ }
64
+
65
+ // ============================================
66
+ // CONTEXTS
67
+ // ============================================
68
+
69
+ interface DialogContextValue {
70
+ close: () => void
71
+ titleId?: string
72
+ }
73
+
74
+ export const DialogContext = createContext<DialogContextValue | null>(null)
75
+
76
+ // Re-export DialogTriggerContext from shared contexts (also imported above for local use)
77
+ export { DialogTriggerContext, useDialogTrigger } from './contexts'
78
+
79
+ // ============================================
80
+ // DIALOG TRIGGER COMPONENT
81
+ // ============================================
82
+
83
+ /**
84
+ * A DialogTrigger opens a dialog when a trigger element is pressed.
85
+ * Children should include a trigger element (e.g. Button) and the dialog content.
86
+ */
87
+ export function DialogTrigger(props: DialogTriggerProps): JSX.Element {
88
+ const [local] = splitProps(props, ['isOpen', 'defaultOpen', 'onOpenChange'])
89
+
90
+ // Create overlay trigger state
91
+ const state = createOverlayTriggerState({
92
+ get isOpen() {
93
+ return local.isOpen
94
+ },
95
+ get defaultOpen() {
96
+ return local.defaultOpen
97
+ },
98
+ onOpenChange: local.onOpenChange,
99
+ })
100
+
101
+ // Ref for the trigger element
102
+ let triggerRef: HTMLElement | null = null
103
+ const triggerId = createUniqueId()
104
+
105
+ // Create overlay trigger props (used via context, not directly applied)
106
+ createOverlayTrigger(
107
+ { type: 'dialog' },
108
+ state,
109
+ () => triggerRef
110
+ )
111
+
112
+ // Context value - memoized to avoid unnecessary re-renders
113
+ const contextValue = createMemo(() => ({
114
+ state,
115
+ triggerRef: () => triggerRef,
116
+ triggerId,
117
+ }))
118
+
119
+ // In SolidJS, we simply render children directly within the provider
120
+ // The children will have access to the context
121
+ return (
122
+ <DialogTriggerContext.Provider value={contextValue()}>
123
+ {props.children}
124
+ </DialogTriggerContext.Provider>
125
+ )
126
+ }
127
+
128
+ // ============================================
129
+ // DIALOG COMPONENT
130
+ // ============================================
131
+
132
+ /**
133
+ * A dialog is an overlay shown above other content in an application.
134
+ */
135
+ export function Dialog(props: DialogProps): JSX.Element {
136
+ const [local, ariaProps, rest] = splitProps(
137
+ props,
138
+ ['class', 'style', 'slot', 'onClose'],
139
+ ['role', 'aria-label', 'aria-labelledby', 'aria-describedby']
140
+ )
141
+
142
+ let dialogRef!: HTMLDivElement
143
+
144
+ // Get trigger context for aria-labelledby fallback
145
+ const triggerContext = useContext(DialogTriggerContext)
146
+
147
+ // createDialog returns dialogProps AND titleProps (with the id for the Heading)
148
+ const { dialogProps, titleProps } = createDialog(
149
+ {
150
+ get role() {
151
+ return ariaProps.role
152
+ },
153
+ get 'aria-label'() {
154
+ return ariaProps['aria-label']
155
+ },
156
+ get 'aria-labelledby'() {
157
+ // Use provided labelledby, or fall back to trigger id if no title
158
+ return ariaProps['aria-labelledby'] ?? triggerContext?.triggerId
159
+ },
160
+ get 'aria-describedby'() {
161
+ return ariaProps['aria-describedby']
162
+ },
163
+ },
164
+ () => dialogRef
165
+ )
166
+
167
+ // Get titleId from titleProps - this links Dialog's aria-labelledby to Heading's id
168
+ const titleId = () => titleProps()?.id
169
+
170
+ // Get close function from OverlayTriggerState context or onClose prop
171
+ const overlayState = useOverlayTriggerState()
172
+
173
+ const close = () => {
174
+ local.onClose?.()
175
+ overlayState?.close()
176
+ triggerContext?.state.close()
177
+ }
178
+
179
+ // Render props values
180
+ const renderValues = createMemo<DialogRenderProps>(() => ({
181
+ close,
182
+ }))
183
+
184
+ // Resolve render props
185
+ const renderProps = useRenderProps(
186
+ {
187
+ children: props.children,
188
+ class: local.class,
189
+ style: local.style,
190
+ defaultClassName: 'solidaria-Dialog',
191
+ },
192
+ renderValues
193
+ )
194
+
195
+ // Filter DOM props
196
+ const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }))
197
+
198
+ return (
199
+ <DialogContext.Provider value={{ close, titleId: titleId() }}>
200
+ <div
201
+ {...dialogProps()}
202
+ {...domProps()}
203
+ ref={dialogRef}
204
+ class={renderProps.class()}
205
+ style={renderProps.style()}
206
+ >
207
+ {renderProps.renderChildren()}
208
+ </div>
209
+ </DialogContext.Provider>
210
+ )
211
+ }
212
+
213
+ // ============================================
214
+ // HEADING COMPONENT
215
+ // ============================================
216
+
217
+ export interface HeadingProps {
218
+ /** The children of the heading. */
219
+ children: JSX.Element
220
+ /** The CSS className. */
221
+ class?: string
222
+ /** The heading level (1-6). Defaults to 2. */
223
+ level?: 1 | 2 | 3 | 4 | 5 | 6
224
+ /** The slot to render into. */
225
+ slot?: string
226
+ }
227
+
228
+ /**
229
+ * Heading element for dialog title.
230
+ * When rendered inside a Dialog, automatically gets the titleProps.
231
+ */
232
+ export function Heading(props: HeadingProps): JSX.Element {
233
+ const dialogContext = useContext(DialogContext)
234
+ const level = () => props.level ?? 2
235
+ const id = () => dialogContext?.titleId
236
+
237
+ return (
238
+ <Switch>
239
+ <Match when={level() === 1}>
240
+ <h1 id={id()} class={props.class}>{props.children}</h1>
241
+ </Match>
242
+ <Match when={level() === 2}>
243
+ <h2 id={id()} class={props.class}>{props.children}</h2>
244
+ </Match>
245
+ <Match when={level() === 3}>
246
+ <h3 id={id()} class={props.class}>{props.children}</h3>
247
+ </Match>
248
+ <Match when={level() === 4}>
249
+ <h4 id={id()} class={props.class}>{props.children}</h4>
250
+ </Match>
251
+ <Match when={level() === 5}>
252
+ <h5 id={id()} class={props.class}>{props.children}</h5>
253
+ </Match>
254
+ <Match when={level() === 6}>
255
+ <h6 id={id()} class={props.class}>{props.children}</h6>
256
+ </Match>
257
+ </Switch>
258
+ )
259
+ }
260
+
261
+ // Keep backward compatibility
262
+ export { Heading as DialogHeading }
@@ -0,0 +1,439 @@
1
+ /**
2
+ * Disclosure and Accordion components for solidaria-components
3
+ *
4
+ * Disclosure is a widget that can be toggled to show or hide content.
5
+ * Accordion (DisclosureGroup) manages multiple disclosures with optional single-expand.
6
+ *
7
+ * Port of react-aria-components Disclosure.
8
+ */
9
+
10
+ import {
11
+ type JSX,
12
+ createContext,
13
+ createMemo,
14
+ createSignal,
15
+ splitProps,
16
+ useContext,
17
+ } from 'solid-js';
18
+ import {
19
+ createDisclosureState,
20
+ createDisclosureGroupState,
21
+ type DisclosureState,
22
+ type DisclosureGroupState,
23
+ type DisclosureStateProps,
24
+ type DisclosureGroupStateProps,
25
+ } from '@proyecto-viviana/solid-stately';
26
+ import {
27
+ createDisclosure,
28
+ createDisclosureGroup,
29
+ } from '@proyecto-viviana/solidaria';
30
+ import {
31
+ type RenderChildren,
32
+ type ClassNameOrFunction,
33
+ type StyleOrFunction,
34
+ useRenderProps,
35
+ filterDOMProps,
36
+ dataAttr,
37
+ } from './utils';
38
+
39
+ // ============================================
40
+ // TYPES
41
+ // ============================================
42
+
43
+ export interface DisclosureRenderProps {
44
+ /** Whether the disclosure is expanded. */
45
+ isExpanded: boolean;
46
+ /** Whether the disclosure is disabled. */
47
+ isDisabled: boolean;
48
+ }
49
+
50
+ export interface DisclosureGroupRenderProps {
51
+ /** Whether all items are disabled. */
52
+ isDisabled: boolean;
53
+ }
54
+
55
+ export interface DisclosureProps extends DisclosureStateProps {
56
+ /** The children of the component. */
57
+ children?: JSX.Element;
58
+ /** The CSS className for the element. */
59
+ class?: ClassNameOrFunction<DisclosureRenderProps>;
60
+ /** The inline style for the element. */
61
+ style?: StyleOrFunction<DisclosureRenderProps>;
62
+ /** Whether the disclosure is disabled. */
63
+ isDisabled?: boolean;
64
+ /** A unique identifier for the disclosure (used in groups). */
65
+ id?: string;
66
+ }
67
+
68
+ export interface DisclosureGroupProps extends DisclosureGroupStateProps {
69
+ /** The children of the component. */
70
+ children?: JSX.Element;
71
+ /** The CSS className for the element. */
72
+ class?: ClassNameOrFunction<DisclosureGroupRenderProps>;
73
+ /** The inline style for the element. */
74
+ style?: StyleOrFunction<DisclosureGroupRenderProps>;
75
+ }
76
+
77
+ export interface DisclosureTriggerProps {
78
+ /** The children of the trigger. */
79
+ children?: JSX.Element;
80
+ /** The CSS className for the element. */
81
+ class?: string;
82
+ /** The inline style for the element. */
83
+ style?: JSX.CSSProperties;
84
+ }
85
+
86
+ export interface DisclosurePanelProps {
87
+ /** The children of the panel. */
88
+ children?: RenderChildren<DisclosureRenderProps>;
89
+ /** The CSS className for the element. */
90
+ class?: ClassNameOrFunction<DisclosureRenderProps>;
91
+ /** The inline style for the element. */
92
+ style?: StyleOrFunction<DisclosureRenderProps>;
93
+ }
94
+
95
+ // ============================================
96
+ // CONTEXT
97
+ // ============================================
98
+
99
+ interface DisclosureContextValue {
100
+ state: DisclosureState;
101
+ isDisabled: () => boolean;
102
+ /** The disclosure ARIA result object - access .buttonProps and .panelProps as getters */
103
+ disclosureAria: {
104
+ readonly buttonProps: JSX.ButtonHTMLAttributes<HTMLButtonElement>;
105
+ readonly panelProps: JSX.HTMLAttributes<HTMLElement>;
106
+ };
107
+ }
108
+
109
+ export const DisclosureContext = createContext<DisclosureContextValue | null>(null);
110
+
111
+ export function useDisclosureContext(): DisclosureContextValue | null {
112
+ return useContext(DisclosureContext);
113
+ }
114
+
115
+ interface DisclosureGroupContextValue {
116
+ state: DisclosureGroupState;
117
+ }
118
+
119
+ export const DisclosureGroupContext = createContext<DisclosureGroupContextValue | null>(null);
120
+
121
+ export function useDisclosureGroupContext(): DisclosureGroupContextValue | null {
122
+ return useContext(DisclosureGroupContext);
123
+ }
124
+
125
+ // ============================================
126
+ // DISCLOSURE GROUP (Accordion)
127
+ // ============================================
128
+
129
+ /**
130
+ * DisclosureGroup manages a group of Disclosure components.
131
+ * Use this to create an accordion where only one item can be expanded at a time.
132
+ *
133
+ * @example
134
+ * ```tsx
135
+ * <DisclosureGroup>
136
+ * <Disclosure id="item1">
137
+ * <DisclosureTrigger>Item 1</DisclosureTrigger>
138
+ * <DisclosurePanel>Content 1</DisclosurePanel>
139
+ * </Disclosure>
140
+ * <Disclosure id="item2">
141
+ * <DisclosureTrigger>Item 2</DisclosureTrigger>
142
+ * <DisclosurePanel>Content 2</DisclosurePanel>
143
+ * </Disclosure>
144
+ * </DisclosureGroup>
145
+ * ```
146
+ */
147
+ export function DisclosureGroup(props: DisclosureGroupProps): JSX.Element {
148
+ // IMPORTANT: Don't destructure or access props.children early!
149
+ // In SolidJS, children are lazily evaluated. Accessing them before
150
+ // the context provider renders causes them to evaluate outside the context.
151
+ // See: https://github.com/solidjs/solid/issues/182
152
+ const [local, rest] = splitProps(props, [
153
+ 'class',
154
+ 'style',
155
+ 'allowsMultipleExpanded',
156
+ 'isDisabled',
157
+ 'expandedKeys',
158
+ 'defaultExpandedKeys',
159
+ 'onExpandedChange',
160
+ ]);
161
+
162
+ // Create group state
163
+ const state = createDisclosureGroupState({
164
+ allowsMultipleExpanded: local.allowsMultipleExpanded,
165
+ isDisabled: local.isDisabled,
166
+ expandedKeys: local.expandedKeys,
167
+ defaultExpandedKeys: local.defaultExpandedKeys,
168
+ onExpandedChange: local.onExpandedChange,
169
+ });
170
+
171
+ // Create group accessibility props
172
+ const { groupProps } = createDisclosureGroup(
173
+ { isDisabled: local.isDisabled },
174
+ state
175
+ );
176
+
177
+ // Render props values
178
+ const renderValues = createMemo<DisclosureGroupRenderProps>(() => ({
179
+ isDisabled: state.isDisabled,
180
+ }));
181
+
182
+ // Resolve render props - don't pass children, we'll render props.children directly
183
+ const renderProps = useRenderProps(
184
+ {
185
+ class: local.class,
186
+ style: local.style,
187
+ defaultClassName: 'solidaria-DisclosureGroup',
188
+ },
189
+ renderValues
190
+ );
191
+
192
+ // Filter DOM props
193
+ const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }));
194
+
195
+ // Context value
196
+ const contextValue: DisclosureGroupContextValue = { state };
197
+
198
+ // Extract ref from groupProps to avoid type conflicts
199
+ const { ref: _ref, ...cleanGroupProps } = groupProps as Record<string, unknown>;
200
+
201
+ return (
202
+ <DisclosureGroupContext.Provider value={contextValue}>
203
+ <div
204
+ {...domProps()}
205
+ {...cleanGroupProps}
206
+ class={renderProps.class()}
207
+ style={renderProps.style()}
208
+ data-disabled={dataAttr(state.isDisabled)}
209
+ >
210
+ {props.children}
211
+ </div>
212
+ </DisclosureGroupContext.Provider>
213
+ );
214
+ }
215
+
216
+ // ============================================
217
+ // DISCLOSURE
218
+ // ============================================
219
+
220
+ /**
221
+ * Disclosure is a widget that can be toggled to show or hide content.
222
+ *
223
+ * @example
224
+ * ```tsx
225
+ * <Disclosure>
226
+ * <DisclosureTrigger>Show more</DisclosureTrigger>
227
+ * <DisclosurePanel>Hidden content here...</DisclosurePanel>
228
+ * </Disclosure>
229
+ * ```
230
+ */
231
+ export function Disclosure(props: DisclosureProps): JSX.Element {
232
+ // IMPORTANT: Don't destructure or access props.children early!
233
+ // In SolidJS, children are lazily evaluated. Accessing them before
234
+ // the context provider renders causes them to evaluate outside the context.
235
+ // See: https://github.com/solidjs/solid/issues/182
236
+ const [local, rest] = splitProps(props, [
237
+ 'class',
238
+ 'style',
239
+ 'isDisabled',
240
+ 'isExpanded',
241
+ 'defaultExpanded',
242
+ 'onExpandedChange',
243
+ 'id',
244
+ ]);
245
+
246
+ // Check if we're inside a DisclosureGroup
247
+ const groupContext = useDisclosureGroupContext();
248
+
249
+ // Create disclosure state
250
+ // If in a group, sync with group state
251
+ const state = createDisclosureState(() => {
252
+ const id = local.id;
253
+ if (groupContext && id) {
254
+ return {
255
+ isExpanded: groupContext.state.isExpanded(id),
256
+ onExpandedChange: (expanded: boolean) => {
257
+ if (expanded !== groupContext.state.isExpanded(id)) {
258
+ groupContext.state.toggleKey(id);
259
+ }
260
+ local.onExpandedChange?.(expanded);
261
+ },
262
+ };
263
+ }
264
+ return {
265
+ isExpanded: local.isExpanded,
266
+ defaultExpanded: local.defaultExpanded,
267
+ onExpandedChange: local.onExpandedChange,
268
+ };
269
+ });
270
+
271
+ // Panel ref as a signal so the createEffect in createDisclosure can track it
272
+ const [panelRef, setPanelRefSignal] = createSignal<HTMLElement | null>(null);
273
+
274
+ // Determine if disabled (used in multiple places)
275
+ const isDisabled = () => local.isDisabled || groupContext?.state.isDisabled || false;
276
+
277
+ // Create disclosure accessibility props
278
+ // Pass props as accessor function for reactivity
279
+ // IMPORTANT: Don't destructure! The getters must be called fresh each render
280
+ const disclosureAria = createDisclosure(
281
+ () => ({ isDisabled: isDisabled() }),
282
+ state,
283
+ panelRef // Pass the accessor directly
284
+ );
285
+
286
+ // Render props values
287
+ const renderValues = createMemo<DisclosureRenderProps>(() => ({
288
+ isExpanded: state.isExpanded(),
289
+ isDisabled: isDisabled(),
290
+ }));
291
+
292
+ // Resolve render props - don't pass children, we'll render props.children directly
293
+ const renderProps = useRenderProps(
294
+ {
295
+ class: local.class,
296
+ style: local.style,
297
+ defaultClassName: 'solidaria-Disclosure',
298
+ },
299
+ renderValues
300
+ );
301
+
302
+ // Filter DOM props
303
+ const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }));
304
+
305
+ // Context value - pass the disclosureAria object with getters intact
306
+ const contextValue: DisclosureContextValue = {
307
+ state,
308
+ isDisabled, // Pass the accessor function, not the value
309
+ disclosureAria,
310
+ };
311
+
312
+ // Setter for panel ref
313
+ const setPanelRef = (el: HTMLElement | null) => {
314
+ setPanelRefSignal(el);
315
+ };
316
+
317
+ return (
318
+ <DisclosureContext.Provider value={contextValue}>
319
+ <DisclosurePanelRefContext.Provider value={setPanelRef}>
320
+ <div
321
+ {...domProps()}
322
+ class={renderProps.class()}
323
+ style={renderProps.style()}
324
+ data-expanded={dataAttr(state.isExpanded())}
325
+ data-disabled={dataAttr(isDisabled())}
326
+ >
327
+ {props.children}
328
+ </div>
329
+ </DisclosurePanelRefContext.Provider>
330
+ </DisclosureContext.Provider>
331
+ );
332
+ }
333
+
334
+ // Internal context to pass panel ref setter
335
+ const DisclosurePanelRefContext = createContext<((el: HTMLElement | null) => void) | null>(null);
336
+
337
+ // ============================================
338
+ // DISCLOSURE TRIGGER
339
+ // ============================================
340
+
341
+ /**
342
+ * DisclosureTrigger is the button that toggles the disclosure.
343
+ * Pattern matches SelectTrigger for consistency.
344
+ */
345
+ export function DisclosureTrigger(props: DisclosureTriggerProps): JSX.Element {
346
+ // Get context - now safe because parent uses lazy children evaluation
347
+ const context = useContext(DisclosureContext);
348
+ if (!context) {
349
+ throw new Error('DisclosureTrigger must be used within a Disclosure');
350
+ }
351
+
352
+ const { state, disclosureAria, isDisabled } = context;
353
+
354
+ // Reactive accessors
355
+ const isExpanded = () => state.isExpanded();
356
+
357
+ // Get buttonProps from the getter each time - this ensures reactivity
358
+ // IMPORTANT: Call the getter fresh each render to get updated aria-expanded, etc.
359
+ const getButtonProps = () => {
360
+ const { ref: _ref, ...rest } = disclosureAria.buttonProps as Record<string, unknown>;
361
+ return rest;
362
+ };
363
+
364
+ return (
365
+ <button
366
+ {...getButtonProps()}
367
+ type="button"
368
+ class={props.class}
369
+ style={props.style}
370
+ data-expanded={dataAttr(isExpanded())}
371
+ data-disabled={dataAttr(isDisabled())}
372
+ >
373
+ {props.children}
374
+ </button>
375
+ );
376
+ }
377
+
378
+ // ============================================
379
+ // DISCLOSURE PANEL
380
+ // ============================================
381
+
382
+ /**
383
+ * DisclosurePanel contains the content that is shown/hidden.
384
+ */
385
+ export function DisclosurePanel(props: DisclosurePanelProps): JSX.Element {
386
+ // Get context - now safe because parent uses lazy children evaluation
387
+ const context = useContext(DisclosureContext);
388
+ const panelRefSetter = useContext(DisclosurePanelRefContext);
389
+
390
+ const [local, rest] = splitProps(props, ['class', 'style']);
391
+
392
+ // Reactive accessors
393
+ const isExpanded = () => context?.state.isExpanded() ?? false;
394
+ const isDisabled = () => context?.isDisabled() ?? false;
395
+
396
+ // Render props values
397
+ const renderValues = createMemo<DisclosureRenderProps>(() => ({
398
+ isExpanded: isExpanded(),
399
+ isDisabled: isDisabled(),
400
+ }));
401
+
402
+ // Resolve render props
403
+ const renderProps = useRenderProps(
404
+ {
405
+ children: props.children,
406
+ class: local.class,
407
+ style: local.style,
408
+ defaultClassName: 'solidaria-DisclosurePanel',
409
+ },
410
+ renderValues
411
+ );
412
+
413
+ // Filter DOM props
414
+ const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }));
415
+
416
+ // Get panelProps from the getter each time - this ensures reactivity
417
+ // IMPORTANT: Call the getter fresh each render to get updated hidden attribute, etc.
418
+ const getPanelProps = () => {
419
+ if (!context) return { id: undefined, role: 'region', 'aria-labelledby': undefined, hidden: true };
420
+ const { ref: _ref, ...rest } = context.disclosureAria.panelProps as Record<string, unknown>;
421
+ return rest;
422
+ };
423
+
424
+ return (
425
+ <div
426
+ {...domProps()}
427
+ {...getPanelProps()}
428
+ ref={(el) => panelRefSetter?.(el)}
429
+ class={renderProps.class()}
430
+ style={renderProps.style()}
431
+ data-expanded={dataAttr(isExpanded())}
432
+ >
433
+ {renderProps.renderChildren()}
434
+ </div>
435
+ );
436
+ }
437
+
438
+ // Re-export state types for convenience
439
+ export type { DisclosureState, DisclosureGroupState };