@proyecto-viviana/solidaria 0.2.4 → 0.2.8

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 (219) hide show
  1. package/LICENSE +21 -0
  2. package/dist/actiongroup/createActionGroup.d.ts +29 -0
  3. package/dist/actiongroup/createActionGroup.d.ts.map +1 -0
  4. package/dist/actiongroup/index.d.ts +2 -0
  5. package/dist/actiongroup/index.d.ts.map +1 -0
  6. package/dist/autocomplete/createAutocomplete.d.ts +6 -2
  7. package/dist/autocomplete/createAutocomplete.d.ts.map +1 -1
  8. package/dist/breadcrumbs/createBreadcrumbs.d.ts +2 -0
  9. package/dist/breadcrumbs/createBreadcrumbs.d.ts.map +1 -1
  10. package/dist/button/createToggleButtonGroup.d.ts +32 -0
  11. package/dist/button/createToggleButtonGroup.d.ts.map +1 -0
  12. package/dist/button/index.d.ts +2 -0
  13. package/dist/button/index.d.ts.map +1 -1
  14. package/dist/calendar/createCalendarCell.d.ts +2 -0
  15. package/dist/calendar/createCalendarCell.d.ts.map +1 -1
  16. package/dist/calendar/createCalendarGrid.d.ts.map +1 -1
  17. package/dist/calendar/createRangeCalendarCell.d.ts +3 -1
  18. package/dist/calendar/createRangeCalendarCell.d.ts.map +1 -1
  19. package/dist/checkbox/createCheckboxGroup.d.ts +5 -1
  20. package/dist/checkbox/createCheckboxGroup.d.ts.map +1 -1
  21. package/dist/collections/index.d.ts +56 -0
  22. package/dist/collections/index.d.ts.map +1 -0
  23. package/dist/color/createColorArea.d.ts.map +1 -1
  24. package/dist/color/createColorSlider.d.ts.map +1 -1
  25. package/dist/color/createColorWheel.d.ts.map +1 -1
  26. package/dist/combobox/createComboBox.d.ts +6 -0
  27. package/dist/combobox/createComboBox.d.ts.map +1 -1
  28. package/dist/datepicker/createDatePicker.d.ts +6 -0
  29. package/dist/datepicker/createDatePicker.d.ts.map +1 -1
  30. package/dist/datepicker/createDateRangePicker.d.ts +40 -0
  31. package/dist/datepicker/createDateRangePicker.d.ts.map +1 -0
  32. package/dist/datepicker/createDateSegment.d.ts +1 -1
  33. package/dist/datepicker/createDateSegment.d.ts.map +1 -1
  34. package/dist/datepicker/createTimeSegment.d.ts +29 -0
  35. package/dist/datepicker/createTimeSegment.d.ts.map +1 -0
  36. package/dist/datepicker/index.d.ts +2 -0
  37. package/dist/datepicker/index.d.ts.map +1 -1
  38. package/dist/disclosure/createDisclosureGroup.d.ts +2 -1
  39. package/dist/disclosure/createDisclosureGroup.d.ts.map +1 -1
  40. package/dist/dnd/createDrag.d.ts.map +1 -1
  41. package/dist/dnd/createDraggableCollection.d.ts +4 -0
  42. package/dist/dnd/createDraggableCollection.d.ts.map +1 -1
  43. package/dist/dnd/createDraggableItem.d.ts.map +1 -1
  44. package/dist/dnd/createDrop.d.ts.map +1 -1
  45. package/dist/dnd/createDroppableCollection.d.ts +32 -1
  46. package/dist/dnd/createDroppableCollection.d.ts.map +1 -1
  47. package/dist/dnd/createDroppableItem.d.ts.map +1 -1
  48. package/dist/dnd/index.d.ts +1 -1
  49. package/dist/dnd/index.d.ts.map +1 -1
  50. package/dist/grid/createGrid.d.ts.map +1 -1
  51. package/dist/gridlist/createGridList.d.ts.map +1 -1
  52. package/dist/index.d.ts +6 -4
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +4659 -3452
  55. package/dist/index.js.map +1 -7
  56. package/dist/index.ssr.js +4659 -3452
  57. package/dist/index.ssr.js.map +1 -7
  58. package/dist/interactions/createFocus.d.ts.map +1 -1
  59. package/dist/interactions/createFocusWithin.d.ts.map +1 -1
  60. package/dist/link/createLink.d.ts +10 -0
  61. package/dist/link/createLink.d.ts.map +1 -1
  62. package/dist/listbox/createListBox.d.ts +1 -0
  63. package/dist/listbox/createListBox.d.ts.map +1 -1
  64. package/dist/listbox/createOption.d.ts.map +1 -1
  65. package/dist/menu/createMenu.d.ts +1 -0
  66. package/dist/menu/createMenu.d.ts.map +1 -1
  67. package/dist/meter/createMeter.d.ts.map +1 -1
  68. package/dist/numberfield/createNumberField.d.ts +18 -0
  69. package/dist/numberfield/createNumberField.d.ts.map +1 -1
  70. package/dist/overlays/createModal.d.ts +16 -0
  71. package/dist/overlays/createModal.d.ts.map +1 -1
  72. package/dist/overlays/createOverlay.d.ts.map +1 -1
  73. package/dist/overlays/index.d.ts +1 -1
  74. package/dist/overlays/index.d.ts.map +1 -1
  75. package/dist/popover/createOverlayPosition.d.ts.map +1 -1
  76. package/dist/popover/createPopover.d.ts.map +1 -1
  77. package/dist/progress/createProgressBar.d.ts.map +1 -1
  78. package/dist/radio/createRadioGroup.d.ts +2 -2
  79. package/dist/radio/createRadioGroup.d.ts.map +1 -1
  80. package/dist/searchfield/createSearchField.d.ts.map +1 -1
  81. package/dist/select/createHiddenSelect.d.ts.map +1 -1
  82. package/dist/select/createSelect.d.ts.map +1 -1
  83. package/dist/slider/createSlider.d.ts.map +1 -1
  84. package/dist/table/createTable.d.ts.map +1 -1
  85. package/dist/tabs/createTabs.d.ts +1 -1
  86. package/dist/tabs/createTabs.d.ts.map +1 -1
  87. package/dist/tag/createTag.d.ts.map +1 -1
  88. package/dist/tag/createTagGroup.d.ts.map +1 -1
  89. package/dist/toast/createToast.d.ts +4 -0
  90. package/dist/toast/createToast.d.ts.map +1 -1
  91. package/dist/toast/createToastRegion.d.ts.map +1 -1
  92. package/dist/toolbar/createToolbar.d.ts.map +1 -1
  93. package/dist/tooltip/createTooltipTrigger.d.ts.map +1 -1
  94. package/dist/tree/createTree.d.ts.map +1 -1
  95. package/dist/tree/createTreeItem.d.ts.map +1 -1
  96. package/dist/tree/types.d.ts +4 -0
  97. package/dist/tree/types.d.ts.map +1 -1
  98. package/dist/utils/env.d.ts +1 -1
  99. package/dist/utils/env.d.ts.map +1 -1
  100. package/dist/utils/platform.d.ts.map +1 -1
  101. package/dist/visually-hidden/createVisuallyHidden.d.ts.map +1 -1
  102. package/package.json +8 -6
  103. package/src/actiongroup/createActionGroup.ts +324 -0
  104. package/src/actiongroup/index.ts +8 -0
  105. package/src/autocomplete/createAutocomplete.ts +32 -9
  106. package/src/breadcrumbs/createBreadcrumbs.ts +10 -15
  107. package/src/button/createButton.ts +1 -1
  108. package/src/button/createToggleButtonGroup.ts +128 -0
  109. package/src/button/index.ts +9 -0
  110. package/src/calendar/createCalendarCell.ts +6 -4
  111. package/src/calendar/createCalendarGrid.ts +27 -18
  112. package/src/calendar/createRangeCalendarCell.ts +26 -9
  113. package/src/checkbox/createCheckboxGroup.ts +21 -4
  114. package/src/collections/index.ts +242 -0
  115. package/src/color/createColorArea.ts +380 -314
  116. package/src/color/createColorField.ts +137 -137
  117. package/src/color/createColorSlider.ts +286 -197
  118. package/src/color/createColorSwatch.ts +40 -40
  119. package/src/color/createColorWheel.ts +218 -208
  120. package/src/color/index.ts +24 -24
  121. package/src/color/types.ts +116 -116
  122. package/src/combobox/createComboBox.ts +670 -647
  123. package/src/combobox/index.ts +6 -6
  124. package/src/datepicker/createDatePicker.ts +54 -16
  125. package/src/datepicker/createDateRangePicker.ts +246 -0
  126. package/src/datepicker/createDateSegment.ts +185 -31
  127. package/src/datepicker/createTimeSegment.ts +370 -0
  128. package/src/datepicker/index.ts +14 -0
  129. package/src/dialog/createDialog.ts +120 -120
  130. package/src/dialog/index.ts +2 -2
  131. package/src/dialog/types.ts +19 -19
  132. package/src/disclosure/createDisclosureGroup.ts +5 -2
  133. package/src/dnd/createDrag.ts +224 -209
  134. package/src/dnd/createDraggableCollection.ts +96 -63
  135. package/src/dnd/createDraggableItem.ts +259 -243
  136. package/src/dnd/createDrop.ts +322 -321
  137. package/src/dnd/createDroppableCollection.ts +682 -293
  138. package/src/dnd/createDroppableItem.ts +215 -213
  139. package/src/dnd/index.ts +55 -47
  140. package/src/dnd/types.ts +89 -89
  141. package/src/dnd/utils.ts +294 -294
  142. package/src/focus/createAutoFocus.ts +321 -321
  143. package/src/focus/createFocusRestore.ts +313 -313
  144. package/src/focus/createVirtualFocus.ts +396 -396
  145. package/src/form/createFormValidation.ts +224 -224
  146. package/src/form/index.ts +11 -11
  147. package/src/grid/createGrid.ts +3 -1
  148. package/src/gridlist/createGridList.ts +16 -0
  149. package/src/gridlist/createGridListItem.ts +1 -1
  150. package/src/i18n/NumberFormatter.ts +266 -266
  151. package/src/i18n/createCollator.ts +79 -79
  152. package/src/i18n/createDateFormatter.ts +83 -83
  153. package/src/i18n/createFilter.ts +131 -131
  154. package/src/i18n/createNumberFormatter.ts +52 -52
  155. package/src/i18n/index.ts +40 -40
  156. package/src/i18n/locale.tsx +188 -188
  157. package/src/i18n/utils.ts +99 -99
  158. package/src/index.ts +51 -0
  159. package/src/interactions/createFocus.ts +6 -5
  160. package/src/interactions/createFocusWithin.ts +6 -5
  161. package/src/interactions/createLongPress.ts +174 -174
  162. package/src/interactions/createMove.ts +289 -289
  163. package/src/interactions/createPress.ts +5 -5
  164. package/src/landmark/createLandmark.ts +377 -377
  165. package/src/landmark/index.ts +8 -8
  166. package/src/link/createLink.ts +23 -8
  167. package/src/listbox/createListBox.ts +308 -269
  168. package/src/listbox/createOption.ts +162 -151
  169. package/src/listbox/index.ts +12 -12
  170. package/src/live-announcer/announce.ts +322 -322
  171. package/src/live-announcer/index.ts +9 -9
  172. package/src/menu/createMenu.ts +405 -396
  173. package/src/menu/createMenuItem.ts +149 -149
  174. package/src/menu/createMenuTrigger.ts +88 -88
  175. package/src/menu/index.ts +18 -18
  176. package/src/meter/createMeter.ts +1 -6
  177. package/src/numberfield/createNumberField.ts +311 -268
  178. package/src/numberfield/index.ts +5 -5
  179. package/src/overlays/ariaHideOutside.ts +219 -219
  180. package/src/overlays/createInteractOutside.ts +149 -149
  181. package/src/overlays/createModal.tsx +238 -202
  182. package/src/overlays/createOverlay.ts +165 -155
  183. package/src/overlays/createOverlayTrigger.ts +85 -85
  184. package/src/overlays/createPreventScroll.ts +266 -266
  185. package/src/overlays/index.ts +48 -44
  186. package/src/popover/calculatePosition.ts +6 -6
  187. package/src/popover/createOverlayPosition.ts +7 -4
  188. package/src/popover/createPopover.ts +21 -7
  189. package/src/progress/createProgressBar.ts +6 -1
  190. package/src/radio/createRadioGroup.ts +88 -14
  191. package/src/searchfield/createSearchField.ts +241 -186
  192. package/src/searchfield/index.ts +2 -2
  193. package/src/select/createHiddenSelect.tsx +263 -236
  194. package/src/select/createSelect.ts +373 -395
  195. package/src/select/index.ts +14 -14
  196. package/src/slider/createSlider.ts +364 -349
  197. package/src/slider/index.ts +2 -2
  198. package/src/ssr/index.tsx +370 -370
  199. package/src/table/createTable.ts +3 -1
  200. package/src/table/createTableColumnHeader.ts +1 -1
  201. package/src/table/createTableRow.ts +1 -1
  202. package/src/tabs/createTabs.ts +80 -51
  203. package/src/tag/createTag.ts +135 -6
  204. package/src/tag/createTagGroup.ts +7 -2
  205. package/src/toast/createToast.ts +8 -2
  206. package/src/toast/createToastRegion.ts +0 -1
  207. package/src/toolbar/createToolbar.ts +75 -1
  208. package/src/tooltip/createTooltip.ts +79 -79
  209. package/src/tooltip/createTooltipTrigger.ts +226 -222
  210. package/src/tooltip/index.ts +6 -6
  211. package/src/tree/createTree.ts +261 -246
  212. package/src/tree/createTreeItem.ts +282 -233
  213. package/src/tree/createTreeSelectionCheckbox.ts +68 -68
  214. package/src/tree/index.ts +16 -16
  215. package/src/tree/types.ts +91 -87
  216. package/src/utils/env.ts +55 -54
  217. package/src/utils/platform.ts +16 -6
  218. package/src/visually-hidden/createVisuallyHidden.ts +139 -124
  219. package/src/visually-hidden/index.ts +6 -6
@@ -5,7 +5,7 @@
5
5
  * Based on @react-aria/calendar useCalendarGrid
6
6
  */
7
7
 
8
- import { createMemo, onMount, onCleanup } from 'solid-js';
8
+ import { createMemo } from 'solid-js';
9
9
  import { type MaybeAccessor } from '../utils/reactivity';
10
10
  import type { CalendarState, CalendarDate } from '@proyecto-viviana/solid-stately';
11
11
 
@@ -47,17 +47,32 @@ export function createCalendarGrid<T extends CalendarState>(
47
47
  const weekDays = createMemo(() => state.weekDays());
48
48
 
49
49
  // Handle keyboard navigation
50
+ const isRTL = (): boolean => {
51
+ const element = ref?.();
52
+ const scopedDirection = element?.closest('[dir]')?.getAttribute('dir');
53
+ const documentDirection = typeof document !== 'undefined' ? document.dir : '';
54
+ return (scopedDirection ?? documentDirection) === 'rtl';
55
+ };
56
+
50
57
  const handleKeyDown = (e: KeyboardEvent) => {
51
58
  if (state.isDisabled()) return;
52
59
 
53
60
  switch (e.key) {
54
61
  case 'ArrowLeft':
55
62
  e.preventDefault();
56
- state.focusPreviousDay();
63
+ if (isRTL()) {
64
+ state.focusNextDay();
65
+ } else {
66
+ state.focusPreviousDay();
67
+ }
57
68
  break;
58
69
  case 'ArrowRight':
59
70
  e.preventDefault();
60
- state.focusNextDay();
71
+ if (isRTL()) {
72
+ state.focusPreviousDay();
73
+ } else {
74
+ state.focusNextDay();
75
+ }
61
76
  break;
62
77
  case 'ArrowUp':
63
78
  e.preventDefault();
@@ -96,20 +111,15 @@ export function createCalendarGrid<T extends CalendarState>(
96
111
  e.preventDefault();
97
112
  state.selectFocusedDate();
98
113
  break;
114
+ case 'Escape':
115
+ if ('setAnchorDate' in state && typeof state.setAnchorDate === 'function') {
116
+ e.preventDefault();
117
+ state.setAnchorDate(null);
118
+ }
119
+ break;
99
120
  }
100
121
  };
101
122
 
102
- // Register keyboard listener
103
- onMount(() => {
104
- const element = ref?.();
105
- if (element) {
106
- element.addEventListener('keydown', handleKeyDown);
107
- onCleanup(() => {
108
- element.removeEventListener('keydown', handleKeyDown);
109
- });
110
- }
111
- });
112
-
113
123
  // Grid props
114
124
  const gridProps = createMemo(() => ({
115
125
  role: 'grid',
@@ -121,10 +131,9 @@ export function createCalendarGrid<T extends CalendarState>(
121
131
  onKeyDown: handleKeyDown,
122
132
  }));
123
133
 
124
- // Header props
125
- const headerProps = createMemo(() => ({
126
- role: 'row',
127
- }));
134
+ // Header props are intentionally empty. Consumers render this on <thead>,
135
+ // which already has correct table semantics.
136
+ const headerProps = createMemo(() => ({}));
128
137
 
129
138
  return {
130
139
  get gridProps() {
@@ -5,7 +5,7 @@
5
5
  * Based on @react-aria/calendar useCalendarCell (with range support)
6
6
  */
7
7
 
8
- import { createSignal, createMemo } from 'solid-js';
8
+ import { createSignal, createMemo, createEffect } from 'solid-js';
9
9
  import { access, type MaybeAccessor } from '../utils/reactivity';
10
10
  import type { RangeCalendarState, CalendarDate, DateValue } from '@proyecto-viviana/solid-stately';
11
11
  import { isToday as isTodayUtil, DateFormatter, getLocalTimeZone } from '@internationalized/date';
@@ -19,6 +19,8 @@ export interface AriaRangeCalendarCellProps {
19
19
  date: DateValue;
20
20
  /** Whether the cell is disabled. */
21
21
  isDisabled?: boolean;
22
+ /** Whether the date is outside the current month grid. */
23
+ isOutsideMonth?: boolean;
22
24
  }
23
25
 
24
26
  export interface RangeCalendarCellAria {
@@ -58,7 +60,7 @@ export interface RangeCalendarCellAria {
58
60
  export function createRangeCalendarCell<T extends RangeCalendarState>(
59
61
  props: MaybeAccessor<AriaRangeCalendarCellProps>,
60
62
  state: T,
61
- _ref?: () => HTMLElement | null
63
+ ref?: () => HTMLElement | null
62
64
  ): RangeCalendarCellAria {
63
65
  const getProps = () => access(props);
64
66
  const [isPressed, setIsPressed] = createSignal(false);
@@ -76,7 +78,9 @@ export function createRangeCalendarCell<T extends RangeCalendarState>(
76
78
  return getProps().isDisabled || state.isCellDisabled(date());
77
79
  });
78
80
  const isUnavailable = createMemo(() => state.isCellUnavailable(date()));
79
- const isOutsideMonth = createMemo(() => state.isOutsideVisibleRange(date()));
81
+ const isOutsideMonth = createMemo(() => {
82
+ return getProps().isOutsideMonth ?? state.isOutsideVisibleRange(date());
83
+ });
80
84
  const isToday = createMemo(() => isTodayUtil(date(), timeZone));
81
85
 
82
86
  // Format the date for display
@@ -84,17 +88,20 @@ export function createRangeCalendarCell<T extends RangeCalendarState>(
84
88
  return date().day.toString();
85
89
  });
86
90
 
87
- // Handle click
88
- const handleClick = () => {
91
+ // Handle pointer down - selection on pointerdown avoids losing selection when
92
+ // hover/focus updates re-render cells before click fires.
93
+ const handlePointerDown = (e: PointerEvent) => {
89
94
  if (!isDisabled() && !isUnavailable()) {
95
+ setIsPressed(true);
90
96
  state.selectDate(date());
97
+ e.preventDefault();
91
98
  }
92
99
  };
93
100
 
94
- // Handle pointer events for pressed state
95
- const handlePointerDown = () => {
101
+ // Handle click for keyboard activation (Enter/Space).
102
+ const handleClick = () => {
96
103
  if (!isDisabled() && !isUnavailable()) {
97
- setIsPressed(true);
104
+ state.selectDate(date());
98
105
  }
99
106
  };
100
107
 
@@ -109,6 +116,14 @@ export function createRangeCalendarCell<T extends RangeCalendarState>(
109
116
  }
110
117
  };
111
118
 
119
+ // Keep DOM focus synchronized with focused date updates.
120
+ createEffect(() => {
121
+ const element = ref?.();
122
+ if (element && isFocused()) {
123
+ element.focus();
124
+ }
125
+ });
126
+
112
127
  // Cell props (for the td element)
113
128
  const cellProps = createMemo(() => ({
114
129
  role: 'gridcell',
@@ -139,7 +154,9 @@ export function createRangeCalendarCell<T extends RangeCalendarState>(
139
154
  onPointerLeave: handlePointerUp,
140
155
  onPointerEnter: handlePointerEnter,
141
156
  onFocus: () => {
142
- state.setFocusedDate(d);
157
+ if (!state.isCellFocused(d)) {
158
+ state.setFocusedDate(d);
159
+ }
143
160
  state.setFocused(true);
144
161
  },
145
162
  };
@@ -13,7 +13,7 @@ import { createFocusWithin } from '../interactions/createFocusWithin';
13
13
  import { filterDOMProps } from '../utils/filterDOMProps';
14
14
  import { mergeProps } from '../utils/mergeProps';
15
15
  import { type MaybeAccessor, access } from '../utils/reactivity';
16
- import { type CheckboxGroupState, type CheckboxGroupProps } from '@proyecto-viviana/solid-stately';
16
+ import { type CheckboxGroupState, type CheckboxGroupProps, type ValidityState } from '@proyecto-viviana/solid-stately';
17
17
 
18
18
  // ============================================
19
19
  // TYPES
@@ -45,6 +45,10 @@ export interface CheckboxGroupAria {
45
45
  errorMessageProps: JSX.HTMLAttributes<HTMLElement>;
46
46
  /** Whether the checkbox group is invalid. */
47
47
  isInvalid: boolean;
48
+ /** Validation errors, if any. */
49
+ validationErrors: string[];
50
+ /** Validation details, if any. */
51
+ validationDetails: ValidityState;
48
52
  }
49
53
 
50
54
  // WeakMap to share data between checkbox group and checkbox group items
@@ -75,8 +79,15 @@ export function createCheckboxGroup(
75
79
  state: CheckboxGroupState
76
80
  ): CheckboxGroupAria {
77
81
  const getProps = () => access(props);
82
+ const displayValidation = () => state.displayValidation();
83
+ const validationErrors = () => displayValidation().validationErrors;
84
+ const validationDetails = () => displayValidation().validationDetails;
78
85
 
79
- const isInvalid = () => state.isInvalid;
86
+ const isInvalid = () => displayValidation().isInvalid;
87
+ const fallbackErrorMessage = () => {
88
+ const errors = validationErrors();
89
+ return errors.length > 0 ? errors : undefined;
90
+ };
80
91
 
81
92
  // Use field for label association
82
93
  const { labelProps, fieldProps, descriptionProps, errorMessageProps } = createField({
@@ -86,7 +97,7 @@ export function createCheckboxGroup(
86
97
  get 'aria-describedby'() { return getProps()['aria-describedby']; },
87
98
  get 'aria-details'() { return getProps()['aria-details']; },
88
99
  get description() { return getProps().description; },
89
- get errorMessage() { return getProps().errorMessage ?? (isInvalid() ? 'Invalid selection' : undefined); },
100
+ get errorMessage() { return getProps().errorMessage ?? fallbackErrorMessage(); },
90
101
  get isInvalid() { return isInvalid(); },
91
102
  // Checkbox group is not an HTML input element so it
92
103
  // shouldn't be labeled by a <label> element.
@@ -99,7 +110,7 @@ export function createCheckboxGroup(
99
110
  form: getProps().form,
100
111
  descriptionId: descriptionProps.id,
101
112
  errorMessageId: errorMessageProps.id,
102
- validationBehavior: 'aria',
113
+ validationBehavior: getProps().validationBehavior ?? 'aria',
103
114
  });
104
115
 
105
116
  // Filter DOM props
@@ -133,5 +144,11 @@ export function createCheckboxGroup(
133
144
  get isInvalid() {
134
145
  return isInvalid();
135
146
  },
147
+ get validationErrors() {
148
+ return validationErrors();
149
+ },
150
+ get validationDetails() {
151
+ return validationDetails();
152
+ },
136
153
  };
137
154
  }
@@ -0,0 +1,242 @@
1
+ import { createContext, createMemo, useContext, type Accessor } from 'solid-js';
2
+ import { access, type MaybeAccessor } from '../utils';
3
+ import {
4
+ ListCollection,
5
+ type Collection as StatelyCollection,
6
+ type CollectionNode as StatelyCollectionNode,
7
+ type Key,
8
+ } from '@proyecto-viviana/solid-stately';
9
+
10
+ export interface CachedChildrenOptions<T> {
11
+ items?: Iterable<T>;
12
+ children?: ((item: T) => unknown) | unknown;
13
+ dependencies?: ReadonlyArray<unknown>;
14
+ getKey?: (item: T) => Key;
15
+ idScope?: Key;
16
+ addIdAndValue?: boolean;
17
+ }
18
+
19
+ export interface CollectionBuilderProps<T> extends CachedChildrenOptions<T> {}
20
+
21
+ export interface CollectionProps<T> extends CollectionBuilderProps<T> {}
22
+
23
+ export interface CollectionCompatNode<TProps = unknown, TValue = unknown> {
24
+ key: Key;
25
+ value: TValue;
26
+ props: TProps;
27
+ }
28
+
29
+ const COLLECTION_NODE_PROP = '__collectionNode';
30
+
31
+ function applyCollectionMetadata<T>(
32
+ rendered: unknown,
33
+ item: T,
34
+ key: Key,
35
+ addIdAndValue: boolean
36
+ ): unknown {
37
+ if (typeof rendered !== 'object' || rendered === null) return rendered;
38
+ const next = {
39
+ ...(rendered as Record<string, unknown>),
40
+ key,
41
+ [COLLECTION_NODE_PROP]: {
42
+ key,
43
+ value: item,
44
+ props: rendered as Record<string, unknown>,
45
+ } satisfies CollectionCompatNode<Record<string, unknown>, T>,
46
+ } as Record<string, unknown>;
47
+ if (addIdAndValue) {
48
+ next.id = key;
49
+ next.value = item;
50
+ }
51
+ return next;
52
+ }
53
+
54
+ /**
55
+ * Compatibility helper mirroring React Aria collections API shape.
56
+ * For Solid, this is a lightweight mapper over item arrays.
57
+ */
58
+ export function CollectionBuilder<T>(props: CollectionBuilderProps<T>): unknown {
59
+ if (typeof props.children === 'function' && props.items) {
60
+ const children = props.children as (item: T) => unknown;
61
+ const mapped: unknown[] = [];
62
+ let index = 0;
63
+
64
+ for (const item of props.items) {
65
+ const baseKey = getResolvedItemKey(item, index, props.getKey);
66
+ if (baseKey == null) {
67
+ throw new Error('Could not determine key for item');
68
+ }
69
+ const key = props.idScope != null ? `${String(props.idScope)}:${String(baseKey)}` : baseKey;
70
+ const rendered = children(item);
71
+ const withMeta = applyCollectionMetadata(
72
+ rendered,
73
+ item,
74
+ key,
75
+ props.addIdAndValue ?? false
76
+ );
77
+ mapped.push(withMeta);
78
+ index += 1;
79
+ }
80
+
81
+ return mapped;
82
+ }
83
+ return props.children ?? null;
84
+ }
85
+
86
+ export function Collection<T>(props: CollectionProps<T>): unknown {
87
+ return CollectionBuilder({
88
+ ...props,
89
+ addIdAndValue: props.addIdAndValue ?? true,
90
+ });
91
+ }
92
+
93
+ /**
94
+ * Identity helper retained for API compatibility.
95
+ */
96
+ export function createLeafComponent<TProps>(
97
+ component: (props: TProps, node?: CollectionCompatNode<TProps, unknown>) => unknown
98
+ ): (props: TProps) => unknown {
99
+ return (props: TProps) => {
100
+ const node = (props as Record<string, unknown>)[COLLECTION_NODE_PROP] as CollectionCompatNode<TProps, unknown> | undefined;
101
+ if (component.length >= 2 && !node) {
102
+ throw new Error(`${component.name || 'Component'} cannot be rendered outside a collection.`);
103
+ }
104
+ return component(props, node);
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Identity helper retained for API compatibility.
110
+ */
111
+ export function createBranchComponent<TProps>(
112
+ component: (props: TProps, node?: CollectionCompatNode<TProps, unknown>) => unknown
113
+ ): (props: TProps) => unknown {
114
+ return (props: TProps) => {
115
+ const node = (props as Record<string, unknown>)[COLLECTION_NODE_PROP] as CollectionCompatNode<TProps, unknown> | undefined;
116
+ if (component.length >= 2 && !node) {
117
+ throw new Error(`${component.name || 'Component'} cannot be rendered outside a collection.`);
118
+ }
119
+ return component(props, node);
120
+ };
121
+ }
122
+
123
+ const HiddenContext = createContext<Accessor<boolean>>(() => false);
124
+
125
+ /**
126
+ * Wraps a component and suppresses rendering when the hidden context is true.
127
+ */
128
+ export function createHideableComponent<TProps>(component: (props: TProps) => unknown): (props: TProps) => unknown {
129
+ return (props: TProps) => {
130
+ const isHidden = useIsHidden();
131
+ if (isHidden()) return null;
132
+ return component(props);
133
+ };
134
+ }
135
+
136
+ export function useIsHidden(): Accessor<boolean> {
137
+ return useContext(HiddenContext) ?? (() => false);
138
+ }
139
+
140
+ /**
141
+ * Memoized item renderer for dynamic child mapping.
142
+ */
143
+ export function useCachedChildren<T>(options: MaybeAccessor<CachedChildrenOptions<T>>): Accessor<unknown[]> {
144
+ let objectCache = new WeakMap<object, unknown>();
145
+ const primitiveCache = new Map<Key, unknown>();
146
+ let lastDependencies: ReadonlyArray<unknown> | undefined;
147
+ let lastIdScope: Key | undefined;
148
+ let lastAddIdAndValue: boolean | undefined;
149
+ let lastGetKey: ((item: T) => Key) | undefined;
150
+
151
+ const clearCaches = (): void => {
152
+ objectCache = new WeakMap<object, unknown>();
153
+ primitiveCache.clear();
154
+ };
155
+
156
+ const getItemKey = (item: T, index: number, getKey?: (item: T) => Key): Key | undefined => {
157
+ return getResolvedItemKey(item, index, getKey);
158
+ };
159
+
160
+ return createMemo(() => {
161
+ const resolved = access(options);
162
+ const resolvedAddIdAndValue = resolved.addIdAndValue ?? false;
163
+ const resolvedGetKey = resolved.getKey;
164
+ const resolvedIdScope = resolved.idScope;
165
+
166
+ if (resolved.dependencies && resolved.dependencies !== lastDependencies) {
167
+ clearCaches();
168
+ lastDependencies = resolved.dependencies;
169
+ }
170
+ if (
171
+ resolvedIdScope !== lastIdScope ||
172
+ resolvedAddIdAndValue !== lastAddIdAndValue ||
173
+ resolvedGetKey !== lastGetKey
174
+ ) {
175
+ clearCaches();
176
+ lastIdScope = resolvedIdScope;
177
+ lastAddIdAndValue = resolvedAddIdAndValue;
178
+ lastGetKey = resolvedGetKey;
179
+ }
180
+
181
+ if (typeof resolved.children === 'function' && resolved.items) {
182
+ const children = resolved.children as (item: T) => unknown;
183
+ const rendered: unknown[] = [];
184
+ let index = 0;
185
+
186
+ for (const item of resolved.items) {
187
+ const baseKey = getItemKey(item, index, resolvedGetKey);
188
+ if (baseKey == null) {
189
+ throw new Error('Could not determine key for item');
190
+ }
191
+ const key = resolvedIdScope != null ? `${String(resolvedIdScope)}:${String(baseKey)}` : baseKey;
192
+ let child: unknown;
193
+
194
+ if (typeof item === 'object' && item !== null) {
195
+ child = objectCache.get(item as object);
196
+ if (child === undefined) {
197
+ child = applyCollectionMetadata(children(item), item, key, resolvedAddIdAndValue);
198
+ objectCache.set(item as object, child);
199
+ }
200
+ } else {
201
+ if (primitiveCache.has(key)) {
202
+ child = primitiveCache.get(key);
203
+ } else {
204
+ child = applyCollectionMetadata(children(item), item, key, resolvedAddIdAndValue);
205
+ primitiveCache.set(key, child);
206
+ }
207
+ }
208
+
209
+ rendered.push(child);
210
+ index += 1;
211
+ }
212
+
213
+ return rendered;
214
+ }
215
+ const child = resolved.children;
216
+ return child == null ? [] : [child];
217
+ });
218
+ }
219
+
220
+ /**
221
+ * Minimal BaseCollection compatibility class.
222
+ */
223
+ export class BaseCollection<T = unknown> extends ListCollection<T> {}
224
+
225
+ export type CollectionNode<T = unknown> = StatelyCollectionNode<T>;
226
+ export type ItemNode<T = unknown> = StatelyCollectionNode<T>;
227
+ export type SectionNode<T = unknown> = StatelyCollectionNode<T>;
228
+ export type FilterableNode<T = unknown> = StatelyCollectionNode<T>;
229
+ export type LoaderNode<T = unknown> = StatelyCollectionNode<T>;
230
+ export type HeaderNode<T = unknown> = StatelyCollectionNode<T>;
231
+ export type CollectionType<T = unknown> = StatelyCollection<T>;
232
+
233
+ function getResolvedItemKey<T>(item: T, index: number, getKey?: (item: T) => Key): Key | undefined {
234
+ if (getKey) return getKey(item);
235
+ if (typeof item === 'object' && item !== null) {
236
+ const keyed = item as Record<string, unknown>;
237
+ const keyValue = keyed.key ?? keyed.id;
238
+ if (typeof keyValue === 'string' || typeof keyValue === 'number') return keyValue;
239
+ }
240
+ if (typeof item === 'string' || typeof item === 'number') return index;
241
+ return undefined;
242
+ }