@proyecto-viviana/solidaria-components 0.2.5 → 0.2.9

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 (194) hide show
  1. package/LICENSE +21 -0
  2. package/dist/ActionBar.d.ts +71 -0
  3. package/dist/ActionBar.d.ts.map +1 -0
  4. package/dist/ActionGroup.d.ts +74 -0
  5. package/dist/ActionGroup.d.ts.map +1 -0
  6. package/dist/Alert.d.ts +70 -0
  7. package/dist/Alert.d.ts.map +1 -0
  8. package/dist/Breadcrumbs.d.ts +10 -2
  9. package/dist/Breadcrumbs.d.ts.map +1 -1
  10. package/dist/Button.d.ts +4 -0
  11. package/dist/Button.d.ts.map +1 -1
  12. package/dist/Calendar.d.ts +13 -0
  13. package/dist/Calendar.d.ts.map +1 -1
  14. package/dist/Checkbox.d.ts +2 -2
  15. package/dist/Checkbox.d.ts.map +1 -1
  16. package/dist/Collection.d.ts +125 -0
  17. package/dist/Collection.d.ts.map +1 -0
  18. package/dist/Color.d.ts +114 -2
  19. package/dist/Color.d.ts.map +1 -1
  20. package/dist/ColorEditor.d.ts +42 -0
  21. package/dist/ColorEditor.d.ts.map +1 -0
  22. package/dist/ComboBox.d.ts +64 -0
  23. package/dist/ComboBox.d.ts.map +1 -1
  24. package/dist/ContextualHelpTrigger.d.ts +40 -0
  25. package/dist/ContextualHelpTrigger.d.ts.map +1 -0
  26. package/dist/DateField.d.ts +27 -2
  27. package/dist/DateField.d.ts.map +1 -1
  28. package/dist/DatePicker.d.ts +67 -2
  29. package/dist/DatePicker.d.ts.map +1 -1
  30. package/dist/Dialog.d.ts.map +1 -1
  31. package/dist/Disclosure.d.ts +2 -0
  32. package/dist/Disclosure.d.ts.map +1 -1
  33. package/dist/DragAndDrop.d.ts +80 -0
  34. package/dist/DragAndDrop.d.ts.map +1 -0
  35. package/dist/DragPreview.d.ts +14 -0
  36. package/dist/DragPreview.d.ts.map +1 -0
  37. package/dist/DropZone.d.ts +27 -0
  38. package/dist/DropZone.d.ts.map +1 -0
  39. package/dist/FieldError.d.ts +23 -0
  40. package/dist/FieldError.d.ts.map +1 -0
  41. package/dist/FileTrigger.d.ts +26 -0
  42. package/dist/FileTrigger.d.ts.map +1 -0
  43. package/dist/Focusable.d.ts +27 -0
  44. package/dist/Focusable.d.ts.map +1 -0
  45. package/dist/Form.d.ts +27 -0
  46. package/dist/Form.d.ts.map +1 -0
  47. package/dist/GridList.d.ts +40 -1
  48. package/dist/GridList.d.ts.map +1 -1
  49. package/dist/Icon.d.ts +57 -0
  50. package/dist/Icon.d.ts.map +1 -0
  51. package/dist/Keyboard.d.ts +13 -0
  52. package/dist/Keyboard.d.ts.map +1 -0
  53. package/dist/Link.d.ts.map +1 -1
  54. package/dist/ListBox.d.ts +43 -1
  55. package/dist/ListBox.d.ts.map +1 -1
  56. package/dist/ListDropTargetDelegate.d.ts +38 -0
  57. package/dist/ListDropTargetDelegate.d.ts.map +1 -0
  58. package/dist/Menu.d.ts +20 -2
  59. package/dist/Menu.d.ts.map +1 -1
  60. package/dist/Meter.d.ts +2 -2
  61. package/dist/Meter.d.ts.map +1 -1
  62. package/dist/Modal.d.ts +2 -0
  63. package/dist/Modal.d.ts.map +1 -1
  64. package/dist/NumberField.d.ts +2 -0
  65. package/dist/NumberField.d.ts.map +1 -1
  66. package/dist/Popover.d.ts +4 -2
  67. package/dist/Popover.d.ts.map +1 -1
  68. package/dist/Pressable.d.ts +27 -0
  69. package/dist/Pressable.d.ts.map +1 -0
  70. package/dist/ProgressBar.d.ts +2 -2
  71. package/dist/ProgressBar.d.ts.map +1 -1
  72. package/dist/RadioGroup.d.ts.map +1 -1
  73. package/dist/RangeCalendar.d.ts +5 -0
  74. package/dist/RangeCalendar.d.ts.map +1 -1
  75. package/dist/RouterProvider.d.ts +75 -0
  76. package/dist/RouterProvider.d.ts.map +1 -0
  77. package/dist/SearchField.d.ts +2 -3
  78. package/dist/SearchField.d.ts.map +1 -1
  79. package/dist/Select.d.ts +11 -0
  80. package/dist/Select.d.ts.map +1 -1
  81. package/dist/SelectionIndicator.d.ts +30 -0
  82. package/dist/SelectionIndicator.d.ts.map +1 -0
  83. package/dist/SharedElementTransition.d.ts +39 -0
  84. package/dist/SharedElementTransition.d.ts.map +1 -0
  85. package/dist/Slider.d.ts +6 -3
  86. package/dist/Slider.d.ts.map +1 -1
  87. package/dist/Table.d.ts +39 -0
  88. package/dist/Table.d.ts.map +1 -1
  89. package/dist/Tabs.d.ts +4 -3
  90. package/dist/Tabs.d.ts.map +1 -1
  91. package/dist/TagGroup.d.ts +12 -2
  92. package/dist/TagGroup.d.ts.map +1 -1
  93. package/dist/Text.d.ts +10 -0
  94. package/dist/Text.d.ts.map +1 -0
  95. package/dist/TextField.d.ts +4 -0
  96. package/dist/TextField.d.ts.map +1 -1
  97. package/dist/TimeField.d.ts +26 -1
  98. package/dist/TimeField.d.ts.map +1 -1
  99. package/dist/Toast.d.ts.map +1 -1
  100. package/dist/ToggleButton.d.ts +30 -0
  101. package/dist/ToggleButton.d.ts.map +1 -0
  102. package/dist/ToggleButtonGroup.d.ts +33 -0
  103. package/dist/ToggleButtonGroup.d.ts.map +1 -0
  104. package/dist/Toolbar.d.ts.map +1 -1
  105. package/dist/Tooltip.d.ts +9 -0
  106. package/dist/Tooltip.d.ts.map +1 -1
  107. package/dist/Tree.d.ts +44 -2
  108. package/dist/Tree.d.ts.map +1 -1
  109. package/dist/Virtualizer.d.ts +61 -0
  110. package/dist/Virtualizer.d.ts.map +1 -0
  111. package/dist/VirtualizerLayouts.d.ts +82 -0
  112. package/dist/VirtualizerLayouts.d.ts.map +1 -0
  113. package/dist/VisuallyHidden.d.ts +3 -1
  114. package/dist/VisuallyHidden.d.ts.map +1 -1
  115. package/dist/contexts.d.ts +1 -0
  116. package/dist/contexts.d.ts.map +1 -1
  117. package/dist/index.d.ts +57 -25
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +13961 -5946
  120. package/dist/index.js.map +1 -7
  121. package/dist/index.ssr.js +9612 -2401
  122. package/dist/index.ssr.js.map +1 -7
  123. package/dist/useDragAndDrop.d.ts +93 -0
  124. package/dist/useDragAndDrop.d.ts.map +1 -0
  125. package/dist/utils.d.ts +7 -1
  126. package/dist/utils.d.ts.map +1 -1
  127. package/dist/virtualizer/Layout.d.ts +79 -0
  128. package/dist/virtualizer/Layout.d.ts.map +1 -0
  129. package/package.json +8 -6
  130. package/src/ActionBar.tsx +248 -0
  131. package/src/ActionGroup.tsx +285 -0
  132. package/src/Alert.tsx +177 -0
  133. package/src/Autocomplete.tsx +1 -1
  134. package/src/Breadcrumbs.tsx +103 -17
  135. package/src/Button.tsx +65 -21
  136. package/src/Calendar.tsx +179 -53
  137. package/src/Checkbox.tsx +1 -2
  138. package/src/Collection.tsx +341 -0
  139. package/src/Color.tsx +652 -34
  140. package/src/ColorEditor.tsx +231 -0
  141. package/src/ComboBox.tsx +315 -81
  142. package/src/ContextualHelpTrigger.tsx +183 -0
  143. package/src/DateField.tsx +93 -19
  144. package/src/DatePicker.tsx +495 -25
  145. package/src/Dialog.tsx +40 -9
  146. package/src/Disclosure.tsx +33 -27
  147. package/src/DragAndDrop.tsx +334 -0
  148. package/src/DragPreview.tsx +45 -0
  149. package/src/DropZone.tsx +213 -0
  150. package/src/FieldError.tsx +67 -0
  151. package/src/FileTrigger.tsx +83 -0
  152. package/src/Focusable.tsx +106 -0
  153. package/src/Form.tsx +85 -0
  154. package/src/GridList.tsx +379 -41
  155. package/src/Icon.tsx +154 -0
  156. package/src/Keyboard.tsx +26 -0
  157. package/src/Link.tsx +14 -1
  158. package/src/ListBox.tsx +484 -33
  159. package/src/ListDropTargetDelegate.ts +282 -0
  160. package/src/Menu.tsx +388 -35
  161. package/src/Meter.tsx +7 -3
  162. package/src/Modal.tsx +32 -4
  163. package/src/NumberField.tsx +163 -43
  164. package/src/Popover.tsx +136 -180
  165. package/src/Pressable.tsx +108 -0
  166. package/src/ProgressBar.tsx +7 -3
  167. package/src/RadioGroup.tsx +35 -25
  168. package/src/RangeCalendar.tsx +100 -68
  169. package/src/RouterProvider.tsx +240 -0
  170. package/src/SearchField.tsx +142 -34
  171. package/src/Select.tsx +221 -73
  172. package/src/SelectionIndicator.tsx +105 -0
  173. package/src/SharedElementTransition.tsx +258 -0
  174. package/src/Slider.tsx +16 -6
  175. package/src/Table.tsx +417 -57
  176. package/src/Tabs.tsx +68 -35
  177. package/src/TagGroup.tsx +121 -36
  178. package/src/Text.tsx +18 -0
  179. package/src/TextField.tsx +25 -8
  180. package/src/TimeField.tsx +101 -151
  181. package/src/Toast.tsx +108 -14
  182. package/src/ToggleButton.tsx +159 -0
  183. package/src/ToggleButtonGroup.tsx +136 -0
  184. package/src/Toolbar.tsx +14 -8
  185. package/src/Tooltip.tsx +108 -19
  186. package/src/Tree.tsx +1143 -87
  187. package/src/Virtualizer.tsx +702 -0
  188. package/src/VirtualizerLayouts.ts +265 -0
  189. package/src/VisuallyHidden.tsx +15 -21
  190. package/src/contexts.ts +1 -0
  191. package/src/index.ts +1057 -620
  192. package/src/useDragAndDrop.ts +351 -0
  193. package/src/utils.tsx +37 -3
  194. package/src/virtualizer/Layout.ts +200 -0
package/src/Menu.tsx CHANGED
@@ -8,7 +8,9 @@
8
8
  import {
9
9
  type JSX,
10
10
  createContext,
11
+ createEffect,
11
12
  createMemo,
13
+ createSignal,
12
14
  splitProps,
13
15
  useContext,
14
16
  For,
@@ -22,6 +24,7 @@ import {
22
24
  createHover,
23
25
  createButton,
24
26
  createInteractOutside,
27
+ mergeProps,
25
28
  FocusScope,
26
29
  type AriaMenuProps,
27
30
  type AriaMenuItemProps,
@@ -33,6 +36,7 @@ import {
33
36
  type MenuState,
34
37
  type OverlayTriggerState,
35
38
  type Key,
39
+ type DropTarget,
36
40
  } from '@proyecto-viviana/solid-stately';
37
41
  import {
38
42
  type RenderChildren,
@@ -41,6 +45,27 @@ import {
41
45
  type SlotProps,
42
46
  useRenderProps,
43
47
  } from './utils';
48
+ import { SharedElementTransition } from './SharedElementTransition';
49
+ import { type DragAndDropHooks } from './useDragAndDrop';
50
+ import {
51
+ CollectionRendererContext,
52
+ Section,
53
+ Header,
54
+ Group,
55
+ type CollectionEntry,
56
+ type CollectionRendererContextValue,
57
+ type SectionProps,
58
+ useCollectionRenderer,
59
+ flattenCollectionEntries,
60
+ isCollectionSection,
61
+ } from './Collection';
62
+ import { useVirtualizerContext } from './Virtualizer';
63
+ import {
64
+ getNormalizedDropTargetKey,
65
+ mergePersistedKeysIntoVirtualRange,
66
+ useDndPersistedKeys,
67
+ useRenderDropIndicator,
68
+ } from './DragAndDrop';
44
69
 
45
70
  // ============================================
46
71
  // TYPES
@@ -57,7 +82,7 @@ export interface MenuProps<T>
57
82
  extends Omit<AriaMenuProps, 'children'>,
58
83
  SlotProps {
59
84
  /** The items to render in the menu. */
60
- items: T[];
85
+ items: CollectionEntry<T>[];
61
86
  /** Function to get the key from an item. */
62
87
  getKey?: (item: T) => Key;
63
88
  /** Function to get the text value from an item. */
@@ -76,6 +101,8 @@ export interface MenuProps<T>
76
101
  class?: ClassNameOrFunction<MenuRenderProps>;
77
102
  /** The inline style for the element. */
78
103
  style?: StyleOrFunction<MenuRenderProps>;
104
+ /** Drag and drop hooks from `useDragAndDrop`. */
105
+ dragAndDropHooks?: DragAndDropHooks<T>;
79
106
  }
80
107
 
81
108
  export interface MenuItemRenderProps {
@@ -140,12 +167,18 @@ export interface MenuTriggerProps extends Omit<AriaMenuTriggerProps, 'children'>
140
167
  onOpenChange?: (isOpen: boolean) => void;
141
168
  }
142
169
 
170
+ export interface SubmenuTriggerProps extends MenuTriggerProps {}
171
+
143
172
  // ============================================
144
173
  // CONTEXT
145
174
  // ============================================
146
175
 
147
176
  interface MenuContextValue<T> {
148
177
  state: MenuState<T>;
178
+ isDisabled: () => boolean;
179
+ dragAndDropHooks?: DragAndDropHooks<T>;
180
+ dragState?: unknown;
181
+ dropState?: unknown;
149
182
  }
150
183
 
151
184
  interface MenuTriggerContextValue {
@@ -157,6 +190,7 @@ interface MenuTriggerContextValue {
157
190
  export const MenuContext = createContext<MenuContextValue<unknown> | null>(null);
158
191
  export const MenuStateContext = createContext<MenuState<unknown> | null>(null);
159
192
  export const MenuTriggerContext = createContext<MenuTriggerContextValue | null>(null);
193
+ export const RootMenuTriggerStateContext = createContext<OverlayTriggerState | null>(null);
160
194
 
161
195
  // ============================================
162
196
  // COMPONENTS
@@ -192,22 +226,28 @@ export function MenuTrigger(props: MenuTriggerProps): JSX.Element {
192
226
  );
193
227
 
194
228
  return (
195
- <MenuTriggerContext.Provider
196
- value={{
197
- state,
198
- triggerProps: menuTriggerProps,
199
- menuProps,
200
- }}
201
- >
202
- {props.children}
203
- </MenuTriggerContext.Provider>
229
+ <RootMenuTriggerStateContext.Provider value={state}>
230
+ <MenuTriggerContext.Provider
231
+ value={{
232
+ state,
233
+ triggerProps: menuTriggerProps,
234
+ menuProps,
235
+ }}
236
+ >
237
+ {props.children}
238
+ </MenuTriggerContext.Provider>
239
+ </RootMenuTriggerStateContext.Provider>
204
240
  );
205
241
  }
206
242
 
243
+ export function SubmenuTrigger(props: SubmenuTriggerProps): JSX.Element {
244
+ return <MenuTrigger {...props} />;
245
+ }
246
+
207
247
  /**
208
248
  * A button that opens a menu.
209
249
  */
210
- export interface MenuButtonProps extends SlotProps {
250
+ export interface MenuButtonProps extends SlotProps, Omit<JSX.HTMLAttributes<HTMLButtonElement>, 'class' | 'style' | 'children'> {
211
251
  /** The children of the button. A function may be provided to receive render props. */
212
252
  children?: RenderChildren<MenuTriggerRenderProps>;
213
253
  /** The CSS className for the element. */
@@ -218,8 +258,10 @@ export interface MenuButtonProps extends SlotProps {
218
258
  isDisabled?: boolean;
219
259
  }
220
260
 
261
+ export interface MenuSectionProps extends SectionProps {}
262
+
221
263
  export function MenuButton(props: MenuButtonProps): JSX.Element {
222
- const [local] = splitProps(props, ['class', 'style', 'slot', 'isDisabled']);
264
+ const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'isDisabled', 'children']);
223
265
 
224
266
  // Get trigger context
225
267
  const context = useContext(MenuTriggerContext);
@@ -289,6 +331,7 @@ export function MenuButton(props: MenuButtonProps): JSX.Element {
289
331
 
290
332
  return (
291
333
  <button
334
+ {...domProps}
292
335
  {...cleanTriggerProps()}
293
336
  {...cleanButtonProps()}
294
337
  {...cleanFocusProps()}
@@ -315,19 +358,25 @@ export function Menu<T>(props: MenuProps<T>): JSX.Element {
315
358
  const [local, stateProps, ariaProps] = splitProps(
316
359
  props,
317
360
  ['children', 'class', 'style', 'slot'],
318
- ['items', 'getKey', 'getTextValue', 'getDisabled', 'disabledKeys', 'onAction', 'onClose']
361
+ ['items', 'getKey', 'getTextValue', 'getDisabled', 'disabledKeys', 'onAction', 'onClose', 'dragAndDropHooks']
319
362
  );
320
363
 
321
364
  // Get trigger context if available
322
365
  const triggerContext = useContext(MenuTriggerContext);
323
366
 
324
367
  // Ref for the menu element (for click outside detection)
325
- let menuRef: HTMLUListElement | undefined;
368
+ const [menuRef, setMenuRef] = createSignal<HTMLUListElement | null>(null);
369
+
370
+ const flatItems = createMemo<T[]>(() => {
371
+ return flattenCollectionEntries(stateProps.items);
372
+ });
373
+
374
+ const hasSections = createMemo(() => stateProps.items.some((item) => isCollectionSection(item)));
326
375
 
327
376
  // Create menu state
328
377
  const state = createMenuState<T>({
329
378
  get items() {
330
- return stateProps.items;
379
+ return flatItems();
331
380
  },
332
381
  get getKey() {
333
382
  return stateProps.getKey;
@@ -349,9 +398,26 @@ export function Menu<T>(props: MenuProps<T>): JSX.Element {
349
398
  },
350
399
  });
351
400
 
401
+ const resolveDisabled = (): boolean => {
402
+ const disabled = ariaProps.isDisabled;
403
+ if (typeof disabled === 'function') {
404
+ return (disabled as () => boolean)();
405
+ }
406
+ return !!disabled;
407
+ };
408
+
352
409
  // Create menu aria props
353
- const { menuProps } = createMenu(
410
+ const { menuProps, labelProps } = createMenu(
354
411
  {
412
+ get isDisabled() {
413
+ return resolveDisabled();
414
+ },
415
+ get label() {
416
+ return ariaProps.label;
417
+ },
418
+ get onAction() {
419
+ return stateProps.onAction;
420
+ },
355
421
  get onClose() {
356
422
  return stateProps.onClose ?? (() => triggerContext?.state.close());
357
423
  },
@@ -361,6 +427,9 @@ export function Menu<T>(props: MenuProps<T>): JSX.Element {
361
427
  get 'aria-labelledby'() {
362
428
  return ariaProps['aria-labelledby'];
363
429
  },
430
+ get 'aria-describedby'() {
431
+ return ariaProps['aria-describedby'];
432
+ },
364
433
  },
365
434
  state
366
435
  );
@@ -370,7 +439,7 @@ export function Menu<T>(props: MenuProps<T>): JSX.Element {
370
439
 
371
440
  // Handle click outside to close menu
372
441
  createInteractOutside({
373
- ref: () => menuRef ?? null,
442
+ ref: () => menuRef(),
374
443
  onInteractOutside: () => {
375
444
  if (triggerContext?.state.isOpen()) {
376
445
  triggerContext.state.close();
@@ -411,27 +480,264 @@ export function Menu<T>(props: MenuProps<T>): JSX.Element {
411
480
  const { ref: _ref3, ...rest } = focusProps as Record<string, unknown>;
412
481
  return rest;
413
482
  };
483
+ const cleanLabelProps = () => {
484
+ const { ref: _ref4, ...rest } = labelProps as Record<string, unknown>;
485
+ return rest;
486
+ };
414
487
 
415
488
  // If inside a MenuTrigger, only render when open
416
489
  // If standalone (no trigger context), always render
417
490
  const shouldRender = () => triggerContext ? triggerContext.state.isOpen() : true;
491
+ const parentCollectionRenderer = useCollectionRenderer<unknown>();
492
+ const virtualizer = useVirtualizerContext();
493
+ const getItemNodes = createMemo(() => Array.from(state.collection()).filter((node) => node.type === 'item'));
494
+ const getDropTargetByIndex = (index: number, position: 'before' | 'after' | 'on'): DropTarget | null => {
495
+ const node = getItemNodes()[index];
496
+ if (!node) return null;
497
+ return { type: 'item', key: node.key, dropPosition: position };
498
+ };
499
+ const hasDroppableDnd = createMemo(() => {
500
+ const hooks = stateProps.dragAndDropHooks;
501
+ return Boolean(
502
+ hooks?.useDroppableCollectionState &&
503
+ hooks.useDroppableCollection &&
504
+ (hooks.dropTargetDelegate || parentCollectionRenderer?.dropTargetDelegate || hooks.ListDropTargetDelegate)
505
+ );
506
+ });
507
+ const hasDraggableDnd = createMemo(() => {
508
+ const hooks = stateProps.dragAndDropHooks;
509
+ return Boolean(hooks?.useDraggableCollectionState && hooks.useDraggableCollection);
510
+ });
511
+ const dragState = createMemo(() => {
512
+ if (!hasDraggableDnd()) return undefined;
513
+ return stateProps.dragAndDropHooks?.useDraggableCollectionState?.({
514
+ items: flatItems(),
515
+ });
516
+ });
517
+ const dropState = createMemo(() => {
518
+ if (!hasDroppableDnd()) return undefined;
519
+ return stateProps.dragAndDropHooks?.useDroppableCollectionState?.({});
520
+ });
521
+ const persistedKeys = useDndPersistedKeys(
522
+ { focusedKey: state.focusedKey },
523
+ stateProps.dragAndDropHooks,
524
+ dropState(),
525
+ state.collection()
526
+ );
527
+ const virtualRange = createMemo(() => {
528
+ if (!virtualizer || !parentCollectionRenderer?.isVirtualized || hasSections()) return null;
529
+ const baseRange = virtualizer.getVisibleRange(stateProps.items.length);
530
+ const itemNodes = getItemNodes();
531
+ const persistedIndexes = Array.from(persistedKeys())
532
+ .map((key) => itemNodes.findIndex((node) => node.key === key))
533
+ .filter((index) => index >= 0);
534
+ const dropTarget = dropState()?.target;
535
+ const normalizedDropKey = getNormalizedDropTargetKey(dropTarget, state.collection());
536
+ const focusedKey = state.focusedKey();
537
+ const focusedIndex = focusedKey != null ? itemNodes.findIndex((node) => node.key === focusedKey) : -1;
538
+ const forceIncludeIndexes = [
539
+ dropTarget?.type === 'item' ? itemNodes.findIndex((node) => node.key === dropTarget.key) : -1,
540
+ normalizedDropKey != null ? itemNodes.findIndex((node) => node.key === normalizedDropKey) : -1,
541
+ dropTarget?.type === 'item' ? -1 : focusedIndex,
542
+ ].filter((index) => index >= 0);
543
+ return mergePersistedKeysIntoVirtualRange(baseRange, persistedIndexes, stateProps.items.length, virtualizer, 80, {
544
+ forceIncludeIndexes,
545
+ forceIncludeMaxSpan: 320,
546
+ });
547
+ });
548
+ const visibleItems = createMemo(() => {
549
+ const range = virtualRange();
550
+ if (!range) return stateProps.items;
551
+ return stateProps.items.slice(range.start, range.end);
552
+ });
553
+ createEffect(() => {
554
+ if (!hasDraggableDnd()) return;
555
+ const hooks = stateProps.dragAndDropHooks;
556
+ const activeDragState = dragState();
557
+ if (!hooks?.useDraggableCollection || !activeDragState) return;
558
+ hooks.useDraggableCollection({}, activeDragState, () => menuRef());
559
+ });
560
+ const droppableCollection = createMemo(() => {
561
+ if (!hasDroppableDnd()) return undefined;
562
+ const hooks = stateProps.dragAndDropHooks;
563
+ const activeDropState = dropState();
564
+ if (!hooks?.useDroppableCollection || !activeDropState) return undefined;
565
+ const resolveDirection = (): 'ltr' | 'rtl' => {
566
+ const menuEl = menuRef();
567
+ if (menuEl && typeof window !== 'undefined' && typeof window.getComputedStyle === 'function') {
568
+ const dir = window.getComputedStyle(menuEl).direction;
569
+ if (dir === 'rtl') return 'rtl';
570
+ }
571
+ return typeof document !== 'undefined' && document.dir === 'rtl' ? 'rtl' : 'ltr';
572
+ };
573
+ const dropTargetDelegate = hooks.dropTargetDelegate
574
+ ?? parentCollectionRenderer?.dropTargetDelegate
575
+ ?? (hooks.ListDropTargetDelegate
576
+ ? new hooks.ListDropTargetDelegate(
577
+ () => state.collection(),
578
+ () => menuRef(),
579
+ { layout: 'stack', orientation: 'vertical', direction: resolveDirection() }
580
+ )
581
+ : undefined);
582
+ if (!dropTargetDelegate) return undefined;
583
+ return hooks.useDroppableCollection(
584
+ {
585
+ dropTargetDelegate,
586
+ keyboardDelegate: {
587
+ getFirstKey: () => state.collection().getFirstKey(),
588
+ getLastKey: () => state.collection().getLastKey(),
589
+ getKeyBelow: (key) => state.collection().getKeyAfter(key),
590
+ getKeyAbove: (key) => state.collection().getKeyBefore(key),
591
+ getKeyPageBelow: (key) => state.collection().getKeyAfter(key),
592
+ getKeyPageAbove: (key) => state.collection().getKeyBefore(key),
593
+ },
594
+ },
595
+ activeDropState,
596
+ () => menuRef()
597
+ );
598
+ });
599
+ const isRootDropTarget = createMemo(() => {
600
+ return Boolean(dropState()?.target?.type === 'root');
601
+ });
602
+ const dndRenderDropIndicator = createMemo(() => useRenderDropIndicator(stateProps.dragAndDropHooks, dropState()));
603
+ const dndDropIndicator = (index: number, position: 'before' | 'after' | 'on') => {
604
+ const target = getDropTargetByIndex(index, position);
605
+ if (!target || target.type !== 'item') return undefined;
606
+ return dndRenderDropIndicator()?.(target);
607
+ };
608
+ const sectionedRenderEntries = createMemo(() => {
609
+ let globalIndex = 0;
610
+ return stateProps.items.map((entry) => {
611
+ if (isCollectionSection(entry)) {
612
+ const sectionItems = entry.items.map((item) => ({
613
+ item,
614
+ index: globalIndex++,
615
+ }));
616
+ return {
617
+ type: 'section' as const,
618
+ section: entry,
619
+ items: sectionItems,
620
+ };
621
+ }
622
+ const indexedItem = {
623
+ item: entry as T,
624
+ index: globalIndex++,
625
+ };
626
+ return {
627
+ type: 'item' as const,
628
+ item: indexedItem,
629
+ };
630
+ });
631
+ });
632
+ const collectionRenderer = createMemo<CollectionRendererContextValue<unknown>>(() => ({
633
+ ...parentCollectionRenderer,
634
+ renderItem: (item) => props.children(item as T),
635
+ renderDropIndicator: (index, position) =>
636
+ dndDropIndicator(index, position) ?? parentCollectionRenderer?.renderDropIndicator?.(index, position),
637
+ }));
418
638
 
419
639
  // Only use FocusScope when inside a MenuTrigger (for popover behavior)
420
640
  // Standalone menus don't need focus restoration
421
641
  const menuContent = () => (
422
- <MenuContext.Provider value={{ state }}>
642
+ <MenuContext.Provider
643
+ value={{
644
+ state,
645
+ isDisabled: resolveDisabled,
646
+ dragAndDropHooks: stateProps.dragAndDropHooks,
647
+ dragState: dragState(),
648
+ dropState: dropState(),
649
+ } as MenuContextValue<unknown>}
650
+ >
423
651
  <MenuStateContext.Provider value={state}>
424
- <ul
425
- ref={(el) => (menuRef = el)}
426
- {...cleanMenuProps()}
427
- {...cleanTriggerMenuProps()}
428
- {...cleanFocusProps()}
429
- class={renderProps.class()}
430
- style={renderProps.style()}
431
- data-focused={state.isFocused() || undefined}
432
- >
433
- <For each={stateProps.items}>{(item) => props.children?.(item)}</For>
434
- </ul>
652
+ <CollectionRendererContext.Provider value={collectionRenderer()}>
653
+ <>
654
+ <Show when={ariaProps.label}>
655
+ <span {...cleanLabelProps()}>{ariaProps.label as JSX.Element}</span>
656
+ </Show>
657
+ <ul
658
+ ref={setMenuRef}
659
+ {...mergeProps(
660
+ cleanMenuProps(),
661
+ cleanTriggerMenuProps(),
662
+ cleanFocusProps(),
663
+ (droppableCollection()?.collectionProps as Record<string, unknown> | undefined) ?? {}
664
+ )}
665
+ class={renderProps.class()}
666
+ style={renderProps.style()}
667
+ data-focused={state.isFocused() || undefined}
668
+ data-disabled={resolveDisabled() || undefined}
669
+ data-drop-target={isRootDropTarget() || undefined}
670
+ >
671
+ <SharedElementTransition>
672
+ {hasSections()
673
+ ? (
674
+ <For each={sectionedRenderEntries()}>
675
+ {(entry) =>
676
+ entry.type === 'section'
677
+ ? (
678
+ <li role="presentation" data-section-wrapper>
679
+ <Section class="solidaria-Menu-section">
680
+ {entry.section.title != null && (
681
+ <Header class="solidaria-Menu-sectionHeader">{entry.section.title}</Header>
682
+ )}
683
+ <Group class="solidaria-Menu-sectionGroup">
684
+ <ul role="group" aria-label={entry.section['aria-label']}>
685
+ <For each={entry.items}>
686
+ {(indexedItem) => (
687
+ <>
688
+ {collectionRenderer().renderDropIndicator?.(indexedItem.index, 'before')}
689
+ {collectionRenderer().renderDropIndicator?.(indexedItem.index, 'on')}
690
+ {props.children?.(indexedItem.item)}
691
+ {collectionRenderer().renderDropIndicator?.(indexedItem.index, 'after')}
692
+ </>
693
+ )}
694
+ </For>
695
+ </ul>
696
+ </Group>
697
+ </Section>
698
+ </li>
699
+ )
700
+ : (
701
+ <>
702
+ {collectionRenderer().renderDropIndicator?.(entry.item.index, 'before')}
703
+ {collectionRenderer().renderDropIndicator?.(entry.item.index, 'on')}
704
+ {props.children?.(entry.item.item)}
705
+ {collectionRenderer().renderDropIndicator?.(entry.item.index, 'after')}
706
+ </>
707
+ )
708
+ }
709
+ </For>
710
+ )
711
+ : (
712
+ <>
713
+ {virtualRange()?.offsetTop
714
+ ? <li role="presentation" aria-hidden="true" style={{ height: `${virtualRange()!.offsetTop}px` }} data-virtualizer-spacer="top" />
715
+ : null}
716
+ <For each={visibleItems()}>
717
+ {(item, index) => {
718
+ const itemIndex = () => (virtualRange()?.start ?? 0) + index();
719
+ const beforeIndicator = () => collectionRenderer().renderDropIndicator?.(itemIndex(), 'before');
720
+ const onIndicator = () => collectionRenderer().renderDropIndicator?.(itemIndex(), 'on');
721
+ const afterIndicator = () => collectionRenderer().renderDropIndicator?.(itemIndex(), 'after');
722
+ return (
723
+ <>
724
+ {beforeIndicator()}
725
+ {onIndicator()}
726
+ {props.children?.(item as T)}
727
+ {afterIndicator()}
728
+ </>
729
+ );
730
+ }}
731
+ </For>
732
+ {virtualRange()?.offsetBottom
733
+ ? <li role="presentation" aria-hidden="true" style={{ height: `${virtualRange()!.offsetBottom}px` }} data-virtualizer-spacer="bottom" />
734
+ : null}
735
+ </>
736
+ )}
737
+ </SharedElementTransition>
738
+ </ul>
739
+ </>
740
+ </CollectionRendererContext.Provider>
435
741
  </MenuStateContext.Provider>
436
742
  </MenuContext.Provider>
437
743
  );
@@ -467,16 +773,18 @@ export function MenuItem<T>(props: MenuItemProps<T>): JSX.Element {
467
773
  throw new Error('MenuItem must be used within a Menu');
468
774
  }
469
775
  const state = context as MenuState<T>;
776
+ const menuContext = useContext(MenuContext) as MenuContextValue<T> | null;
777
+ const [ref, setRef] = createSignal<HTMLLIElement | null>(null);
470
778
 
471
779
  // Create menu item aria props
472
780
  const itemAria = createMenuItem<T>(
473
781
  {
474
782
  key: local.id,
475
783
  get isDisabled() {
476
- return ariaProps.isDisabled;
784
+ return Boolean(ariaProps.isDisabled || menuContext?.isDisabled());
477
785
  },
478
786
  get 'aria-label'() {
479
- return ariaProps['aria-label'];
787
+ return ariaProps['aria-label'] ?? local.textValue;
480
788
  },
481
789
  get onAction() {
482
790
  return local.onAction;
@@ -512,21 +820,55 @@ export function MenuItem<T>(props: MenuItemProps<T>): JSX.Element {
512
820
  },
513
821
  renderValues
514
822
  );
823
+ const hasPrimitiveLabel = () => {
824
+ return typeof props.children === 'string' || typeof props.children === 'number';
825
+ };
515
826
 
516
827
  // Remove ref from spread props
517
828
  const cleanItemProps = () => {
518
- const { ref: _ref1, ...rest } = itemAria.menuItemProps as Record<string, unknown>;
829
+ const {
830
+ ref: _ref1,
831
+ 'aria-describedby': _ariaDescribedby,
832
+ ...rest
833
+ } = itemAria.menuItemProps as Record<string, unknown>;
834
+ if (!hasPrimitiveLabel() && rest['aria-label'] == null) {
835
+ delete rest['aria-labelledby'];
836
+ }
519
837
  return rest;
520
838
  };
521
839
  const cleanHoverProps = () => {
522
840
  const { ref: _ref2, ...rest } = hoverProps as Record<string, unknown>;
523
841
  return rest;
524
842
  };
843
+ const draggableItem = createMemo(() => {
844
+ if (!menuContext?.dragAndDropHooks?.useDraggableItem || !menuContext.dragState) return undefined;
845
+ return menuContext.dragAndDropHooks.useDraggableItem(
846
+ {
847
+ key: local.id as string | number,
848
+ },
849
+ menuContext.dragState as Parameters<NonNullable<DragAndDropHooks<T>['useDraggableItem']>>[1]
850
+ );
851
+ });
852
+ const droppableItem = createMemo(() => {
853
+ if (!menuContext?.dragAndDropHooks?.useDroppableItem || !menuContext.dropState) return undefined;
854
+ return menuContext.dragAndDropHooks.useDroppableItem(
855
+ {
856
+ key: local.id as string | number,
857
+ },
858
+ menuContext.dropState as Parameters<NonNullable<DragAndDropHooks<T>['useDroppableItem']>>[1],
859
+ () => ref()
860
+ );
861
+ });
525
862
 
526
863
  return (
527
864
  <li
528
- {...cleanItemProps()}
529
- {...cleanHoverProps()}
865
+ ref={setRef}
866
+ {...mergeProps(
867
+ cleanItemProps(),
868
+ cleanHoverProps(),
869
+ (draggableItem()?.dragProps as Record<string, unknown> | undefined) ?? {},
870
+ (droppableItem()?.dropProps as Record<string, unknown> | undefined) ?? {}
871
+ )}
530
872
  class={renderProps.class()}
531
873
  style={renderProps.style()}
532
874
  data-focused={itemAria.isFocused() || undefined}
@@ -534,11 +876,22 @@ export function MenuItem<T>(props: MenuItemProps<T>): JSX.Element {
534
876
  data-pressed={itemAria.isPressed() || undefined}
535
877
  data-hovered={isHovered() || undefined}
536
878
  data-disabled={itemAria.isDisabled() || undefined}
879
+ data-dragging={draggableItem()?.isDragging || undefined}
880
+ data-drop-target={droppableItem()?.isDropTarget || undefined}
537
881
  >
538
- {renderProps.renderChildren()}
882
+ {hasPrimitiveLabel()
883
+ ? <span {...itemAria.labelProps}>{renderProps.renderChildren()}</span>
884
+ : renderProps.renderChildren()}
539
885
  </li>
540
886
  );
541
887
  }
542
888
 
889
+ /**
890
+ * Section primitive alias for Menu composition parity.
891
+ */
892
+ export function MenuSection(props: MenuSectionProps): JSX.Element {
893
+ return <Section {...props} />;
894
+ }
895
+
543
896
  // Attach Item as a static property
544
897
  Menu.Item = MenuItem;
package/src/Meter.tsx CHANGED
@@ -10,7 +10,6 @@
10
10
 
11
11
  import {
12
12
  type JSX,
13
- type ParentProps,
14
13
  createContext,
15
14
  createMemo,
16
15
  splitProps,
@@ -64,6 +63,11 @@ function clamp(value: number, min: number, max: number): number {
64
63
  return Math.min(Math.max(value, min), max);
65
64
  }
66
65
 
66
+ function getSafeRange(min: number, max: number): number {
67
+ const range = max - min;
68
+ return Number.isFinite(range) && range > 0 ? range : 1;
69
+ }
70
+
67
71
  // ============================================
68
72
  // METER COMPONENT
69
73
  // ============================================
@@ -85,7 +89,7 @@ function clamp(value: number, min: number, max: number): number {
85
89
  * </Meter>
86
90
  * ```
87
91
  */
88
- export function Meter(props: ParentProps<MeterProps>): JSX.Element {
92
+ export function Meter(props: MeterProps): JSX.Element {
89
93
  const [local, ariaProps] = splitProps(props, [
90
94
  'children',
91
95
  'class',
@@ -115,7 +119,7 @@ export function Meter(props: ParentProps<MeterProps>): JSX.Element {
115
119
  // Calculate percentage
116
120
  const percentage = createMemo(() => {
117
121
  const clampedValue = clamp(value(), minValue(), maxValue());
118
- return ((clampedValue - minValue()) / (maxValue() - minValue())) * 100;
122
+ return ((clampedValue - minValue()) / getSafeRange(minValue(), maxValue())) * 100;
119
123
  });
120
124
 
121
125
  // Get value text from aria props