@proyecto-viviana/solidaria-components 0.2.5 → 0.3.0

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 (225) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -272
  3. package/dist/ActionBar.d.ts +79 -0
  4. package/dist/ActionBar.d.ts.map +1 -0
  5. package/dist/ActionGroup.d.ts +74 -0
  6. package/dist/ActionGroup.d.ts.map +1 -0
  7. package/dist/Alert.d.ts +70 -0
  8. package/dist/Alert.d.ts.map +1 -0
  9. package/dist/Autocomplete.d.ts +5 -5
  10. package/dist/Autocomplete.d.ts.map +1 -1
  11. package/dist/Breadcrumbs.d.ts +27 -8
  12. package/dist/Breadcrumbs.d.ts.map +1 -1
  13. package/dist/Button.d.ts +28 -5
  14. package/dist/Button.d.ts.map +1 -1
  15. package/dist/Calendar.d.ts +51 -7
  16. package/dist/Calendar.d.ts.map +1 -1
  17. package/dist/Checkbox.d.ts +33 -8
  18. package/dist/Checkbox.d.ts.map +1 -1
  19. package/dist/Collection.d.ts +130 -0
  20. package/dist/Collection.d.ts.map +1 -0
  21. package/dist/Color.d.ts +210 -9
  22. package/dist/Color.d.ts.map +1 -1
  23. package/dist/ColorEditor.d.ts +42 -0
  24. package/dist/ColorEditor.d.ts.map +1 -0
  25. package/dist/ComboBox.d.ts +146 -16
  26. package/dist/ComboBox.d.ts.map +1 -1
  27. package/dist/ContextualHelpTrigger.d.ts +40 -0
  28. package/dist/ContextualHelpTrigger.d.ts.map +1 -0
  29. package/dist/DateField.d.ts +35 -8
  30. package/dist/DateField.d.ts.map +1 -1
  31. package/dist/DatePicker.d.ts +101 -5
  32. package/dist/DatePicker.d.ts.map +1 -1
  33. package/dist/DateRangePickerContext.d.ts +30 -0
  34. package/dist/DateRangePickerContext.d.ts.map +1 -0
  35. package/dist/Dialog.d.ts +5 -5
  36. package/dist/Dialog.d.ts.map +1 -1
  37. package/dist/Disclosure.d.ts +25 -5
  38. package/dist/Disclosure.d.ts.map +1 -1
  39. package/dist/DragAndDrop.d.ts +80 -0
  40. package/dist/DragAndDrop.d.ts.map +1 -0
  41. package/dist/DragPreview.d.ts +14 -0
  42. package/dist/DragPreview.d.ts.map +1 -0
  43. package/dist/DropZone.d.ts +27 -0
  44. package/dist/DropZone.d.ts.map +1 -0
  45. package/dist/FieldError.d.ts +27 -0
  46. package/dist/FieldError.d.ts.map +1 -0
  47. package/dist/FileTrigger.d.ts +26 -0
  48. package/dist/FileTrigger.d.ts.map +1 -0
  49. package/dist/Focusable.d.ts +27 -0
  50. package/dist/Focusable.d.ts.map +1 -0
  51. package/dist/Form.d.ts +41 -0
  52. package/dist/Form.d.ts.map +1 -0
  53. package/dist/GridList.d.ts +69 -10
  54. package/dist/GridList.d.ts.map +1 -1
  55. package/dist/HiddenDateInput.d.ts +26 -0
  56. package/dist/HiddenDateInput.d.ts.map +1 -0
  57. package/dist/HiddenTimeInput.d.ts +25 -0
  58. package/dist/HiddenTimeInput.d.ts.map +1 -0
  59. package/dist/Icon.d.ts +57 -0
  60. package/dist/Icon.d.ts.map +1 -0
  61. package/dist/Keyboard.d.ts +13 -0
  62. package/dist/Keyboard.d.ts.map +1 -0
  63. package/dist/Landmark.d.ts +3 -3
  64. package/dist/Landmark.d.ts.map +1 -1
  65. package/dist/Link.d.ts +10 -4
  66. package/dist/Link.d.ts.map +1 -1
  67. package/dist/ListBox.d.ts +73 -11
  68. package/dist/ListBox.d.ts.map +1 -1
  69. package/dist/ListDropTargetDelegate.d.ts +38 -0
  70. package/dist/ListDropTargetDelegate.d.ts.map +1 -0
  71. package/dist/Menu.d.ts +79 -10
  72. package/dist/Menu.d.ts.map +1 -1
  73. package/dist/Meter.d.ts +4 -4
  74. package/dist/Meter.d.ts.map +1 -1
  75. package/dist/Modal.d.ts +6 -4
  76. package/dist/Modal.d.ts.map +1 -1
  77. package/dist/NumberField.d.ts +10 -12
  78. package/dist/NumberField.d.ts.map +1 -1
  79. package/dist/Popover.d.ts +32 -7
  80. package/dist/Popover.d.ts.map +1 -1
  81. package/dist/Pressable.d.ts +27 -0
  82. package/dist/Pressable.d.ts.map +1 -0
  83. package/dist/ProgressBar.d.ts +6 -4
  84. package/dist/ProgressBar.d.ts.map +1 -1
  85. package/dist/RadioGroup.d.ts +43 -9
  86. package/dist/RadioGroup.d.ts.map +1 -1
  87. package/dist/RangeCalendar.d.ts +39 -7
  88. package/dist/RangeCalendar.d.ts.map +1 -1
  89. package/dist/RouterProvider.d.ts +75 -0
  90. package/dist/RouterProvider.d.ts.map +1 -0
  91. package/dist/SearchField.d.ts +23 -21
  92. package/dist/SearchField.d.ts.map +1 -1
  93. package/dist/Select.d.ts +48 -7
  94. package/dist/Select.d.ts.map +1 -1
  95. package/dist/SelectionIndicator.d.ts +30 -0
  96. package/dist/SelectionIndicator.d.ts.map +1 -0
  97. package/dist/Separator.d.ts +9 -3
  98. package/dist/Separator.d.ts.map +1 -1
  99. package/dist/SharedElementTransition.d.ts +41 -0
  100. package/dist/SharedElementTransition.d.ts.map +1 -0
  101. package/dist/Slider.d.ts +15 -8
  102. package/dist/Slider.d.ts.map +1 -1
  103. package/dist/StepList.d.ts +90 -0
  104. package/dist/StepList.d.ts.map +1 -0
  105. package/dist/Switch.d.ts +11 -5
  106. package/dist/Switch.d.ts.map +1 -1
  107. package/dist/Table.d.ts +222 -19
  108. package/dist/Table.d.ts.map +1 -1
  109. package/dist/Tabs.d.ts +47 -10
  110. package/dist/Tabs.d.ts.map +1 -1
  111. package/dist/TagGroup.d.ts +22 -10
  112. package/dist/TagGroup.d.ts.map +1 -1
  113. package/dist/Text.d.ts +10 -0
  114. package/dist/Text.d.ts.map +1 -0
  115. package/dist/TextField.d.ts +19 -11
  116. package/dist/TextField.d.ts.map +1 -1
  117. package/dist/TimeField.d.ts +32 -7
  118. package/dist/TimeField.d.ts.map +1 -1
  119. package/dist/Toast.d.ts +29 -14
  120. package/dist/Toast.d.ts.map +1 -1
  121. package/dist/ToggleButton.d.ts +36 -0
  122. package/dist/ToggleButton.d.ts.map +1 -0
  123. package/dist/ToggleButtonGroup.d.ts +33 -0
  124. package/dist/ToggleButtonGroup.d.ts.map +1 -0
  125. package/dist/Toolbar.d.ts +7 -3
  126. package/dist/Toolbar.d.ts.map +1 -1
  127. package/dist/Tooltip.d.ts +58 -7
  128. package/dist/Tooltip.d.ts.map +1 -1
  129. package/dist/Tree.d.ts +102 -11
  130. package/dist/Tree.d.ts.map +1 -1
  131. package/dist/Virtualizer.d.ts +61 -0
  132. package/dist/Virtualizer.d.ts.map +1 -0
  133. package/dist/VirtualizerLayouts.d.ts +82 -0
  134. package/dist/VirtualizerLayouts.d.ts.map +1 -0
  135. package/dist/VisuallyHidden.d.ts +4 -2
  136. package/dist/VisuallyHidden.d.ts.map +1 -1
  137. package/dist/contexts.d.ts +6 -1
  138. package/dist/contexts.d.ts.map +1 -1
  139. package/dist/index.d.ts +73 -39
  140. package/dist/index.d.ts.map +1 -1
  141. package/dist/index.js +23342 -10644
  142. package/dist/index.js.map +1 -7
  143. package/dist/index.jsx +18110 -0
  144. package/dist/index.jsx.map +1 -0
  145. package/dist/useDragAndDrop.d.ts +93 -0
  146. package/dist/useDragAndDrop.d.ts.map +1 -0
  147. package/dist/utils.d.ts +8 -2
  148. package/dist/utils.d.ts.map +1 -1
  149. package/dist/virtualizer/Layout.d.ts +79 -0
  150. package/dist/virtualizer/Layout.d.ts.map +1 -0
  151. package/package.json +33 -32
  152. package/src/ActionBar.tsx +251 -0
  153. package/src/ActionGroup.tsx +277 -0
  154. package/src/Alert.tsx +152 -0
  155. package/src/Autocomplete.tsx +39 -44
  156. package/src/Breadcrumbs.tsx +227 -72
  157. package/src/Button.tsx +315 -74
  158. package/src/Calendar.tsx +347 -141
  159. package/src/Checkbox.tsx +414 -123
  160. package/src/Collection.tsx +350 -0
  161. package/src/Color.tsx +1325 -284
  162. package/src/ColorEditor.tsx +213 -0
  163. package/src/ComboBox.tsx +644 -245
  164. package/src/ContextualHelpTrigger.tsx +195 -0
  165. package/src/DateField.tsx +274 -106
  166. package/src/DatePicker.tsx +892 -111
  167. package/src/DateRangePickerContext.tsx +44 -0
  168. package/src/Dialog.tsx +173 -104
  169. package/src/Disclosure.tsx +158 -105
  170. package/src/DragAndDrop.tsx +340 -0
  171. package/src/DragPreview.tsx +47 -0
  172. package/src/DropZone.tsx +233 -0
  173. package/src/FieldError.tsx +89 -0
  174. package/src/FileTrigger.tsx +83 -0
  175. package/src/Focusable.tsx +103 -0
  176. package/src/Form.tsx +140 -0
  177. package/src/GridList.tsx +542 -128
  178. package/src/HiddenDateInput.tsx +153 -0
  179. package/src/HiddenTimeInput.tsx +133 -0
  180. package/src/Icon.tsx +133 -0
  181. package/src/Keyboard.tsx +26 -0
  182. package/src/Landmark.tsx +37 -63
  183. package/src/Link.tsx +132 -69
  184. package/src/ListBox.tsx +656 -106
  185. package/src/ListDropTargetDelegate.ts +283 -0
  186. package/src/Menu.tsx +1234 -132
  187. package/src/Meter.tsx +44 -58
  188. package/src/Modal.tsx +262 -166
  189. package/src/NumberField.tsx +267 -151
  190. package/src/Popover.tsx +452 -343
  191. package/src/Pressable.tsx +108 -0
  192. package/src/ProgressBar.tsx +54 -59
  193. package/src/RadioGroup.tsx +533 -121
  194. package/src/RangeCalendar.tsx +249 -150
  195. package/src/RouterProvider.tsx +223 -0
  196. package/src/SearchField.tsx +460 -133
  197. package/src/Select.tsx +804 -233
  198. package/src/SelectionIndicator.tsx +108 -0
  199. package/src/Separator.tsx +47 -49
  200. package/src/SharedElementTransition.tsx +264 -0
  201. package/src/Slider.tsx +148 -98
  202. package/src/StepList.tsx +272 -0
  203. package/src/Switch.tsx +93 -46
  204. package/src/Table.tsx +1551 -225
  205. package/src/Tabs.tsx +377 -123
  206. package/src/TagGroup.tsx +233 -135
  207. package/src/Text.tsx +18 -0
  208. package/src/TextField.tsx +413 -86
  209. package/src/TimeField.tsx +232 -222
  210. package/src/Toast.tsx +306 -160
  211. package/src/ToggleButton.tsx +169 -0
  212. package/src/ToggleButtonGroup.tsx +141 -0
  213. package/src/Toolbar.tsx +61 -70
  214. package/src/Tooltip.tsx +473 -116
  215. package/src/Tree.tsx +1514 -175
  216. package/src/Virtualizer.tsx +730 -0
  217. package/src/VirtualizerLayouts.ts +280 -0
  218. package/src/VisuallyHidden.tsx +32 -38
  219. package/src/contexts.ts +29 -36
  220. package/src/index.ts +972 -620
  221. package/src/useDragAndDrop.ts +367 -0
  222. package/src/utils.tsx +69 -50
  223. package/src/virtualizer/Layout.ts +192 -0
  224. package/dist/index.ssr.js +0 -9785
  225. package/dist/index.ssr.js.map +0 -7
package/src/Table.tsx CHANGED
@@ -8,13 +8,16 @@
8
8
  import {
9
9
  type JSX,
10
10
  createContext,
11
+ createEffect,
11
12
  createMemo,
13
+ createUniqueId,
12
14
  createSignal,
15
+ onCleanup,
13
16
  splitProps,
14
17
  useContext,
15
18
  For,
16
19
  Show,
17
- } from 'solid-js';
20
+ } from "solid-js";
18
21
  import {
19
22
  createTable,
20
23
  createTableColumnHeader,
@@ -24,31 +27,58 @@ import {
24
27
  createTableSelectionCheckbox,
25
28
  createTableSelectAllCheckbox,
26
29
  createFocusRing,
30
+ isFocusVisible as isGlobalFocusVisible,
31
+ getTableData,
27
32
  createHover,
33
+ mergeProps,
28
34
  type AriaTableProps,
29
- } from '@proyecto-viviana/solidaria';
35
+ createTableColumnResize,
36
+ } from "@proyecto-viviana/solidaria";
30
37
  import {
31
38
  createTableState,
32
39
  createTableCollection,
40
+ createTableColumnResizeState,
33
41
  type TableState,
34
42
  type TableCollection,
43
+ type TableColumnResizeState,
35
44
  type Key,
36
45
  type SortDescriptor,
37
46
  type ColumnDefinition,
47
+ type ColumnSize,
38
48
  type GridNode,
39
- } from '@proyecto-viviana/solid-stately';
49
+ type DropTarget,
50
+ } from "@proyecto-viviana/solid-stately";
40
51
  import {
41
52
  type RenderChildren,
42
53
  type ClassNameOrFunction,
43
54
  type StyleOrFunction,
44
55
  type SlotProps,
56
+ dataAttr,
45
57
  useRenderProps,
46
58
  filterDOMProps,
47
- } from './utils';
48
-
49
- // ============================================
50
- // TYPES
51
- // ============================================
59
+ } from "./utils";
60
+ import { SharedElementTransition } from "./SharedElementTransition";
61
+ import { type DragAndDropHooks } from "./useDragAndDrop";
62
+ import { ButtonContext, type ButtonProps } from "./Button";
63
+ import {
64
+ CollectionRendererContext,
65
+ type CollectionRendererContextValue,
66
+ useCollectionRenderer,
67
+ } from "./Collection";
68
+ import { useVirtualizerContext } from "./Virtualizer";
69
+ import {
70
+ type LinkDOMProps,
71
+ type RouterOptions,
72
+ useRouter,
73
+ useLinkProps,
74
+ type RouterClickModifiers,
75
+ } from "./RouterProvider";
76
+ import {
77
+ getNormalizedDropTargetKey,
78
+ mergePersistedKeysIntoVirtualRange,
79
+ useDndPersistedKeys,
80
+ useRenderDropIndicator,
81
+ } from "./DragAndDrop";
52
82
 
53
83
  export interface TableRenderProps {
54
84
  /** Whether the table has focus. */
@@ -61,25 +91,84 @@ export interface TableRenderProps {
61
91
  isEmpty: boolean;
62
92
  }
63
93
 
64
- export interface TableProps<T extends object> extends Omit<AriaTableProps, 'children'>, SlotProps {
94
+ type RefLike<T> = ((el: T) => void) | { current?: T | null } | undefined;
95
+ export type TableColumnDefinition<T = unknown> = Omit<ColumnDefinition<T>, "key" | "children"> & {
96
+ /** React Spectrum-style alias for the column key. */
97
+ id?: Key;
98
+ key?: Key;
99
+ children?: TableColumnDefinition<T>[];
100
+ };
101
+
102
+ function assignRef<T>(ref: RefLike<T>, el: T): void {
103
+ if (!ref) return;
104
+ if (typeof ref === "function") ref(el);
105
+ else ref.current = el;
106
+ }
107
+
108
+ function normalizeColumnDefinitions<T>(
109
+ columns: TableColumnDefinition<T>[],
110
+ parentKey?: Key,
111
+ ): (ColumnDefinition<T> & { id?: Key })[] {
112
+ return columns.map((column, index) => {
113
+ const key = column.key ?? column.id ?? (parentKey == null ? index : `${parentKey}-${index}`);
114
+ const id = column.id ?? column.key;
115
+
116
+ return {
117
+ ...column,
118
+ key,
119
+ id,
120
+ children: column.children ? normalizeColumnDefinitions(column.children, key) : undefined,
121
+ } as ColumnDefinition<T> & { id?: Key };
122
+ });
123
+ }
124
+
125
+ function getRowHeaderColumnKeys<T>(columns: (ColumnDefinition<T> & { id?: Key })[]): Set<Key> {
126
+ const keys = new Set<Key>();
127
+
128
+ for (const column of columns) {
129
+ if (column.isRowHeader) {
130
+ keys.add(column.key);
131
+ }
132
+
133
+ if (column.children) {
134
+ for (const key of getRowHeaderColumnKeys(
135
+ column.children as (ColumnDefinition<T> & {
136
+ id?: Key;
137
+ })[],
138
+ )) {
139
+ keys.add(key);
140
+ }
141
+ }
142
+ }
143
+
144
+ return keys;
145
+ }
146
+
147
+ export interface TableProps<T extends object> extends Omit<AriaTableProps, "children">, SlotProps {
65
148
  /** The data items to render in the table. */
66
149
  items: T[];
67
150
  /** The column definitions. */
68
- columns: ColumnDefinition<T>[];
151
+ columns: TableColumnDefinition<T>[];
69
152
  /** Function to get the key from an item. */
70
153
  getKey?: (item: T) => Key;
71
154
  /** Function to get the text value from an item for a column. */
72
- getTextValue?: (item: T, column: ColumnDefinition<T>) => string;
155
+ getTextValue?: (item: T, column: TableColumnDefinition<T>) => string;
73
156
  /** The selection mode. */
74
- selectionMode?: 'none' | 'single' | 'multiple';
157
+ selectionMode?: "none" | "single" | "multiple";
158
+ /** The selection behavior (toggle vs replace). */
159
+ selectionBehavior?: "toggle" | "replace";
160
+ /** Whether disabled rows remain focusable. */
161
+ disabledBehavior?: "selection" | "all";
162
+ /** Whether Escape clears selection. */
163
+ escapeKeyBehavior?: "clearSelection" | "none";
75
164
  /** Keys of disabled items. */
76
165
  disabledKeys?: Iterable<Key>;
77
166
  /** Currently selected keys (controlled). */
78
- selectedKeys?: 'all' | Iterable<Key>;
167
+ selectedKeys?: "all" | Iterable<Key>;
79
168
  /** Default selected keys (uncontrolled). */
80
- defaultSelectedKeys?: 'all' | Iterable<Key>;
169
+ defaultSelectedKeys?: "all" | Iterable<Key>;
81
170
  /** Handler called when selection changes. */
82
- onSelectionChange?: (keys: 'all' | Set<Key>) => void;
171
+ onSelectionChange?: (keys: "all" | Set<Key>) => void;
83
172
  /** The current sort descriptor. */
84
173
  sortDescriptor?: SortDescriptor;
85
174
  /** Handler called when sort changes. */
@@ -94,11 +183,22 @@ export interface TableProps<T extends object> extends Omit<AriaTableProps, 'chil
94
183
  style?: StyleOrFunction<TableRenderProps>;
95
184
  /** A function to render when the table is empty. */
96
185
  renderEmptyState?: () => JSX.Element;
186
+ /** Ref for the table element. */
187
+ ref?: RefLike<HTMLTableElement>;
188
+ /** Custom renderer for the table element. */
189
+ render?: (
190
+ props: JSX.HTMLAttributes<HTMLTableElement>,
191
+ renderProps: TableRenderProps,
192
+ ) => JSX.Element;
193
+ /** Drag and drop hooks from `useDragAndDrop`. */
194
+ dragAndDropHooks?: DragAndDropHooks<T>;
97
195
  }
98
196
 
99
197
  export interface TableHeaderRenderProps {
100
198
  /** Whether the header has focus. */
101
199
  isFocused: boolean;
200
+ /** Whether the header is being hovered. */
201
+ isHovered: boolean;
102
202
  }
103
203
 
104
204
  export interface TableHeaderProps extends SlotProps {
@@ -108,6 +208,13 @@ export interface TableHeaderProps extends SlotProps {
108
208
  class?: ClassNameOrFunction<TableHeaderRenderProps>;
109
209
  /** The inline style for the element. */
110
210
  style?: StyleOrFunction<TableHeaderRenderProps>;
211
+ /** Ref for the table header element. */
212
+ ref?: RefLike<HTMLTableSectionElement>;
213
+ /** Custom renderer for the table header element. */
214
+ render?: (
215
+ props: JSX.HTMLAttributes<HTMLTableSectionElement>,
216
+ renderProps: TableHeaderRenderProps,
217
+ ) => JSX.Element;
111
218
  }
112
219
 
113
220
  export interface TableColumnRenderProps {
@@ -118,9 +225,13 @@ export interface TableColumnRenderProps {
118
225
  /** Whether the column is sortable. */
119
226
  isSortable: boolean;
120
227
  /** The current sort direction ('ascending', 'descending', or undefined). */
121
- sortDirection: 'ascending' | 'descending' | undefined;
228
+ sortDirection: "ascending" | "descending" | undefined;
122
229
  /** Whether the column is being hovered. */
123
230
  isHovered: boolean;
231
+ /** Whether the column allows resizing. */
232
+ allowsResizing: boolean;
233
+ /** Whether the column is currently being resized. */
234
+ isResizing: boolean;
124
235
  }
125
236
 
126
237
  export interface TableColumnProps extends SlotProps {
@@ -128,12 +239,29 @@ export interface TableColumnProps extends SlotProps {
128
239
  id: Key;
129
240
  /** Whether the column allows sorting. */
130
241
  allowsSorting?: boolean;
242
+ /** Whether the column allows resizing. */
243
+ allowsResizing?: boolean;
244
+ /** Column width (number for px, string for 'Xfr', 'X%', 'Xpx'). */
245
+ width?: ColumnSize;
246
+ /** Default width for uncontrolled mode. */
247
+ defaultWidth?: ColumnSize;
248
+ /** Minimum column width in px. */
249
+ minWidth?: number;
250
+ /** Maximum column width in px. */
251
+ maxWidth?: number;
131
252
  /** The children of the column. */
132
253
  children?: RenderChildren<TableColumnRenderProps>;
133
254
  /** The CSS className for the element. */
134
255
  class?: ClassNameOrFunction<TableColumnRenderProps>;
135
256
  /** The inline style for the element. */
136
257
  style?: StyleOrFunction<TableColumnRenderProps>;
258
+ /** Ref for the column header element. */
259
+ ref?: RefLike<HTMLTableCellElement>;
260
+ /** Custom renderer for the column header element. */
261
+ render?: (
262
+ props: JSX.ThHTMLAttributes<HTMLTableCellElement>,
263
+ renderProps: TableColumnRenderProps,
264
+ ) => JSX.Element;
137
265
  }
138
266
 
139
267
  export interface TableBodyRenderProps {
@@ -152,6 +280,35 @@ export interface TableBodyProps<T> extends SlotProps {
152
280
  style?: StyleOrFunction<TableBodyRenderProps>;
153
281
  /** A function to render when the body is empty. */
154
282
  renderEmptyState?: () => JSX.Element;
283
+ /** Whether there are more rows to load. */
284
+ hasMore?: boolean;
285
+ /** Whether additional rows are currently loading. */
286
+ isLoading?: boolean;
287
+ /** Called when the load more sentinel becomes visible. */
288
+ onLoadMore?: () => void | Promise<void>;
289
+ /** Ref for the table body element. */
290
+ ref?: RefLike<HTMLTableSectionElement>;
291
+ /** Custom renderer for the table body element. */
292
+ render?: (
293
+ props: JSX.HTMLAttributes<HTMLTableSectionElement>,
294
+ renderProps: TableBodyRenderProps,
295
+ ) => JSX.Element;
296
+ }
297
+
298
+ export interface TableFooterRenderProps {
299
+ /** Whether the footer has no items. */
300
+ isEmpty: boolean;
301
+ }
302
+
303
+ export interface TableFooterProps<T> extends SlotProps {
304
+ /** The footer items to render. */
305
+ items?: T[];
306
+ /** The children, or a render function when `items` is provided. */
307
+ children?: JSX.Element | ((item: T) => JSX.Element);
308
+ /** The CSS className for the element. */
309
+ class?: ClassNameOrFunction<TableFooterRenderProps>;
310
+ /** The inline style for the element. */
311
+ style?: StyleOrFunction<TableFooterRenderProps>;
155
312
  }
156
313
 
157
314
  export interface TableRowRenderProps {
@@ -171,17 +328,45 @@ export interface TableRowRenderProps {
171
328
 
172
329
  export interface TableRowProps<T> extends SlotProps {
173
330
  /** The unique key for the row. */
174
- id: Key;
331
+ id?: Key;
175
332
  /** The item value. */
176
333
  item?: T;
334
+ /** Columns to render when children is a column render function. */
335
+ columns?: TableColumnDefinition<T>[];
336
+ /** Whether the row is disabled. */
337
+ isDisabled?: boolean;
177
338
  /** The children of the row (usually TableCell components). */
178
- children?: JSX.Element | RenderChildren<TableRowRenderProps>;
339
+ children?:
340
+ | JSX.Element
341
+ | RenderChildren<TableRowRenderProps>
342
+ | ((column: TableColumnDefinition<T>) => JSX.Element);
179
343
  /** The CSS className for the element. */
180
344
  class?: ClassNameOrFunction<TableRowRenderProps>;
181
345
  /** The inline style for the element. */
182
346
  style?: StyleOrFunction<TableRowRenderProps>;
183
347
  /** Handler called when the row is activated (double-click or Enter). */
184
348
  onAction?: () => void;
349
+ /** The URL this row links to. */
350
+ href?: string;
351
+ /** Link target for linked rows. */
352
+ target?: LinkDOMProps["target"];
353
+ /** Link relationship for linked rows. */
354
+ rel?: LinkDOMProps["rel"];
355
+ /** Download attribute for linked rows. */
356
+ download?: LinkDOMProps["download"];
357
+ /** Ping attribute for linked rows. */
358
+ ping?: LinkDOMProps["ping"];
359
+ /** Referrer policy for linked rows. */
360
+ referrerPolicy?: LinkDOMProps["referrerPolicy"];
361
+ /** Router options for linked rows. */
362
+ routerOptions?: RouterOptions;
363
+ /** Ref for the table row element. */
364
+ ref?: RefLike<HTMLTableRowElement>;
365
+ /** Custom renderer for the table row element. */
366
+ render?: (
367
+ props: JSX.HTMLAttributes<HTMLTableRowElement>,
368
+ renderProps: TableRowRenderProps,
369
+ ) => JSX.Element;
185
370
  }
186
371
 
187
372
  export interface TableCellRenderProps {
@@ -189,6 +374,8 @@ export interface TableCellRenderProps {
189
374
  isFocused: boolean;
190
375
  /** Whether the cell has keyboard focus. */
191
376
  isFocusVisible: boolean;
377
+ /** The zero-based column index for the cell. */
378
+ columnIndex: number;
192
379
  /** Whether the cell is pressed. */
193
380
  isPressed: boolean;
194
381
  /** Whether the cell is hovered. */
@@ -198,42 +385,69 @@ export interface TableCellRenderProps {
198
385
  export interface TableCellProps extends SlotProps {
199
386
  /** The unique key for the cell. */
200
387
  id?: Key;
388
+ /** Number of columns spanned by the cell. */
389
+ colSpan?: number;
201
390
  /** The children of the cell. */
202
391
  children?: RenderChildren<TableCellRenderProps>;
203
392
  /** The CSS className for the element. */
204
393
  class?: ClassNameOrFunction<TableCellRenderProps>;
205
394
  /** The inline style for the element. */
206
395
  style?: StyleOrFunction<TableCellRenderProps>;
396
+ /** Ref for the table cell element. */
397
+ ref?: RefLike<HTMLTableCellElement>;
398
+ /** Custom renderer for the table cell element. */
399
+ render?: (
400
+ props: JSX.TdHTMLAttributes<HTMLTableCellElement>,
401
+ renderProps: TableCellRenderProps,
402
+ ) => JSX.Element;
207
403
  }
208
404
 
209
- // ============================================
210
- // CONTEXT
211
- // ============================================
405
+ export interface TableLoadMoreItemProps extends SlotProps {
406
+ onLoadMore: () => void | Promise<void>;
407
+ isLoading?: boolean;
408
+ /** Scroll offset multiplier for early loading trigger (default: 1 = 100% of viewport height). */
409
+ scrollOffset?: number;
410
+ colSpan?: number;
411
+ children?: JSX.Element;
412
+ class?: ClassNameOrFunction<{ isLoading: boolean }>;
413
+ style?: StyleOrFunction<{ isLoading: boolean }>;
414
+ }
212
415
 
213
416
  interface TableContextValue<T extends object> {
214
417
  state: TableState<T, TableCollection<T>>;
215
418
  collection: TableCollection<T>;
216
419
  items: T[];
217
- columns: ColumnDefinition<T>[];
420
+ columns: (ColumnDefinition<T> & { id?: Key })[];
218
421
  isDisabled: boolean;
219
422
  showSelectionCheckboxes: boolean;
423
+ dragAndDropHooks?: DragAndDropHooks<T>;
424
+ dragState?: unknown;
425
+ dropState?: unknown;
426
+ isVirtualized: boolean;
220
427
  }
221
428
 
222
429
  export const TableContext = createContext<TableContextValue<object> | null>(null);
223
- export const TableStateContext = createContext<TableState<object, TableCollection<object>> | null>(null);
430
+ export const TableStateContext = createContext<TableState<object, TableCollection<object>> | null>(
431
+ null,
432
+ );
433
+ /** The resize context carries a getter for the resize state. The getter may return null before columns register. */
434
+ export const TableColumnResizeStateContext = createContext<{
435
+ getState: () => TableColumnResizeState | null;
436
+ getCallbacks?: () => {
437
+ onResizeStart?: (widths: Map<Key, number>) => void;
438
+ onResize?: (widths: Map<Key, number>) => void;
439
+ onResizeEnd?: (widths: Map<Key, number>) => void;
440
+ };
441
+ } | null>(null);
224
442
 
225
- // Row-level context for cells
226
443
  interface TableRowContextValue {
227
444
  rowKey: Key;
228
445
  rowNode: GridNode<unknown>;
446
+ getCellColumnKey(cellId: string, explicitId?: Key): Key | undefined;
229
447
  }
230
448
 
231
449
  export const TableRowContext = createContext<TableRowContextValue | null>(null);
232
450
 
233
- // ============================================
234
- // COMPONENTS
235
- // ============================================
236
-
237
451
  /**
238
452
  * A table displays data in rows and columns and enables a user to navigate its contents via directional navigation keys,
239
453
  * and optionally supports row selection and sorting.
@@ -241,42 +455,48 @@ export const TableRowContext = createContext<TableRowContextValue | null>(null);
241
455
  export function Table<T extends object>(props: TableProps<T>): JSX.Element {
242
456
  const [local, stateProps, ariaProps] = splitProps(
243
457
  props,
244
- ['class', 'style', 'slot', 'renderEmptyState'],
458
+ ["class", "style", "render", "slot", "renderEmptyState", "dragAndDropHooks", "ref"],
245
459
  [
246
- 'items',
247
- 'columns',
248
- 'getKey',
249
- 'getTextValue',
250
- 'disabledKeys',
251
- 'selectionMode',
252
- 'selectedKeys',
253
- 'defaultSelectedKeys',
254
- 'onSelectionChange',
255
- 'sortDescriptor',
256
- 'onSortChange',
257
- 'showSelectionCheckboxes',
258
- ]
259
- );
260
-
261
- // Create ref signal
460
+ "items",
461
+ "columns",
462
+ "getKey",
463
+ "getTextValue",
464
+ "disabledKeys",
465
+ "disabledBehavior",
466
+ "escapeKeyBehavior",
467
+ "selectionMode",
468
+ "selectionBehavior",
469
+ "selectedKeys",
470
+ "defaultSelectedKeys",
471
+ "onSelectionChange",
472
+ "sortDescriptor",
473
+ "onSortChange",
474
+ "showSelectionCheckboxes",
475
+ ],
476
+ );
477
+
262
478
  const [ref, setRef] = createSignal<HTMLTableElement | null>(null);
479
+ const normalizedColumns = createMemo(() => normalizeColumnDefinitions(stateProps.columns));
480
+ const rowHeaderColumnKeys = createMemo(() => getRowHeaderColumnKeys(normalizedColumns()));
263
481
 
264
- // Create collection
265
482
  const collection = createMemo(() =>
266
483
  createTableCollection<T>({
267
- columns: stateProps.columns,
484
+ columns: normalizedColumns(),
268
485
  rows: stateProps.items,
269
486
  getKey: stateProps.getKey,
270
- getTextValue: stateProps.getTextValue,
487
+ getTextValue: stateProps.getTextValue as
488
+ | ((item: T, column: ColumnDefinition<T>) => string)
489
+ | undefined,
271
490
  showSelectionCheckboxes: stateProps.showSelectionCheckboxes ?? false,
272
- })
491
+ rowHeaderColumnKeys: rowHeaderColumnKeys().size > 0 ? rowHeaderColumnKeys() : undefined,
492
+ }),
273
493
  );
274
494
 
275
- // Create table state
276
495
  const state = createTableState<T, TableCollection<T>>(() => ({
277
496
  collection: collection(),
278
497
  disabledKeys: stateProps.disabledKeys,
279
498
  selectionMode: stateProps.selectionMode,
499
+ selectionBehavior: stateProps.selectionBehavior,
280
500
  selectedKeys: stateProps.selectedKeys,
281
501
  defaultSelectedKeys: stateProps.defaultSelectedKeys,
282
502
  onSelectionChange: stateProps.onSelectionChange,
@@ -284,27 +504,27 @@ export function Table<T extends object>(props: TableProps<T>): JSX.Element {
284
504
  onSortChange: stateProps.onSortChange,
285
505
  showSelectionCheckboxes: stateProps.showSelectionCheckboxes,
286
506
  }));
507
+ const parentCollectionRenderer = useCollectionRenderer<T>();
287
508
 
288
- // Create table aria props
289
509
  const { gridProps } = createTable<T>(
290
510
  () => ({
291
511
  id: ariaProps.id,
292
- 'aria-label': ariaProps['aria-label'],
293
- 'aria-labelledby': ariaProps['aria-labelledby'],
294
- 'aria-describedby': ariaProps['aria-describedby'],
295
- isVirtualized: ariaProps.isVirtualized,
512
+ "aria-label": ariaProps["aria-label"],
513
+ "aria-labelledby": ariaProps["aria-labelledby"],
514
+ "aria-describedby": ariaProps["aria-describedby"],
515
+ isVirtualized: ariaProps.isVirtualized ?? parentCollectionRenderer?.isVirtualized,
296
516
  onRowAction: ariaProps.onRowAction,
297
517
  onCellAction: ariaProps.onCellAction,
518
+ shouldSelectOnPressUp: ariaProps.shouldSelectOnPressUp,
298
519
  focusMode: ariaProps.focusMode,
520
+ escapeKeyBehavior: stateProps.escapeKeyBehavior,
299
521
  }),
300
522
  () => state,
301
- ref
523
+ ref,
302
524
  );
303
525
 
304
- // Create focus ring
305
526
  const { isFocused, isFocusVisible, focusProps } = createFocusRing();
306
527
 
307
- // Render props values
308
528
  const renderValues = createMemo<TableRenderProps>(() => ({
309
529
  isFocused: state.isFocused || isFocused(),
310
530
  isFocusVisible: isFocusVisible(),
@@ -312,24 +532,22 @@ export function Table<T extends object>(props: TableProps<T>): JSX.Element {
312
532
  isEmpty: stateProps.items.length === 0,
313
533
  }));
314
534
 
315
- // Resolve render props
535
+ // Resolve render props (class and style only — children rendered directly in JSX
536
+ // to avoid eager evaluation before context providers mount)
316
537
  const renderProps = useRenderProps(
317
538
  {
318
- children: props.children,
319
539
  class: local.class,
320
540
  style: local.style,
321
- defaultClassName: 'solidaria-Table',
541
+ defaultClassName: "solidaria-Table",
322
542
  },
323
- renderValues
543
+ renderValues,
324
544
  );
325
545
 
326
- // Filter DOM props
327
546
  const domProps = createMemo(() => {
328
547
  const filtered = filterDOMProps(ariaProps as Record<string, unknown>, { global: true });
329
548
  return filtered;
330
549
  });
331
550
 
332
- // Remove ref from spread props
333
551
  const cleanGridProps = () => {
334
552
  const { ref: _ref1, ...rest } = gridProps as Record<string, unknown>;
335
553
  return rest;
@@ -338,32 +556,189 @@ export function Table<T extends object>(props: TableProps<T>): JSX.Element {
338
556
  const { ref: _ref2, ...rest } = focusProps as Record<string, unknown>;
339
557
  return rest;
340
558
  };
559
+ const getItemNodes = createMemo(() =>
560
+ Array.from(state.collection).filter((node) => node.type === "item"),
561
+ );
562
+ const getDropTargetByIndex = (
563
+ index: number,
564
+ position: "before" | "after" | "on",
565
+ ): DropTarget | null => {
566
+ const node = getItemNodes()[index];
567
+ if (!node) return null;
568
+ return { type: "item", key: node.key, dropPosition: position };
569
+ };
570
+ const hasDroppableDnd = createMemo(() => {
571
+ const hooks = local.dragAndDropHooks;
572
+ return Boolean(
573
+ hooks?.useDroppableCollectionState &&
574
+ hooks.useDroppableCollection &&
575
+ (hooks.dropTargetDelegate ||
576
+ parentCollectionRenderer?.dropTargetDelegate ||
577
+ hooks.ListDropTargetDelegate),
578
+ );
579
+ });
580
+ const hasDraggableDnd = createMemo(() => {
581
+ const hooks = local.dragAndDropHooks;
582
+ return Boolean(hooks?.useDraggableCollectionState && hooks.useDraggableCollection);
583
+ });
584
+ const dragState = createMemo(() => {
585
+ if (!hasDraggableDnd()) return undefined;
586
+ return local.dragAndDropHooks?.useDraggableCollectionState?.({
587
+ items: stateProps.items,
588
+ });
589
+ });
590
+ const dropState = createMemo(() => {
591
+ if (!hasDroppableDnd()) return undefined;
592
+ return local.dragAndDropHooks?.useDroppableCollectionState?.({});
593
+ });
594
+ createEffect(() => {
595
+ if (!hasDraggableDnd()) return;
596
+ const hooks = local.dragAndDropHooks;
597
+ const activeDragState = dragState();
598
+ if (!hooks?.useDraggableCollection || !activeDragState) return;
599
+ hooks.useDraggableCollection({}, activeDragState, () => ref());
600
+ });
601
+ const droppableCollection = createMemo(() => {
602
+ if (!hasDroppableDnd()) return undefined;
603
+ const hooks = local.dragAndDropHooks;
604
+ const activeDropState = dropState();
605
+ if (!hooks?.useDroppableCollection || !activeDropState) return undefined;
606
+ const resolveDirection = (): "ltr" | "rtl" => {
607
+ const el = ref();
608
+ if (el && typeof window !== "undefined" && typeof window.getComputedStyle === "function") {
609
+ const dir = window.getComputedStyle(el).direction;
610
+ if (dir === "rtl") return "rtl";
611
+ }
612
+ return typeof document !== "undefined" && document.dir === "rtl" ? "rtl" : "ltr";
613
+ };
614
+ const dropTargetDelegate =
615
+ hooks.dropTargetDelegate ??
616
+ parentCollectionRenderer?.dropTargetDelegate ??
617
+ (hooks.ListDropTargetDelegate
618
+ ? new hooks.ListDropTargetDelegate(
619
+ () => state.collection,
620
+ () => ref(),
621
+ { layout: "grid", orientation: "vertical", direction: resolveDirection() },
622
+ )
623
+ : undefined);
624
+ if (!dropTargetDelegate) return undefined;
625
+ return hooks.useDroppableCollection(
626
+ {
627
+ dropTargetDelegate,
628
+ keyboardDelegate: {
629
+ getFirstKey: () => state.collection.getFirstKey?.() ?? null,
630
+ getLastKey: () => state.collection.getLastKey?.() ?? null,
631
+ getKeyBelow: (key) => state.collection.getKeyAfter?.(key) ?? null,
632
+ getKeyAbove: (key) => state.collection.getKeyBefore?.(key) ?? null,
633
+ getKeyLeftOf: (key) =>
634
+ resolveDirection() === "rtl"
635
+ ? (state.collection.getKeyAfter?.(key) ?? null)
636
+ : (state.collection.getKeyBefore?.(key) ?? null),
637
+ getKeyRightOf: (key) =>
638
+ resolveDirection() === "rtl"
639
+ ? (state.collection.getKeyBefore?.(key) ?? null)
640
+ : (state.collection.getKeyAfter?.(key) ?? null),
641
+ getKeyPageBelow: (key) => state.collection.getKeyAfter?.(key) ?? null,
642
+ getKeyPageAbove: (key) => state.collection.getKeyBefore?.(key) ?? null,
643
+ },
644
+ get collection() {
645
+ return state.collection;
646
+ },
647
+ get selectedKeys() {
648
+ return state.selectedKeys;
649
+ },
650
+ setSelectedKeys: (keys: Set<Key>) => {
651
+ if (state.selectionMode === "none") return;
652
+ state.clearSelection();
653
+ for (const key of keys) {
654
+ state.toggleSelection(key);
655
+ }
656
+ },
657
+ setFocusedKey: (key) => state.setFocusedKey(key),
658
+ },
659
+ activeDropState,
660
+ () => ref(),
661
+ );
662
+ });
663
+ const isRootDropTarget = createMemo(() => {
664
+ return Boolean(dropState()?.target?.type === "root");
665
+ });
666
+ const dndRenderDropIndicator = createMemo(() =>
667
+ useRenderDropIndicator(local.dragAndDropHooks, dropState()),
668
+ );
669
+ const dndDropIndicator = (index: number, position: "before" | "after" | "on") => {
670
+ const target = getDropTargetByIndex(index, position);
671
+ if (!target || target.type !== "item") return undefined;
672
+ return dndRenderDropIndicator()?.(target);
673
+ };
341
674
 
342
- const contextValue = createMemo<TableContextValue<T>>(() => ({
675
+ const contextValue: TableContextValue<T> = {
343
676
  state,
344
- collection: collection(),
345
- items: stateProps.items,
346
- columns: stateProps.columns,
677
+ get collection() {
678
+ return collection();
679
+ },
680
+ get items() {
681
+ return stateProps.items;
682
+ },
683
+ get columns() {
684
+ return normalizedColumns();
685
+ },
347
686
  isDisabled: false,
348
- showSelectionCheckboxes: stateProps.showSelectionCheckboxes ?? false,
687
+ get showSelectionCheckboxes() {
688
+ return stateProps.showSelectionCheckboxes ?? false;
689
+ },
690
+ get dragAndDropHooks() {
691
+ return local.dragAndDropHooks;
692
+ },
693
+ get dragState() {
694
+ return dragState();
695
+ },
696
+ get dropState() {
697
+ return dropState();
698
+ },
699
+ get isVirtualized() {
700
+ return ariaProps.isVirtualized ?? parentCollectionRenderer?.isVirtualized ?? false;
701
+ },
702
+ };
703
+ const collectionRenderer = createMemo<CollectionRendererContextValue<unknown>>(() => ({
704
+ ...parentCollectionRenderer,
705
+ renderItem: (item) => item as JSX.Element,
706
+ renderDropIndicator: (index: number, position: "before" | "after" | "on") =>
707
+ dndDropIndicator(index, position) ??
708
+ parentCollectionRenderer?.renderDropIndicator?.(index, position),
349
709
  }));
710
+ const tableChildren = () =>
711
+ typeof props.children === "function" ? props.children(renderValues()) : props.children;
712
+ const tableProps = () =>
713
+ ({
714
+ ref: (el: HTMLTableElement) => {
715
+ setRef(el);
716
+ assignRef(local.ref, el);
717
+ },
718
+ ...mergeProps(
719
+ domProps(),
720
+ cleanGridProps(),
721
+ cleanFocusProps(),
722
+ (droppableCollection()?.collectionProps as Record<string, unknown> | undefined) ?? {},
723
+ ),
724
+ class: renderProps.class(),
725
+ style: renderProps.style(),
726
+ "data-focused": state.isFocused || undefined,
727
+ "data-focus-visible": isFocusVisible() || undefined,
728
+ "data-empty": stateProps.items.length === 0 || undefined,
729
+ "data-drop-target": isRootDropTarget() || undefined,
730
+ slot: local.slot,
731
+ children: tableChildren(),
732
+ }) as JSX.HTMLAttributes<HTMLTableElement>;
350
733
 
351
734
  return (
352
- <TableContext.Provider value={contextValue() as TableContextValue<object>}>
353
- <TableStateContext.Provider value={state as unknown as TableState<object, TableCollection<object>>}>
354
- <table
355
- ref={setRef}
356
- {...domProps()}
357
- {...cleanGridProps()}
358
- {...cleanFocusProps()}
359
- class={renderProps.class()}
360
- style={renderProps.style()}
361
- data-focused={state.isFocused || undefined}
362
- data-focus-visible={isFocusVisible() || undefined}
363
- data-empty={stateProps.items.length === 0 || undefined}
364
- >
365
- {renderProps.renderChildren()}
366
- </table>
735
+ <TableContext.Provider value={contextValue as unknown as TableContextValue<object>}>
736
+ <TableStateContext.Provider
737
+ value={state as unknown as TableState<object, TableCollection<object>>}
738
+ >
739
+ <CollectionRendererContext.Provider value={collectionRenderer()}>
740
+ {local.render ? local.render(tableProps(), renderValues()) : <table {...tableProps()} />}
741
+ </CollectionRendererContext.Provider>
367
742
  </TableStateContext.Provider>
368
743
  </TableContext.Provider>
369
744
  );
@@ -373,39 +748,83 @@ export function Table<T extends object>(props: TableProps<T>): JSX.Element {
373
748
  * A header row in a table containing column headers.
374
749
  */
375
750
  export function TableHeader(props: TableHeaderProps): JSX.Element {
376
- const [local] = splitProps(props, ['class', 'style', 'slot']);
751
+ const [local, domProps] = splitProps(props, [
752
+ "class",
753
+ "style",
754
+ "render",
755
+ "slot",
756
+ "children",
757
+ "ref",
758
+ ]);
377
759
 
378
- // Get context
379
760
  const context = useContext(TableContext);
380
761
  if (!context) {
381
- throw new Error('TableHeader must be used within a Table');
762
+ throw new Error("TableHeader must be used within a Table");
382
763
  }
383
764
 
384
- const { rowGroupProps } = createTableRowGroup(() => ({ type: 'thead' }));
765
+ const { rowGroupProps } = createTableRowGroup(() => ({ type: "thead" }));
766
+
767
+ const { isHovered, hoverProps } = createHover({
768
+ isDisabled: false,
769
+ onHoverStart(e) {
770
+ (domProps as Record<string, (e: unknown) => void>).onHoverStart?.(e);
771
+ },
772
+ onHoverEnd(e) {
773
+ (domProps as Record<string, (e: unknown) => void>).onHoverEnd?.(e);
774
+ },
775
+ onHoverChange(isHovering) {
776
+ (domProps as Record<string, (isHovering: boolean) => void>).onHoverChange?.(isHovering);
777
+ },
778
+ });
385
779
 
386
- // Render props values
387
780
  const renderValues = createMemo<TableHeaderRenderProps>(() => ({
388
781
  isFocused: false,
782
+ isHovered: isHovered(),
389
783
  }));
390
784
 
391
- // Resolve render props
392
785
  const renderProps = useRenderProps(
393
786
  {
394
787
  class: local.class,
395
788
  style: local.style,
396
- defaultClassName: 'solidaria-Table-header',
789
+ defaultClassName: "solidaria-Table-header",
397
790
  },
398
- renderValues
791
+ renderValues,
399
792
  );
400
793
 
401
794
  const cleanRowGroupProps = () => {
402
795
  const { ref: _ref, ...rest } = rowGroupProps as Record<string, unknown>;
403
796
  return rest;
404
797
  };
798
+ const cleanHoverProps = () => {
799
+ const { ref: _ref, ...rest } = hoverProps as Record<string, unknown>;
800
+ return rest;
801
+ };
405
802
 
406
- return (
407
- <thead {...cleanRowGroupProps()} class={renderProps.class()} style={renderProps.style()}>
408
- <tr role="row">{props.children}</tr>
803
+ const headerProps = () =>
804
+ ({
805
+ ref: (el: HTMLTableSectionElement) => assignRef(local.ref, el),
806
+ ...domProps,
807
+ ...cleanRowGroupProps(),
808
+ ...cleanHoverProps(),
809
+ class: renderProps.class(),
810
+ style: renderProps.style(),
811
+ "data-hovered": isHovered() || undefined,
812
+ children: <tr role="row">{local.children}</tr>,
813
+ }) as JSX.HTMLAttributes<HTMLTableSectionElement>;
814
+
815
+ return local.render ? (
816
+ local.render(headerProps(), renderValues())
817
+ ) : (
818
+ <thead
819
+ ref={(el) => assignRef(local.ref, el)}
820
+ {...domProps}
821
+ {...cleanRowGroupProps()}
822
+ {...cleanHoverProps()}
823
+ class={renderProps.class()}
824
+ style={renderProps.style()}
825
+ data-hovered={isHovered() || undefined}
826
+ >
827
+ <tr role="row">{local.children}</tr>
409
828
  </thead>
410
829
  );
411
830
  }
@@ -414,25 +833,35 @@ export function TableHeader(props: TableHeaderProps): JSX.Element {
414
833
  * A column header in a table.
415
834
  */
416
835
  export function TableColumn(props: TableColumnProps): JSX.Element {
417
- const [local] = splitProps(props, ['class', 'style', 'slot', 'id', 'allowsSorting']);
836
+ const [local, domProps] = splitProps(props, [
837
+ "class",
838
+ "style",
839
+ "render",
840
+ "slot",
841
+ "id",
842
+ "allowsSorting",
843
+ "allowsResizing",
844
+ "width",
845
+ "defaultWidth",
846
+ "minWidth",
847
+ "maxWidth",
848
+ "children",
849
+ "ref",
850
+ ]);
418
851
 
419
- // Get context
420
852
  const context = useContext(TableContext);
421
853
  if (!context) {
422
- throw new Error('TableColumn must be used within a Table');
854
+ throw new Error("TableColumn must be used within a Table");
423
855
  }
424
856
  const { state, collection } = context;
425
857
 
426
- // Create ref signal
427
858
  const [ref, setRef] = createSignal<HTMLTableCellElement | null>(null);
428
859
 
429
- // Find the column node
430
860
  const columnNode = createMemo(() => {
431
861
  const node = collection.getItem(local.id);
432
862
  if (!node) {
433
- // Create a simple node for the column
434
863
  return {
435
- type: 'column' as const,
864
+ type: "column" as const,
436
865
  key: local.id,
437
866
  value: null,
438
867
  textValue: String(local.id),
@@ -445,25 +874,32 @@ export function TableColumn(props: TableColumnProps): JSX.Element {
445
874
  return node;
446
875
  });
447
876
 
448
- // Create column header aria props
449
- const { columnHeaderProps } = createTableColumnHeader<object>(
877
+ const columnHeaderAria = createTableColumnHeader<object>(
450
878
  () => ({
451
879
  node: columnNode(),
452
880
  allowsSorting: local.allowsSorting,
453
881
  }),
454
882
  () => state as TableState<object, TableCollection<object>>,
455
- ref
883
+ ref,
456
884
  );
457
885
 
458
- // Create hover
459
886
  const { isHovered, hoverProps } = createHover({
460
- isDisabled: false,
887
+ get isDisabled() {
888
+ return !local.allowsSorting;
889
+ },
890
+ onHoverStart(e) {
891
+ (domProps as Record<string, (e: unknown) => void>).onHoverStart?.(e);
892
+ },
893
+ onHoverEnd(e) {
894
+ (domProps as Record<string, (e: unknown) => void>).onHoverEnd?.(e);
895
+ },
896
+ onHoverChange(isHovering) {
897
+ (domProps as Record<string, (isHovering: boolean) => void>).onHoverChange?.(isHovering);
898
+ },
461
899
  });
462
900
 
463
- // Create focus ring
464
901
  const { isFocusVisible, focusProps } = createFocusRing();
465
902
 
466
- // Get sort direction
467
903
  const sortDirection = createMemo(() => {
468
904
  const sortDescriptor = state.sortDescriptor;
469
905
  if (sortDescriptor?.column === local.id) {
@@ -472,29 +908,43 @@ export function TableColumn(props: TableColumnProps): JSX.Element {
472
908
  return undefined;
473
909
  });
474
910
 
475
- // Render props values
911
+ const resizeCtx = useContext(TableColumnResizeStateContext);
912
+
913
+ const isResizing = createMemo(() => {
914
+ const rs = resizeCtx?.getState();
915
+ if (!rs) return false;
916
+ return rs.resizingColumn() === local.id;
917
+ });
918
+
919
+ const resizeWidth = createMemo(() => {
920
+ const rs = resizeCtx?.getState();
921
+ if (!rs) return undefined;
922
+ const w = rs.getColumnWidth(local.id);
923
+ return w > 0 ? w : undefined;
924
+ });
925
+
476
926
  const renderValues = createMemo<TableColumnRenderProps>(() => ({
477
927
  isFocused: state.focusedKey === local.id,
478
928
  isFocusVisible: isFocusVisible() && state.focusedKey === local.id,
479
929
  isSortable: local.allowsSorting ?? false,
480
930
  sortDirection: sortDirection(),
481
931
  isHovered: isHovered(),
932
+ allowsResizing: local.allowsResizing ?? false,
933
+ isResizing: isResizing(),
482
934
  }));
483
935
 
484
- // Resolve render props
936
+ // Resolve render props (children rendered directly in JSX to avoid eager evaluation)
485
937
  const renderProps = useRenderProps(
486
938
  {
487
- children: props.children,
488
939
  class: local.class,
489
940
  style: local.style,
490
- defaultClassName: 'solidaria-Table-column',
941
+ defaultClassName: "solidaria-Table-column",
491
942
  },
492
- renderValues
943
+ renderValues,
493
944
  );
494
945
 
495
- // Remove ref from spread props
496
946
  const cleanColumnHeaderProps = () => {
497
- const { ref: _ref1, ...rest } = columnHeaderProps as Record<string, unknown>;
947
+ const { ref: _ref1, ...rest } = columnHeaderAria.columnHeaderProps as Record<string, unknown>;
498
948
  return rest;
499
949
  };
500
950
  const cleanHoverProps = () => {
@@ -506,21 +956,59 @@ export function TableColumn(props: TableColumnProps): JSX.Element {
506
956
  return rest;
507
957
  };
508
958
 
509
- return (
959
+ const columnStyle = createMemo(() => {
960
+ const base = renderProps.style();
961
+ const rw = resizeWidth();
962
+ if (rw == null) return base;
963
+ const widthStyle = { width: `${rw}px`, "min-width": `${rw}px`, "max-width": `${rw}px` };
964
+ if (!base) return widthStyle;
965
+ if (typeof base === "string") return widthStyle; // fallback
966
+ return { ...base, ...widthStyle };
967
+ });
968
+
969
+ const columnChildren = () =>
970
+ typeof local.children === "function" ? local.children(renderValues()) : local.children;
971
+ const columnProps = () =>
972
+ ({
973
+ ref: (el: HTMLTableCellElement) => {
974
+ setRef(el);
975
+ assignRef(local.ref, el);
976
+ },
977
+ ...domProps,
978
+ ...mergeProps(cleanColumnHeaderProps(), cleanHoverProps(), cleanFocusProps()),
979
+ class: renderProps.class(),
980
+ style: columnStyle(),
981
+ "data-sortable": local.allowsSorting || undefined,
982
+ "data-sort-direction": sortDirection() || undefined,
983
+ "data-resizable": local.allowsResizing || undefined,
984
+ "data-resizing": isResizing() || undefined,
985
+ "data-hovered": isHovered() || undefined,
986
+ "data-focused": state.focusedKey === local.id || undefined,
987
+ "data-focus-visible": (isFocusVisible() && state.focusedKey === local.id) || undefined,
988
+ children: columnChildren(),
989
+ }) as JSX.ThHTMLAttributes<HTMLTableCellElement>;
990
+
991
+ return local.render ? (
992
+ local.render(columnProps(), renderValues())
993
+ ) : (
510
994
  <th
511
- ref={setRef}
512
- {...cleanColumnHeaderProps()}
513
- {...cleanHoverProps()}
514
- {...cleanFocusProps()}
995
+ ref={(el) => {
996
+ setRef(el);
997
+ assignRef(local.ref, el);
998
+ }}
999
+ {...domProps}
1000
+ {...mergeProps(cleanColumnHeaderProps(), cleanHoverProps(), cleanFocusProps())}
515
1001
  class={renderProps.class()}
516
- style={renderProps.style()}
1002
+ style={columnStyle()}
517
1003
  data-sortable={local.allowsSorting || undefined}
518
1004
  data-sort-direction={sortDirection() || undefined}
1005
+ data-resizable={local.allowsResizing || undefined}
1006
+ data-resizing={isResizing() || undefined}
519
1007
  data-hovered={isHovered() || undefined}
520
1008
  data-focused={state.focusedKey === local.id || undefined}
521
1009
  data-focus-visible={(isFocusVisible() && state.focusedKey === local.id) || undefined}
522
1010
  >
523
- {renderProps.renderChildren()}
1011
+ {columnChildren()}
524
1012
  </th>
525
1013
  );
526
1014
  }
@@ -529,32 +1017,40 @@ export function TableColumn(props: TableColumnProps): JSX.Element {
529
1017
  * The body of a table containing data rows.
530
1018
  */
531
1019
  export function TableBody<T extends object>(props: TableBodyProps<T>): JSX.Element {
532
- const [local] = splitProps(props, ['items', 'class', 'style', 'slot', 'renderEmptyState']);
1020
+ const [local, domProps] = splitProps(props, [
1021
+ "items",
1022
+ "class",
1023
+ "style",
1024
+ "render",
1025
+ "slot",
1026
+ "renderEmptyState",
1027
+ "hasMore",
1028
+ "isLoading",
1029
+ "onLoadMore",
1030
+ "children",
1031
+ "ref",
1032
+ ]);
533
1033
 
534
- // Get context
535
1034
  const context = useContext(TableContext);
536
1035
  if (!context) {
537
- throw new Error('TableBody must be used within a Table');
1036
+ throw new Error("TableBody must be used within a Table");
538
1037
  }
539
1038
 
540
- const { rowGroupProps } = createTableRowGroup(() => ({ type: 'tbody' }));
1039
+ const { rowGroupProps } = createTableRowGroup(() => ({ type: "tbody" }));
541
1040
 
542
- // Use provided items or context items
543
1041
  const items = createMemo(() => (local.items ?? context.items) as T[]);
544
1042
 
545
- // Render props values
546
1043
  const renderValues = createMemo<TableBodyRenderProps>(() => ({
547
1044
  isEmpty: items().length === 0,
548
1045
  }));
549
1046
 
550
- // Resolve render props
551
1047
  const renderProps = useRenderProps(
552
1048
  {
553
1049
  class: local.class,
554
1050
  style: local.style,
555
- defaultClassName: 'solidaria-Table-body',
1051
+ defaultClassName: "solidaria-Table-body",
556
1052
  },
557
- renderValues
1053
+ renderValues,
558
1054
  );
559
1055
 
560
1056
  const cleanRowGroupProps = () => {
@@ -563,13 +1059,350 @@ export function TableBody<T extends object>(props: TableBodyProps<T>): JSX.Eleme
563
1059
  };
564
1060
 
565
1061
  const isEmpty = () => items().length === 0;
1062
+ const virtualizer = useVirtualizerContext();
1063
+ const parentCollectionRenderer = useCollectionRenderer<T>();
1064
+ const rowNodes = createMemo(() =>
1065
+ Array.from(context.collection).filter((node) => node.type === "item"),
1066
+ );
1067
+ const persistedKeys = useDndPersistedKeys(
1068
+ { focusedKey: () => context.state.focusedKey },
1069
+ context.dragAndDropHooks,
1070
+ context.dropState as { target?: DropTarget | null } | undefined,
1071
+ context.collection,
1072
+ );
1073
+ const virtualRange = createMemo(() => {
1074
+ if (!virtualizer || !parentCollectionRenderer?.isVirtualized) return null;
1075
+ const rowCount = items().length;
1076
+ const baseRange = virtualizer.getVisibleRange(rowCount);
1077
+ const persistedIndexes = Array.from(persistedKeys())
1078
+ .map((key) => rowNodes().findIndex((node) => node.key === key))
1079
+ .filter((index) => index >= 0);
1080
+ const dropTarget = (context.dropState as { target?: DropTarget | null } | undefined)?.target;
1081
+ const normalizedDropKey = getNormalizedDropTargetKey(dropTarget, context.collection);
1082
+ const focusedKey = context.state.focusedKey;
1083
+ const focusedIndex =
1084
+ focusedKey != null ? rowNodes().findIndex((node) => node.key === focusedKey) : -1;
1085
+ const forceIncludeIndexes = [
1086
+ dropTarget?.type === "item"
1087
+ ? rowNodes().findIndex((node) => node.key === dropTarget.key)
1088
+ : -1,
1089
+ normalizedDropKey != null
1090
+ ? rowNodes().findIndex((node) => node.key === normalizedDropKey)
1091
+ : -1,
1092
+ dropTarget?.type === "item" ? -1 : focusedIndex,
1093
+ ].filter((index) => index >= 0);
1094
+ return mergePersistedKeysIntoVirtualRange(
1095
+ baseRange,
1096
+ persistedIndexes,
1097
+ rowCount,
1098
+ virtualizer,
1099
+ 80,
1100
+ {
1101
+ forceIncludeIndexes,
1102
+ forceIncludeMaxSpan: 320,
1103
+ },
1104
+ );
1105
+ });
1106
+ createEffect(() => {
1107
+ if (!virtualizer || !parentCollectionRenderer?.isVirtualized) return;
1108
+ virtualizer.setDropTargetItemCountResolver(() => items().length);
1109
+ virtualizer.setDropTargetIndexResolver((key) => {
1110
+ const index = rowNodes().findIndex((node) => node.key === key);
1111
+ return index >= 0 ? index : null;
1112
+ });
1113
+ virtualizer.setDropTargetResolver((target) => {
1114
+ const node = rowNodes()[target.index];
1115
+ if (!node) return target;
1116
+ return {
1117
+ ...target,
1118
+ key: typeof node.key === "string" || typeof node.key === "number" ? node.key : undefined,
1119
+ };
1120
+ });
1121
+ onCleanup(() => {
1122
+ virtualizer.setDropTargetIndexResolver(undefined);
1123
+ virtualizer.setDropTargetItemCountResolver(undefined);
1124
+ virtualizer.setDropTargetResolver(undefined);
1125
+ });
1126
+ });
1127
+ const visibleItems = createMemo(() => {
1128
+ const range = virtualRange();
1129
+ if (!range) return items();
1130
+ return items().slice(range.start, range.end);
1131
+ });
1132
+ const spacerColSpan = () => context.columns.length + (context.showSelectionCheckboxes ? 1 : 0);
1133
+
1134
+ const bodyChildren = () => (
1135
+ <>
1136
+ <SharedElementTransition>
1137
+ <Show
1138
+ when={isEmpty() && local.renderEmptyState && !local.isLoading}
1139
+ fallback={
1140
+ <>
1141
+ {virtualRange()?.offsetTop ? (
1142
+ <tr role="presentation" aria-hidden="true" data-virtualizer-spacer="top">
1143
+ <td
1144
+ colSpan={spacerColSpan()}
1145
+ style={{ height: `${virtualRange()!.offsetTop}px`, padding: "0", border: "0" }}
1146
+ />
1147
+ </tr>
1148
+ ) : null}
1149
+ <For each={visibleItems()}>
1150
+ {(item, index) => {
1151
+ const itemIndex = () => (virtualRange()?.start ?? 0) + index();
1152
+ const beforeIndicator = () =>
1153
+ parentCollectionRenderer?.renderDropIndicator?.(itemIndex(), "before");
1154
+ const onIndicator = () =>
1155
+ parentCollectionRenderer?.renderDropIndicator?.(itemIndex(), "on");
1156
+ const afterIndicator = () =>
1157
+ parentCollectionRenderer?.renderDropIndicator?.(itemIndex(), "after");
1158
+ return (
1159
+ <>
1160
+ {beforeIndicator()}
1161
+ {onIndicator()}
1162
+ {local.children?.(item)}
1163
+ {afterIndicator()}
1164
+ </>
1165
+ );
1166
+ }}
1167
+ </For>
1168
+ {virtualRange()?.offsetBottom ? (
1169
+ <tr role="presentation" aria-hidden="true" data-virtualizer-spacer="bottom">
1170
+ <td
1171
+ colSpan={spacerColSpan()}
1172
+ style={{
1173
+ height: `${virtualRange()!.offsetBottom}px`,
1174
+ padding: "0",
1175
+ border: "0",
1176
+ }}
1177
+ />
1178
+ </tr>
1179
+ ) : null}
1180
+ </>
1181
+ }
1182
+ >
1183
+ <tr role="row" data-empty-state>
1184
+ <th role="rowheader" colSpan={spacerColSpan()}>
1185
+ {local.renderEmptyState?.()}
1186
+ </th>
1187
+ </tr>
1188
+ </Show>
1189
+ </SharedElementTransition>
1190
+ <Show when={local.hasMore && local.onLoadMore}>
1191
+ <TableLoadMoreItem
1192
+ onLoadMore={local.onLoadMore!}
1193
+ isLoading={local.isLoading}
1194
+ colSpan={spacerColSpan()}
1195
+ />
1196
+ </Show>
1197
+ </>
1198
+ );
1199
+ const bodyProps = () =>
1200
+ ({
1201
+ ref: (el: HTMLTableSectionElement) => assignRef(local.ref, el),
1202
+ ...domProps,
1203
+ ...cleanRowGroupProps(),
1204
+ class: renderProps.class(),
1205
+ style: renderProps.style(),
1206
+ "data-empty": isEmpty() || undefined,
1207
+ children: bodyChildren(),
1208
+ }) as JSX.HTMLAttributes<HTMLTableSectionElement>;
1209
+
1210
+ return local.render ? (
1211
+ local.render(bodyProps(), renderValues())
1212
+ ) : (
1213
+ <tbody
1214
+ ref={(el) => assignRef(local.ref, el)}
1215
+ {...domProps}
1216
+ {...cleanRowGroupProps()}
1217
+ class={renderProps.class()}
1218
+ style={renderProps.style()}
1219
+ data-empty={isEmpty() || undefined}
1220
+ >
1221
+ <SharedElementTransition>
1222
+ <Show
1223
+ when={isEmpty() && local.renderEmptyState && !local.isLoading}
1224
+ fallback={
1225
+ <>
1226
+ {virtualRange()?.offsetTop ? (
1227
+ <tr role="presentation" aria-hidden="true" data-virtualizer-spacer="top">
1228
+ <td
1229
+ colSpan={spacerColSpan()}
1230
+ style={{ height: `${virtualRange()!.offsetTop}px`, padding: "0", border: "0" }}
1231
+ />
1232
+ </tr>
1233
+ ) : null}
1234
+ <For each={visibleItems()}>
1235
+ {(item, index) => {
1236
+ const itemIndex = () => (virtualRange()?.start ?? 0) + index();
1237
+ const beforeIndicator = () =>
1238
+ parentCollectionRenderer?.renderDropIndicator?.(itemIndex(), "before");
1239
+ const onIndicator = () =>
1240
+ parentCollectionRenderer?.renderDropIndicator?.(itemIndex(), "on");
1241
+ const afterIndicator = () =>
1242
+ parentCollectionRenderer?.renderDropIndicator?.(itemIndex(), "after");
1243
+ return (
1244
+ <>
1245
+ {beforeIndicator()}
1246
+ {onIndicator()}
1247
+ {local.children?.(item)}
1248
+ {afterIndicator()}
1249
+ </>
1250
+ );
1251
+ }}
1252
+ </For>
1253
+ {virtualRange()?.offsetBottom ? (
1254
+ <tr role="presentation" aria-hidden="true" data-virtualizer-spacer="bottom">
1255
+ <td
1256
+ colSpan={spacerColSpan()}
1257
+ style={{
1258
+ height: `${virtualRange()!.offsetBottom}px`,
1259
+ padding: "0",
1260
+ border: "0",
1261
+ }}
1262
+ />
1263
+ </tr>
1264
+ ) : null}
1265
+ </>
1266
+ }
1267
+ >
1268
+ <tr role="row" data-empty-state>
1269
+ <th role="rowheader" colSpan={spacerColSpan()}>
1270
+ {local.renderEmptyState?.()}
1271
+ </th>
1272
+ </tr>
1273
+ </Show>
1274
+ </SharedElementTransition>
1275
+ <Show when={local.hasMore && local.onLoadMore}>
1276
+ <TableLoadMoreItem
1277
+ onLoadMore={local.onLoadMore!}
1278
+ isLoading={local.isLoading}
1279
+ colSpan={spacerColSpan()}
1280
+ />
1281
+ </Show>
1282
+ </tbody>
1283
+ );
1284
+ }
1285
+
1286
+ /**
1287
+ * The footer of a table containing summary rows.
1288
+ */
1289
+ export function TableFooter<T extends object>(props: TableFooterProps<T>): JSX.Element {
1290
+ const [local, domProps] = splitProps(props, ["items", "class", "style", "slot", "children"]);
1291
+
1292
+ const context = useContext(TableContext);
1293
+ if (!context) {
1294
+ throw new Error("TableFooter must be used within a Table");
1295
+ }
1296
+
1297
+ const { rowGroupProps } = createTableRowGroup(() => ({ type: "tfoot" }));
1298
+ const items = createMemo(() => local.items ?? []);
1299
+
1300
+ const renderValues = createMemo<TableFooterRenderProps>(() => ({
1301
+ isEmpty: items().length === 0,
1302
+ }));
1303
+
1304
+ const renderProps = useRenderProps(
1305
+ {
1306
+ class: local.class,
1307
+ style: local.style,
1308
+ defaultClassName: "solidaria-Table-footer",
1309
+ },
1310
+ renderValues,
1311
+ );
1312
+
1313
+ const cleanRowGroupProps = () => {
1314
+ const { ref: _ref, ...rest } = rowGroupProps as Record<string, unknown>;
1315
+ return rest;
1316
+ };
566
1317
 
567
1318
  return (
568
- <tbody {...cleanRowGroupProps()} class={renderProps.class()} style={renderProps.style()}>
569
- <Show when={isEmpty() && local.renderEmptyState} fallback={<For each={items()}>{(item) => props.children?.(item)}</For>}>
570
- {local.renderEmptyState?.()}
1319
+ <tfoot
1320
+ {...domProps}
1321
+ {...cleanRowGroupProps()}
1322
+ class={renderProps.class()}
1323
+ style={renderProps.style()}
1324
+ >
1325
+ <Show
1326
+ when={local.items && typeof local.children === "function"}
1327
+ fallback={local.children as JSX.Element}
1328
+ >
1329
+ <For each={items()}>{(item) => (local.children as (item: T) => JSX.Element)(item)}</For>
571
1330
  </Show>
572
- </tbody>
1331
+ </tfoot>
1332
+ );
1333
+ }
1334
+
1335
+ export function TableLoadMoreItem(props: TableLoadMoreItemProps): JSX.Element {
1336
+ let sentinelRef: HTMLDivElement | undefined;
1337
+ const [isPending, setIsPending] = createSignal(false);
1338
+ const isLoading = () => !!props.isLoading || isPending();
1339
+
1340
+ const triggerLoadMore = async () => {
1341
+ if (isPending()) return;
1342
+ setIsPending(true);
1343
+ try {
1344
+ await props.onLoadMore();
1345
+ } finally {
1346
+ setIsPending(false);
1347
+ }
1348
+ };
1349
+
1350
+ createEffect(() => {
1351
+ if (!sentinelRef || typeof IntersectionObserver !== "function") return;
1352
+ const offset = props.scrollOffset ?? 1;
1353
+ const margin = `0px 0px ${100 * offset}% 0px`;
1354
+ const observer = new IntersectionObserver(
1355
+ (entries) => {
1356
+ if (entries[0]?.isIntersecting) {
1357
+ void triggerLoadMore();
1358
+ }
1359
+ },
1360
+ { rootMargin: margin },
1361
+ );
1362
+ observer.observe(sentinelRef);
1363
+ return () => observer.disconnect();
1364
+ });
1365
+
1366
+ const renderProps = useRenderProps(
1367
+ {
1368
+ children:
1369
+ props.children ??
1370
+ (() => (isLoading() ? <div role="progressbar" aria-label="loading" /> : null)),
1371
+ class: props.class,
1372
+ style: props.style,
1373
+ defaultClassName: "solidaria-Table-loadMore",
1374
+ },
1375
+ () => ({ isLoading: isLoading() }),
1376
+ );
1377
+
1378
+ return (
1379
+ <>
1380
+ <tr style={{ position: "relative", width: 0, height: 0, overflow: "hidden" }} inert>
1381
+ <td>
1382
+ <div
1383
+ ref={sentinelRef}
1384
+ data-testid="loadMoreSentinel"
1385
+ style={{ position: "absolute", height: "1px", width: "1px" }}
1386
+ />
1387
+ </td>
1388
+ </tr>
1389
+ <Show when={isLoading()}>
1390
+ <tr
1391
+ role="row"
1392
+ tabIndex={0}
1393
+ onFocus={() => {
1394
+ void triggerLoadMore();
1395
+ }}
1396
+ class={renderProps.class()}
1397
+ style={renderProps.style()}
1398
+ data-loading
1399
+ >
1400
+ <td role="rowheader" colSpan={props.colSpan ?? 1}>
1401
+ {renderProps.renderChildren()}
1402
+ </td>
1403
+ </tr>
1404
+ </Show>
1405
+ </>
573
1406
  );
574
1407
  }
575
1408
 
@@ -577,26 +1410,56 @@ export function TableBody<T extends object>(props: TableBodyProps<T>): JSX.Eleme
577
1410
  * A row in a table.
578
1411
  */
579
1412
  export function TableRow<T extends object>(props: TableRowProps<T>): JSX.Element {
580
- const [local] = splitProps(props, ['class', 'style', 'slot', 'id', 'item', 'onAction']);
1413
+ const [local, domProps] = splitProps(props, [
1414
+ "class",
1415
+ "style",
1416
+ "render",
1417
+ "slot",
1418
+ "id",
1419
+ "item",
1420
+ "columns",
1421
+ "isDisabled",
1422
+ "onAction",
1423
+ "children",
1424
+ "ref",
1425
+ "href",
1426
+ "target",
1427
+ "rel",
1428
+ "download",
1429
+ "ping",
1430
+ "referrerPolicy",
1431
+ "routerOptions",
1432
+ ]);
581
1433
 
582
- // Get context
583
1434
  const context = useContext(TableContext);
584
1435
  if (!context) {
585
- throw new Error('TableRow must be used within a Table');
1436
+ throw new Error("TableRow must be used within a Table");
586
1437
  }
587
1438
  const { state, collection } = context;
1439
+ const tableContext = context as unknown as TableContextValue<T>;
1440
+ const registeredCellIds: string[] = [];
1441
+ const generatedId = createUniqueId();
1442
+ const rowKey = () => local.id ?? generatedId;
1443
+ const router = useRouter();
1444
+ const linkProps = createMemo(() =>
1445
+ useLinkProps({
1446
+ href: local.href,
1447
+ target: local.target,
1448
+ rel: local.rel,
1449
+ download: local.download,
1450
+ ping: local.ping,
1451
+ referrerPolicy: local.referrerPolicy,
1452
+ }),
1453
+ );
588
1454
 
589
- // Create ref signal
590
1455
  const [ref, setRef] = createSignal<HTMLTableRowElement | null>(null);
591
1456
 
592
- // Find the row node
593
1457
  const rowNode = createMemo(() => {
594
- const node = collection.getItem(local.id);
1458
+ const node = collection.getItem(rowKey());
595
1459
  if (!node) {
596
- // Create a simple node for the row
597
1460
  return {
598
- type: 'item' as const,
599
- key: local.id,
1461
+ type: "item" as const,
1462
+ key: rowKey(),
600
1463
  value: local.item ?? null,
601
1464
  textValue: String(local.id),
602
1465
  level: 0,
@@ -608,53 +1471,105 @@ export function TableRow<T extends object>(props: TableRowProps<T>): JSX.Element
608
1471
  return node;
609
1472
  });
610
1473
 
611
- // Create row aria props
612
- const { rowProps, isSelected, isDisabled, isPressed } = createTableRow<object>(
1474
+ const rowAria = createTableRow<object>(
613
1475
  () => ({
614
1476
  node: rowNode(),
1477
+ isVirtualized: tableContext.isVirtualized,
1478
+ isDisabled: local.isDisabled,
615
1479
  onAction: local.onAction,
1480
+ href: linkProps().href,
1481
+ onLinkAction: (event) => {
1482
+ const target = ref();
1483
+ if (!target || !local.href) return;
1484
+ router.open(target, event as RouterClickModifiers, local.href, local.routerOptions);
1485
+ },
616
1486
  }),
617
1487
  () => state as TableState<object, TableCollection<object>>,
618
- ref
1488
+ ref,
619
1489
  );
1490
+ const isSelected = () => rowAria.isSelected;
1491
+ const isDisabled = () => rowAria.isDisabled;
1492
+ const isPressed = () => rowAria.isPressed;
1493
+ const isInteractive = () => {
1494
+ const tableData = getTableData(state as TableState<object, TableCollection<object>>);
1495
+ return state.selectionMode !== "none" || !!tableData?.actions.onRowAction || !!local.onAction;
1496
+ };
620
1497
 
621
- // Create hover
622
1498
  const { isHovered, hoverProps } = createHover({
623
1499
  get isDisabled() {
624
- return isDisabled;
1500
+ return isDisabled() || !isInteractive();
1501
+ },
1502
+ onHoverStart(e) {
1503
+ (domProps as Record<string, (e: unknown) => void>).onHoverStart?.(e);
1504
+ },
1505
+ onHoverEnd(e) {
1506
+ (domProps as Record<string, (e: unknown) => void>).onHoverEnd?.(e);
1507
+ },
1508
+ onHoverChange(isHovering) {
1509
+ (domProps as Record<string, (isHovering: boolean) => void>).onHoverChange?.(isHovering);
625
1510
  },
626
1511
  });
627
1512
 
628
- // Create focus ring
629
1513
  const { isFocusVisible, focusProps } = createFocusRing();
1514
+ const [isFocusWithin, setIsFocusWithin] = createSignal(false);
1515
+ const focusWithinProps = {
1516
+ onFocusIn() {
1517
+ setIsFocusWithin(true);
1518
+ },
1519
+ onFocusOut(e: FocusEvent) {
1520
+ const currentTarget = e.currentTarget as HTMLElement;
1521
+ const nextTarget = e.relatedTarget as Node | null;
1522
+ if (!nextTarget || !currentTarget.contains(nextTarget)) {
1523
+ setIsFocusWithin(false);
1524
+ }
1525
+ },
1526
+ };
630
1527
 
631
- // Check if focused
632
- const isFocused = createMemo(() => state.focusedKey === local.id);
1528
+ const isFocused = createMemo(() => state.focusedKey === rowKey());
1529
+ const draggableItem = createMemo(() => {
1530
+ if (!tableContext.dragAndDropHooks?.useDraggableItem || !tableContext.dragState)
1531
+ return undefined;
1532
+ return tableContext.dragAndDropHooks.useDraggableItem(
1533
+ {
1534
+ key: rowKey() as string | number,
1535
+ hasDragButton: true,
1536
+ },
1537
+ tableContext.dragState as Parameters<NonNullable<DragAndDropHooks<T>["useDraggableItem"]>>[1],
1538
+ );
1539
+ });
1540
+ const droppableItem = createMemo(() => {
1541
+ if (!tableContext.dragAndDropHooks?.useDroppableItem || !tableContext.dropState)
1542
+ return undefined;
1543
+ return tableContext.dragAndDropHooks.useDroppableItem(
1544
+ {
1545
+ key: rowKey() as string | number,
1546
+ },
1547
+ tableContext.dropState as Parameters<NonNullable<DragAndDropHooks<T>["useDroppableItem"]>>[1],
1548
+ () => ref(),
1549
+ );
1550
+ });
633
1551
 
634
- // Render props values
635
1552
  const renderValues = createMemo<TableRowRenderProps>(() => ({
636
- isSelected,
1553
+ isSelected: isSelected(),
637
1554
  isFocused: isFocused(),
638
1555
  isFocusVisible: isFocusVisible() && isFocused(),
639
- isPressed,
1556
+ isPressed: isPressed(),
640
1557
  isHovered: isHovered(),
641
- isDisabled,
1558
+ isDisabled: isDisabled(),
642
1559
  }));
643
1560
 
644
- // Resolve render props
1561
+ // Resolve render props (children rendered directly in JSX to avoid eager evaluation)
645
1562
  const renderProps = useRenderProps(
646
1563
  {
647
- children: props.children,
648
1564
  class: local.class,
649
1565
  style: local.style,
650
- defaultClassName: 'solidaria-Table-row',
1566
+ defaultClassName: "solidaria-Table-row",
651
1567
  },
652
- renderValues
1568
+ renderValues,
653
1569
  );
654
1570
 
655
- // Remove ref from spread props
656
1571
  const cleanRowProps = () => {
657
- const { ref: _ref1, ...rest } = rowProps as Record<string, unknown>;
1572
+ const { ref: _ref1, ...rest } = rowAria.rowProps as Record<string, unknown>;
658
1573
  return rest;
659
1574
  };
660
1575
  const cleanHoverProps = () => {
@@ -667,28 +1582,101 @@ export function TableRow<T extends object>(props: TableRowProps<T>): JSX.Element
667
1582
  };
668
1583
 
669
1584
  const rowContextValue: TableRowContextValue = {
670
- rowKey: local.id,
1585
+ rowKey: rowKey(),
671
1586
  rowNode: rowNode(),
1587
+ getCellColumnKey(cellId, explicitId) {
1588
+ if (explicitId === "__selection__") {
1589
+ return explicitId;
1590
+ }
1591
+
1592
+ let index = registeredCellIds.indexOf(cellId);
1593
+ if (index < 0) {
1594
+ index = registeredCellIds.length;
1595
+ registeredCellIds.push(cellId);
1596
+ }
1597
+
1598
+ return explicitId ?? tableContext.columns[index]?.key;
1599
+ },
672
1600
  };
1601
+ const dragButtonProps = createMemo<ButtonProps>(() => {
1602
+ const props = (draggableItem()?.dragButtonProps as ButtonProps | undefined) ?? {};
1603
+ const textValue = (rowNode().textValue || String(rowKey())).trim();
1604
+ return {
1605
+ ...props,
1606
+ "aria-label": `Drag ${textValue}`,
1607
+ style: {
1608
+ ...(typeof props.style === "object" ? props.style : {}),
1609
+ "pointer-events": "none",
1610
+ },
1611
+ };
1612
+ });
1613
+ const buttonContextValue = createMemo(() => ({
1614
+ slots: {
1615
+ default: {},
1616
+ drag: dragButtonProps(),
1617
+ },
1618
+ }));
1619
+
1620
+ const rowChildren = () => (
1621
+ <ButtonContext.Provider value={buttonContextValue()}>
1622
+ {typeof local.children === "function" ? (
1623
+ local.columns ? (
1624
+ <For each={local.columns}>
1625
+ {(column) =>
1626
+ (local.children as (column: TableColumnDefinition<T>) => JSX.Element)(column)
1627
+ }
1628
+ </For>
1629
+ ) : (
1630
+ (local.children as (renderProps: TableRowRenderProps) => JSX.Element)(renderValues())
1631
+ )
1632
+ ) : (
1633
+ local.children
1634
+ )}
1635
+ </ButtonContext.Provider>
1636
+ );
1637
+ const tableRowProps = () =>
1638
+ ({
1639
+ ref: (el: HTMLTableRowElement) => {
1640
+ setRef(el);
1641
+ assignRef(local.ref, el);
1642
+ },
1643
+ ...domProps,
1644
+ ...mergeProps(
1645
+ cleanRowProps(),
1646
+ cleanHoverProps(),
1647
+ cleanFocusProps(),
1648
+ focusWithinProps as Record<string, unknown>,
1649
+ (draggableItem()?.dragProps as Record<string, unknown> | undefined) ?? {},
1650
+ (droppableItem()?.dropProps as Record<string, unknown> | undefined) ?? {},
1651
+ ),
1652
+ class: renderProps.class(),
1653
+ style: renderProps.style(),
1654
+ "data-selected": isSelected() || undefined,
1655
+ "data-focused": isFocused() || undefined,
1656
+ "data-focus-visible": (isFocusVisible() && isFocused()) || undefined,
1657
+ "data-focus-visible-within": dataAttr(isFocusWithin() && isGlobalFocusVisible()),
1658
+ "data-pressed": isPressed() || undefined,
1659
+ "data-hovered": isHovered() || undefined,
1660
+ "data-disabled": isDisabled() || undefined,
1661
+ "data-href": linkProps().href,
1662
+ "data-target": linkProps().target,
1663
+ "data-rel": linkProps().rel,
1664
+ "data-download":
1665
+ typeof linkProps().download === "string"
1666
+ ? linkProps().download
1667
+ : linkProps().download
1668
+ ? ""
1669
+ : undefined,
1670
+ "data-ping": linkProps().ping,
1671
+ "data-referrer-policy": linkProps().referrerPolicy,
1672
+ "data-dragging": draggableItem()?.isDragging || undefined,
1673
+ "data-drop-target": droppableItem()?.isDropTarget || undefined,
1674
+ children: rowChildren(),
1675
+ }) as JSX.HTMLAttributes<HTMLTableRowElement>;
673
1676
 
674
1677
  return (
675
1678
  <TableRowContext.Provider value={rowContextValue}>
676
- <tr
677
- ref={setRef}
678
- {...cleanRowProps()}
679
- {...cleanHoverProps()}
680
- {...cleanFocusProps()}
681
- class={renderProps.class()}
682
- style={renderProps.style()}
683
- data-selected={isSelected || undefined}
684
- data-focused={isFocused() || undefined}
685
- data-focus-visible={(isFocusVisible() && isFocused()) || undefined}
686
- data-pressed={isPressed || undefined}
687
- data-hovered={isHovered() || undefined}
688
- data-disabled={isDisabled || undefined}
689
- >
690
- {renderProps.renderChildren()}
691
- </tr>
1679
+ {local.render ? local.render(tableRowProps(), renderValues()) : <tr {...tableRowProps()} />}
692
1680
  </TableRowContext.Provider>
693
1681
  );
694
1682
  }
@@ -697,40 +1685,47 @@ export function TableRow<T extends object>(props: TableRowProps<T>): JSX.Element
697
1685
  * A cell in a table row.
698
1686
  */
699
1687
  export function TableCell(props: TableCellProps): JSX.Element {
700
- const [local] = splitProps(props, ['class', 'style', 'slot', 'id']);
1688
+ const [local, domProps] = splitProps(props, [
1689
+ "class",
1690
+ "style",
1691
+ "render",
1692
+ "slot",
1693
+ "id",
1694
+ "colSpan",
1695
+ "children",
1696
+ "ref",
1697
+ ]);
701
1698
 
702
- // Get context
703
1699
  const tableContext = useContext(TableContext);
704
1700
  const rowContext = useContext(TableRowContext);
705
1701
 
706
1702
  if (!tableContext) {
707
- throw new Error('TableCell must be used within a Table');
1703
+ throw new Error("TableCell must be used within a Table");
708
1704
  }
709
1705
  if (!rowContext) {
710
- throw new Error('TableCell must be used within a Table');
1706
+ throw new Error("TableCell must be used within a Table");
711
1707
  }
712
1708
 
713
1709
  const { state, collection } = tableContext;
714
1710
  const { rowKey, rowNode } = rowContext;
715
1711
 
716
- // Create ref signal
717
1712
  const [ref, setRef] = createSignal<HTMLTableCellElement | null>(null);
1713
+ const cellId = createUniqueId();
1714
+ const columnKey = createMemo(() => rowContext.getCellColumnKey(cellId, local.id));
718
1715
 
719
- // Find the cell node
720
1716
  const cellNode = createMemo(() => {
721
- // If id is provided, look for that specific cell
722
- if (local.id != null) {
723
- const cellKey = `${rowKey}-${local.id}`;
1717
+ const key = columnKey();
1718
+ if (key != null) {
1719
+ const cellKey = `${rowKey}-${key}`;
724
1720
  const node = collection.getItem(cellKey);
725
1721
  if (node) return node;
726
1722
  }
727
1723
 
728
- // Otherwise create a simple node
729
1724
  return {
730
- type: 'cell' as const,
731
- key: local.id ?? `${rowKey}-cell`,
1725
+ type: "cell" as const,
1726
+ key: key ?? `${rowKey}-cell`,
732
1727
  value: rowNode.value,
733
- textValue: '',
1728
+ textValue: "",
734
1729
  level: 1,
735
1730
  index: 0,
736
1731
  parentKey: rowKey,
@@ -738,49 +1733,50 @@ export function TableCell(props: TableCellProps): JSX.Element {
738
1733
  childNodes: [],
739
1734
  } as GridNode<unknown>;
740
1735
  });
1736
+ const cellColumnIndex = createMemo(() => {
1737
+ const key = columnKey();
1738
+ if (key == null) return undefined;
1739
+ const cellKey = `${rowKey}-${key}`;
1740
+ return collection.getItem(cellKey) ? (cellNode().index ?? 0) : undefined;
1741
+ });
741
1742
 
742
- // Create cell aria props
743
- const { gridCellProps, isPressed } = createTableCell<object>(
1743
+ const cellAria = createTableCell<object>(
744
1744
  () => ({
745
1745
  node: cellNode(),
746
1746
  }),
747
1747
  () => state as TableState<object, TableCollection<object>>,
748
- ref
1748
+ ref,
749
1749
  );
1750
+ const isPressed = () => cellAria.isPressed;
750
1751
 
751
- // Create hover
752
1752
  const { isHovered, hoverProps } = createHover({
753
1753
  isDisabled: false,
754
1754
  });
755
1755
 
756
- // Create focus ring
757
1756
  const { isFocusVisible, focusProps } = createFocusRing();
758
1757
 
759
- // Check if focused
760
1758
  const isFocused = createMemo(() => state.focusedKey === cellNode().key);
761
1759
 
762
- // Render props values
763
1760
  const renderValues = createMemo<TableCellRenderProps>(() => ({
764
1761
  isFocused: isFocused(),
765
1762
  isFocusVisible: isFocusVisible() && isFocused(),
766
- isPressed,
1763
+ columnIndex: cellColumnIndex() ?? 0,
1764
+ isPressed: isPressed(),
767
1765
  isHovered: isHovered(),
768
1766
  }));
769
1767
 
770
- // Resolve render props
1768
+ // Resolve render props (children rendered directly in JSX to avoid eager evaluation)
771
1769
  const renderProps = useRenderProps(
772
1770
  {
773
- children: props.children,
774
1771
  class: local.class,
775
1772
  style: local.style,
776
- defaultClassName: 'solidaria-Table-cell',
1773
+ defaultClassName: "solidaria-Table-cell",
777
1774
  },
778
- renderValues
1775
+ renderValues,
779
1776
  );
780
1777
 
781
- // Remove ref from spread props
782
1778
  const cleanCellProps = () => {
783
- const { ref: _ref1, ...rest } = gridCellProps as Record<string, unknown>;
1779
+ const { ref: _ref1, ...rest } = cellAria.gridCellProps as Record<string, unknown>;
784
1780
  return rest;
785
1781
  };
786
1782
  const cleanHoverProps = () => {
@@ -792,66 +1788,396 @@ export function TableCell(props: TableCellProps): JSX.Element {
792
1788
  return rest;
793
1789
  };
794
1790
 
795
- return (
1791
+ const cellChildren = () =>
1792
+ typeof local.children === "function" ? local.children(renderValues()) : local.children;
1793
+ const tableCellProps = () =>
1794
+ ({
1795
+ ref: (el: HTMLTableCellElement) => {
1796
+ setRef(el);
1797
+ assignRef(local.ref, el);
1798
+ },
1799
+ ...domProps,
1800
+ ...mergeProps(cleanCellProps(), cleanHoverProps(), cleanFocusProps()),
1801
+ colSpan: local.colSpan,
1802
+ class: renderProps.class(),
1803
+ style: renderProps.style(),
1804
+ "data-focused": isFocused() || undefined,
1805
+ "data-focus-visible": (isFocusVisible() && isFocused()) || undefined,
1806
+ "data-column-index": cellColumnIndex(),
1807
+ "data-pressed": isPressed() || undefined,
1808
+ "data-hovered": isHovered() || undefined,
1809
+ children: cellChildren(),
1810
+ }) as JSX.TdHTMLAttributes<HTMLTableCellElement>;
1811
+
1812
+ return local.render ? (
1813
+ local.render(tableCellProps(), renderValues())
1814
+ ) : (
796
1815
  <td
797
- ref={setRef}
798
- {...cleanCellProps()}
799
- {...cleanHoverProps()}
800
- {...cleanFocusProps()}
1816
+ ref={(el) => {
1817
+ setRef(el);
1818
+ assignRef(local.ref, el);
1819
+ }}
1820
+ {...domProps}
1821
+ {...mergeProps(cleanCellProps(), cleanHoverProps(), cleanFocusProps())}
1822
+ colSpan={local.colSpan}
801
1823
  class={renderProps.class()}
802
1824
  style={renderProps.style()}
803
1825
  data-focused={isFocused() || undefined}
804
1826
  data-focus-visible={(isFocusVisible() && isFocused()) || undefined}
805
- data-pressed={isPressed || undefined}
1827
+ data-column-index={cellColumnIndex()}
1828
+ data-pressed={isPressed() || undefined}
806
1829
  data-hovered={isHovered() || undefined}
807
1830
  >
808
- {renderProps.renderChildren()}
1831
+ {cellChildren()}
809
1832
  </td>
810
1833
  );
811
1834
  }
812
1835
 
1836
+ export interface TableSelectionCheckboxProps {
1837
+ rowKey: Key;
1838
+ class?: string;
1839
+ style?: JSX.CSSProperties;
1840
+ excludeFromTabOrder?: boolean;
1841
+ "aria-label"?: string;
1842
+ }
1843
+
813
1844
  /**
814
1845
  * A checkbox cell for row selection.
815
1846
  */
816
- export function TableSelectionCheckbox(props: { rowKey: Key }): JSX.Element {
1847
+ export function TableSelectionCheckbox(props: TableSelectionCheckboxProps): JSX.Element {
817
1848
  const context = useContext(TableContext);
818
1849
  if (!context) {
819
- throw new Error('TableSelectionCheckbox must be used within a Table');
1850
+ throw new Error("TableSelectionCheckbox must be used within a Table");
820
1851
  }
821
1852
 
822
1853
  const { state } = context;
823
1854
 
824
- const { checkboxProps } = createTableSelectionCheckbox<object>(
1855
+ const selectionCheckboxAria = createTableSelectionCheckbox<object>(
825
1856
  () => ({ key: props.rowKey }),
826
- () => state as TableState<object, TableCollection<object>>
1857
+ () => state as TableState<object, TableCollection<object>>,
1858
+ );
1859
+
1860
+ return (
1861
+ <input
1862
+ {...selectionCheckboxAria.checkboxProps}
1863
+ class={props.class}
1864
+ style={props.style}
1865
+ tabIndex={props.excludeFromTabOrder ? -1 : selectionCheckboxAria.checkboxProps.tabIndex}
1866
+ aria-label={props["aria-label"] ?? selectionCheckboxAria.checkboxProps["aria-label"]}
1867
+ />
827
1868
  );
1869
+ }
828
1870
 
829
- return <input {...checkboxProps} />;
1871
+ export interface TableSelectAllCheckboxProps {
1872
+ class?: string;
1873
+ style?: JSX.CSSProperties;
1874
+ "aria-label"?: string;
830
1875
  }
831
1876
 
832
1877
  /**
833
1878
  * A checkbox for select-all functionality.
834
1879
  */
835
- export function TableSelectAllCheckbox(): JSX.Element {
1880
+ export function TableSelectAllCheckbox(props: TableSelectAllCheckboxProps = {}): JSX.Element {
836
1881
  const context = useContext(TableContext);
837
1882
  if (!context) {
838
- throw new Error('TableSelectAllCheckbox must be used within a Table');
1883
+ throw new Error("TableSelectAllCheckbox must be used within a Table");
839
1884
  }
840
1885
 
841
1886
  const { state } = context;
842
1887
 
843
- const { checkboxProps } = createTableSelectAllCheckbox<object>(
844
- () => state as TableState<object, TableCollection<object>>
1888
+ const selectAllCheckboxAria = createTableSelectAllCheckbox<object>(
1889
+ () => state as TableState<object, TableCollection<object>>,
845
1890
  );
846
1891
 
847
- return <input {...checkboxProps} />;
1892
+ return (
1893
+ <input
1894
+ {...selectAllCheckboxAria.checkboxProps}
1895
+ class={props.class}
1896
+ style={props.style}
1897
+ aria-label={props["aria-label"] ?? selectAllCheckboxAria.checkboxProps["aria-label"]}
1898
+ />
1899
+ );
848
1900
  }
849
1901
 
850
- // Attach components as static properties
851
1902
  Table.Header = TableHeader;
852
1903
  Table.Column = TableColumn;
853
1904
  Table.Body = TableBody;
1905
+ Table.LoadMoreItem = TableLoadMoreItem;
854
1906
  Table.Row = TableRow;
855
1907
  Table.Cell = TableCell;
856
1908
  Table.SelectionCheckbox = TableSelectionCheckbox;
857
1909
  Table.SelectAllCheckbox = TableSelectAllCheckbox;
1910
+
1911
+ export interface ColumnResizerRenderProps {
1912
+ /** Whether the resizer handle is hovered. */
1913
+ isHovered: boolean;
1914
+ /** Whether the resizer's hidden input is focused. */
1915
+ isFocused: boolean;
1916
+ /** Whether the column is currently being resized. */
1917
+ isResizing: boolean;
1918
+ /** The direction(s) the column can be resized: 'both', 'left', 'right'. */
1919
+ resizableDirection: "both" | "left" | "right";
1920
+ }
1921
+
1922
+ export interface ColumnResizerProps extends SlotProps {
1923
+ /** The column key this resizer belongs to. */
1924
+ column: { key: Key };
1925
+ /** Accessible label for the resizer. */
1926
+ "aria-label"?: string;
1927
+ /** Whether resizing is disabled. */
1928
+ isDisabled?: boolean;
1929
+ /** Called when resize starts. */
1930
+ onResizeStart?: (widths: Map<Key, number>) => void;
1931
+ /** Called during resize. */
1932
+ onResize?: (widths: Map<Key, number>) => void;
1933
+ /** Called when resize ends. */
1934
+ onResizeEnd?: (widths: Map<Key, number>) => void;
1935
+ /** CSS class — can be a string or function of render props. */
1936
+ class?: ClassNameOrFunction<ColumnResizerRenderProps>;
1937
+ /** Inline style — can be object or function of render props. */
1938
+ style?: StyleOrFunction<ColumnResizerRenderProps>;
1939
+ /** Children — can be JSX or render function. */
1940
+ children?: JSX.Element | RenderChildren<ColumnResizerRenderProps>;
1941
+ }
1942
+
1943
+ export function ColumnResizer(props: ColumnResizerProps): JSX.Element {
1944
+ const [local, domProps] = splitProps(props, [
1945
+ "column",
1946
+ "aria-label",
1947
+ "isDisabled",
1948
+ "onResizeStart",
1949
+ "onResize",
1950
+ "onResizeEnd",
1951
+ "class",
1952
+ "style",
1953
+ "slot",
1954
+ "children",
1955
+ ]);
1956
+
1957
+ // Register this column with the ResizableTableContainer (auto-collect columns)
1958
+ const registerColumn = useContext(ResizableTableRegisterContext);
1959
+ if (registerColumn) {
1960
+ registerColumn(local.column.key, { key: local.column.key });
1961
+ }
1962
+
1963
+ const resizeCtx = useContext(TableColumnResizeStateContext);
1964
+ const hasResizeContext = !!resizeCtx;
1965
+
1966
+ // Create a fallback "no-op" resize state for when there's no ResizableTableContainer
1967
+ const noopResizeState: TableColumnResizeState = {
1968
+ resizingColumn: () => null,
1969
+ columnWidths: () => new Map(),
1970
+ startResize() {},
1971
+ endResize() {},
1972
+ updateResizedColumns(_key: Key, _width: number) {
1973
+ return new Map();
1974
+ },
1975
+ getColumnWidth() {
1976
+ return 0;
1977
+ },
1978
+ getColumnMinWidth() {
1979
+ return 75;
1980
+ },
1981
+ getColumnMaxWidth() {
1982
+ return Infinity;
1983
+ },
1984
+ };
1985
+
1986
+ const { isHovered, hoverProps } = createHover({ isDisabled: local.isDisabled ?? false });
1987
+ const [isFocused, setIsFocused] = createSignal(false);
1988
+
1989
+ // Create the ARIA resize hook — always create it but use reactive state getter
1990
+ const columnResize = createTableColumnResize(
1991
+ () => ({
1992
+ column: local.column,
1993
+ "aria-label": local["aria-label"] ?? "Resizer",
1994
+ isDisabled: local.isDisabled,
1995
+ onResizeStart: (widths) => {
1996
+ resizeCtx?.getCallbacks?.().onResizeStart?.(widths);
1997
+ local.onResizeStart?.(widths);
1998
+ },
1999
+ onResize: (widths) => {
2000
+ resizeCtx?.getCallbacks?.().onResize?.(widths);
2001
+ local.onResize?.(widths);
2002
+ },
2003
+ onResizeEnd: (widths) => {
2004
+ resizeCtx?.getCallbacks?.().onResizeEnd?.(widths);
2005
+ local.onResizeEnd?.(widths);
2006
+ },
2007
+ }),
2008
+ () => resizeCtx?.getState() ?? noopResizeState,
2009
+ );
2010
+
2011
+ const renderValues = createMemo<ColumnResizerRenderProps>(() => ({
2012
+ isHovered: isHovered(),
2013
+ isFocused: isFocused(),
2014
+ isResizing: columnResize.isResizing(),
2015
+ resizableDirection: "both",
2016
+ }));
2017
+
2018
+ const renderProps = useRenderProps(
2019
+ {
2020
+ class: local.class,
2021
+ style: local.style,
2022
+ defaultClassName: "solidaria-Table-columnResizer",
2023
+ },
2024
+ renderValues,
2025
+ );
2026
+
2027
+ const cleanHoverProps = () => {
2028
+ const { ref: _ref, ...rest } = hoverProps as Record<string, unknown>;
2029
+ return rest;
2030
+ };
2031
+
2032
+ return (
2033
+ <div
2034
+ {...domProps}
2035
+ {...columnResize.resizerProps}
2036
+ {...cleanHoverProps()}
2037
+ class={renderProps.class()}
2038
+ style={renderProps.style()}
2039
+ data-hovered={isHovered() || undefined}
2040
+ data-resizing={columnResize.isResizing() || undefined}
2041
+ >
2042
+ <Show when={hasResizeContext}>
2043
+ <input
2044
+ {...columnResize.inputProps}
2045
+ onFocus={() => setIsFocused(true)}
2046
+ onBlur={() => setIsFocused(false)}
2047
+ />
2048
+ </Show>
2049
+ {typeof local.children === "function"
2050
+ ? (local.children as (props: ColumnResizerRenderProps) => JSX.Element)(renderValues())
2051
+ : local.children}
2052
+ </div>
2053
+ );
2054
+ }
2055
+
2056
+ export interface ResizableTableContainerProps extends SlotProps {
2057
+ /** Children (should contain a Table). */
2058
+ children?: JSX.Element;
2059
+ /** Column resize definitions. If not provided, columns from child ColumnResizers are auto-detected. */
2060
+ columns?: Array<{ key: Key; width?: ColumnSize; minWidth?: number; maxWidth?: number }>;
2061
+ /** CSS class name. */
2062
+ class?: string;
2063
+ /** Inline style. */
2064
+ style?: JSX.CSSProperties;
2065
+ /** Called when column resize starts. */
2066
+ onResizeStart?: (widths: Map<Key, number>) => void;
2067
+ /** Called during column resize. */
2068
+ onResize?: (widths: Map<Key, number>) => void;
2069
+ /** Called when column resize ends. */
2070
+ onResizeEnd?: (widths: Map<Key, number>) => void;
2071
+ }
2072
+
2073
+ export function ResizableTableContainer(props: ResizableTableContainerProps): JSX.Element {
2074
+ const [local, domProps] = splitProps(props, [
2075
+ "class",
2076
+ "style",
2077
+ "slot",
2078
+ "children",
2079
+ "columns",
2080
+ "onResizeStart",
2081
+ "onResize",
2082
+ "onResizeEnd",
2083
+ ]);
2084
+
2085
+ const [containerRef, setContainerRef] = createSignal<HTMLDivElement | null>(null);
2086
+ const [tableWidth, setTableWidth] = createSignal(0);
2087
+
2088
+ // Track container width via ResizeObserver
2089
+ createEffect(() => {
2090
+ const el = containerRef();
2091
+ if (!el) return;
2092
+
2093
+ // Initial measurement
2094
+ setTableWidth(el.clientWidth);
2095
+
2096
+ if (typeof ResizeObserver !== "undefined") {
2097
+ const observer = new ResizeObserver((entries) => {
2098
+ for (const entry of entries) {
2099
+ setTableWidth(entry.contentRect.width);
2100
+ }
2101
+ });
2102
+ observer.observe(el);
2103
+ onCleanup(() => observer.disconnect());
2104
+ }
2105
+ });
2106
+
2107
+ // Auto-collected columns from ColumnResizer children
2108
+ const [autoColumns, setAutoColumns] = createSignal<
2109
+ Map<Key, { key: Key; width?: ColumnSize; minWidth?: number; maxWidth?: number }>
2110
+ >(new Map());
2111
+
2112
+ const registerColumn = (
2113
+ key: Key,
2114
+ def: { key: Key; width?: ColumnSize; minWidth?: number; maxWidth?: number },
2115
+ ) => {
2116
+ setAutoColumns((prev) => {
2117
+ const next = new Map(prev);
2118
+ next.set(key, def);
2119
+ return next;
2120
+ });
2121
+ };
2122
+
2123
+ // Columns: prefer explicit prop, fall back to auto-collected
2124
+ const effectiveColumns = createMemo(() => {
2125
+ if (local.columns && local.columns.length > 0) return local.columns;
2126
+ return Array.from(autoColumns().values());
2127
+ });
2128
+
2129
+ // Use measured width, with a reasonable fallback for environments without layout (e.g. jsdom)
2130
+ const effectiveWidth = createMemo(() => {
2131
+ const w = tableWidth();
2132
+ return w > 0 ? w : 800; // fallback to 800px
2133
+ });
2134
+
2135
+ // Create resize state
2136
+ const resizeState = createMemo(() => {
2137
+ const cols = effectiveColumns();
2138
+ if (cols.length === 0) return null;
2139
+
2140
+ return createTableColumnResizeState(() => ({
2141
+ tableWidth: effectiveWidth(),
2142
+ columns: cols,
2143
+ }));
2144
+ });
2145
+
2146
+ // Provide a stable context object with a reactive getter
2147
+ const contextValue = {
2148
+ getState: () => resizeState(),
2149
+ getCallbacks: () => ({
2150
+ onResizeStart: local.onResizeStart,
2151
+ onResize: local.onResize,
2152
+ onResizeEnd: local.onResizeEnd,
2153
+ }),
2154
+ };
2155
+
2156
+ return (
2157
+ <ResizableTableRegisterContext.Provider value={registerColumn}>
2158
+ <TableColumnResizeStateContext.Provider value={contextValue}>
2159
+ <div
2160
+ ref={setContainerRef}
2161
+ {...domProps}
2162
+ class={local.class ?? "solidaria-ResizableTableContainer"}
2163
+ style={{ position: "relative", overflow: "auto", ...local.style }}
2164
+ >
2165
+ {local.children}
2166
+ </div>
2167
+ </TableColumnResizeStateContext.Provider>
2168
+ </ResizableTableRegisterContext.Provider>
2169
+ );
2170
+ }
2171
+
2172
+ /** Internal context for ColumnResizer to register itself with ResizableTableContainer */
2173
+ const ResizableTableRegisterContext = createContext<
2174
+ | ((
2175
+ key: Key,
2176
+ def: { key: Key; width?: ColumnSize; minWidth?: number; maxWidth?: number },
2177
+ ) => void)
2178
+ | null
2179
+ >(null);
2180
+
2181
+ export function useTableOptions() {
2182
+ return useContext(TableContext);
2183
+ }