@human-kit/svelte-components 1.0.0-alpha.4 → 1.0.0-alpha.5

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 (82) hide show
  1. package/dist/checkbox/README.md +53 -0
  2. package/dist/checkbox/TODO.md +16 -0
  3. package/dist/checkbox/index.d.ts +6 -0
  4. package/dist/checkbox/index.js +6 -0
  5. package/dist/checkbox/index.parts.d.ts +2 -0
  6. package/dist/checkbox/index.parts.js +2 -0
  7. package/dist/checkbox/indicator/README.md +23 -0
  8. package/dist/checkbox/indicator/checkbox-indicator.svelte +43 -0
  9. package/dist/checkbox/indicator/checkbox-indicator.svelte.d.ts +10 -0
  10. package/dist/checkbox/root/README.md +47 -0
  11. package/dist/checkbox/root/checkbox-label-test.svelte +10 -0
  12. package/dist/checkbox/root/checkbox-label-test.svelte.d.ts +18 -0
  13. package/dist/checkbox/root/checkbox-root.svelte +361 -0
  14. package/dist/checkbox/root/checkbox-root.svelte.d.ts +23 -0
  15. package/dist/checkbox/root/checkbox-test.svelte +59 -0
  16. package/dist/checkbox/root/checkbox-test.svelte.d.ts +18 -0
  17. package/dist/checkbox/root/context.d.ts +21 -0
  18. package/dist/checkbox/root/context.js +15 -0
  19. package/dist/combobox/list/combobox-listbox.svelte.d.ts +1 -1
  20. package/dist/index.d.ts +4 -0
  21. package/dist/index.js +4 -0
  22. package/dist/table/IMPLEMENTATION_NOTES.md +8 -0
  23. package/dist/table/PLAN-HIDDEN-COLUMNS.md +152 -0
  24. package/dist/table/PLAN.md +924 -0
  25. package/dist/table/README.md +116 -0
  26. package/dist/table/SELECTION_CHECKBOX_PLAN.md +234 -0
  27. package/dist/table/TODO.md +100 -0
  28. package/dist/table/body/README.md +24 -0
  29. package/dist/table/body/table-body.svelte +25 -0
  30. package/dist/table/body/table-body.svelte.d.ts +9 -0
  31. package/dist/table/cell/README.md +25 -0
  32. package/dist/table/cell/table-cell.svelte +247 -0
  33. package/dist/table/cell/table-cell.svelte.d.ts +9 -0
  34. package/dist/table/checkbox/README.md +38 -0
  35. package/dist/table/checkbox/table-checkbox-test.svelte +121 -0
  36. package/dist/table/checkbox/table-checkbox-test.svelte.d.ts +16 -0
  37. package/dist/table/checkbox/table-checkbox.svelte +274 -0
  38. package/dist/table/checkbox/table-checkbox.svelte.d.ts +13 -0
  39. package/dist/table/checkbox-indicator/README.md +29 -0
  40. package/dist/table/checkbox-indicator/table-checkbox-indicator.svelte +22 -0
  41. package/dist/table/checkbox-indicator/table-checkbox-indicator.svelte.d.ts +10 -0
  42. package/dist/table/column/README.md +32 -0
  43. package/dist/table/column/table-column.svelte +108 -0
  44. package/dist/table/column/table-column.svelte.d.ts +18 -0
  45. package/dist/table/column-header-cell/README.md +28 -0
  46. package/dist/table/column-header-cell/table-column-header-cell.svelte +281 -0
  47. package/dist/table/column-header-cell/table-column-header-cell.svelte.d.ts +9 -0
  48. package/dist/table/column-resizer/README.md +32 -0
  49. package/dist/table/column-resizer/table-column-resizer-freeze-layout-test.svelte +51 -0
  50. package/dist/table/column-resizer/table-column-resizer-freeze-layout-test.svelte.d.ts +3 -0
  51. package/dist/table/column-resizer/table-column-resizer-selection-column-test.svelte +83 -0
  52. package/dist/table/column-resizer/table-column-resizer-selection-column-test.svelte.d.ts +3 -0
  53. package/dist/table/column-resizer/table-column-resizer-test.svelte +75 -0
  54. package/dist/table/column-resizer/table-column-resizer-test.svelte.d.ts +3 -0
  55. package/dist/table/column-resizer/table-column-resizer.svelte +616 -0
  56. package/dist/table/column-resizer/table-column-resizer.svelte.d.ts +11 -0
  57. package/dist/table/empty-state/README.md +25 -0
  58. package/dist/table/empty-state/table-empty-state.svelte +38 -0
  59. package/dist/table/empty-state/table-empty-state.svelte.d.ts +8 -0
  60. package/dist/table/footer/README.md +24 -0
  61. package/dist/table/footer/table-footer.svelte +19 -0
  62. package/dist/table/footer/table-footer.svelte.d.ts +9 -0
  63. package/dist/table/header/README.md +24 -0
  64. package/dist/table/header/table-header.svelte +19 -0
  65. package/dist/table/header/table-header.svelte.d.ts +9 -0
  66. package/dist/table/index.d.ts +16 -0
  67. package/dist/table/index.js +16 -0
  68. package/dist/table/index.parts.d.ts +12 -0
  69. package/dist/table/index.parts.js +12 -0
  70. package/dist/table/root/README.md +56 -0
  71. package/dist/table/root/context.d.ts +198 -0
  72. package/dist/table/root/context.js +1426 -0
  73. package/dist/table/root/table-reorder-test.svelte +64 -0
  74. package/dist/table/root/table-reorder-test.svelte.d.ts +3 -0
  75. package/dist/table/root/table-root.svelte +410 -0
  76. package/dist/table/root/table-root.svelte.d.ts +29 -0
  77. package/dist/table/root/table-test.svelte +165 -0
  78. package/dist/table/root/table-test.svelte.d.ts +25 -0
  79. package/dist/table/row/README.md +27 -0
  80. package/dist/table/row/table-row.svelte +321 -0
  81. package/dist/table/row/table-row.svelte.d.ts +13 -0
  82. package/package.json +11 -1
@@ -0,0 +1,924 @@
1
+ <!-- markdownlint-disable MD007 MD010 MD060 -->
2
+
3
+ # Table Plan
4
+
5
+ ## Goal
6
+
7
+ Design and implement a new public `Table` component for the Svelte library, using the React Aria Components usability model as the main reference, but adapting it to the repository conventions: part-based composition, centralized state in `Root`, typed context, Svelte 5 runes, colocated tests for each part, and component/part documentation.
8
+
9
+ ## Product Goals
10
+
11
+ - Provide a composable and readable API for headless tables.
12
+ - Prioritize robust and predictable keyboard navigation.
13
+ - Support row selection and sorting in the first version.
14
+ - Keep the initial scope reasonable so the component is not blocked by advanced features.
15
+ - Establish an internal foundation that can be extended later.
16
+
17
+ ## Repository References
18
+
19
+ ### Patterns to Follow
20
+
21
+ - Namespace-style exports and named exports, as in `ListBox`.
22
+ - Explicit typed context in `root/context.ts`.
23
+ - Controlled/uncontrolled state pattern in `Root`.
24
+ - Colocated interaction and accessibility tests.
25
+ - Base component README plus a README for each public part.
26
+
27
+ ### Relevant Components and Utilities
28
+
29
+ - `packages/svelte/src/lib/listbox`
30
+ - `packages/svelte/src/lib/calendar`
31
+ - `packages/svelte/src/lib/primitives/keyboard-navigation.ts`
32
+ - `packages/svelte/src/lib/FOCUS_STATE_CONTRACT.md`
33
+ - `packages/svelte/src/lib/test-utils/focus-contract.ts`
34
+
35
+ ## Decisions Already Made
36
+
37
+ - The v1 public API will be static/composable, not dynamic.
38
+ - `Table` must support cell focus and row-derived focus state.
39
+ - `Table.Column` is added to the anatomy to solve column metadata.
40
+ - `Table.Column` is a logical component with no DOM output; it registers column metadata in context. `Table.ColumnHeaderCell` renders the `<th>`.
41
+ - `Table.EmptyState` is a dedicated part for the body empty state.
42
+ - V1 must include:
43
+ - base anatomy
44
+ - `Table.Footer`
45
+ - `Table.EmptyState`
46
+ - keyboard grid navigation
47
+ - row selection
48
+ - sorting
49
+
50
+ ## V1 Scope
51
+
52
+ ### Proposed Public Anatomy
53
+
54
+ ```svelte
55
+ <Table.Root aria-label="Users">
56
+ <Table.Header>
57
+ <Table.Row>
58
+ <!-- Column es lógico (sin DOM), solo registra metadata -->
59
+ <!-- Column is logical (no DOM), it only registers metadata -->
60
+ <Table.Column id="email" isRowHeader>
61
+ <Table.ColumnHeaderCell>Email</Table.ColumnHeaderCell>
62
+ </Table.Column>
63
+ <Table.Column id="group" allowsSorting>
64
+ <Table.ColumnHeaderCell>Group</Table.ColumnHeaderCell>
65
+ </Table.Column>
66
+ </Table.Row>
67
+ </Table.Header>
68
+
69
+ <Table.Body>
70
+ <Table.Row id="danilo">
71
+ <Table.Cell>danilo@example.com</Table.Cell>
72
+ <Table.Cell>Developer</Table.Cell>
73
+ </Table.Row>
74
+ <Table.Row id="zahra">
75
+ <Table.Cell>zahra@example.com</Table.Cell>
76
+ <Table.Cell>Admin</Table.Cell>
77
+ </Table.Row>
78
+
79
+ <!-- Only shown when Body has no rows -->
80
+ <Table.EmptyState>No users found.</Table.EmptyState>
81
+ </Table.Body>
82
+
83
+ <Table.Footer>
84
+ <Table.Row>
85
+ <Table.Cell>Total</Table.Cell>
86
+ <Table.Cell>2 users</Table.Cell>
87
+ </Table.Row>
88
+ </Table.Footer>
89
+ </Table.Root>
90
+ ```
91
+
92
+ ### Target Public Parts
93
+
94
+ - `Table.Root`
95
+ - `Table.Column` — logical, no DOM
96
+ - `Table.Header`
97
+ - `Table.Body`
98
+ - `Table.EmptyState`
99
+ - `Table.Footer`
100
+ - `Table.Row`
101
+ - `Table.ColumnHeaderCell`
102
+ - `Table.Cell`
103
+
104
+ ## Semantics and Accessibility
105
+
106
+ ### Semantic Model
107
+
108
+ - The table should behave as an interactive table, not only as a passive `<table>`.
109
+ - The primary reference is a `grid` model with directional navigation.
110
+ - `Table.Root` must require an accessible name through `aria-label` or `aria-labelledby`.
111
+ - There must be support for marking a column as `isRowHeader` to improve screen reader announcements.
112
+
113
+ ### Recommended Semantic Rendering
114
+
115
+ - `Table.Root` should render `<table role="grid">`.
116
+ - `Table.Header` should render `<thead role="rowgroup">`.
117
+ - `Table.Body` should render `<tbody role="rowgroup">`.
118
+ - `Table.Footer` should render `<tfoot role="rowgroup">`.
119
+ - `Table.Row` should render `<tr role="row">`.
120
+ - `Table.ColumnHeaderCell` should render `<th role="columnheader">`.
121
+ - `Table.Cell` should render:
122
+ - `<th scope="row" role="rowheader">` when the associated column has `isRowHeader`
123
+ - `<td role="gridcell">` in all other cases
124
+
125
+ This combination preserves real HTML semantics while allowing interactive grid behavior without inventing a structure entirely based on `div`s.
126
+
127
+ ### Focus Model
128
+
129
+ Two related levels will be implemented:
130
+
131
+ 1. **DOM focus on cell/header cell**
132
+ - `Table.ColumnHeaderCell` and `Table.Cell` are the real navigation targets.
133
+ - This keeps the behavior close to React Aria Components.
134
+
135
+ 2. **Row-derived state**
136
+ - `Table.Row` exposes derived states such as focused row, selected row, or disabled row.
137
+ - This helps with styling and simplifies the visual experience.
138
+
139
+ ### Target Keyboard Support in V1
140
+
141
+ - `Tab` enters and leaves the grid.
142
+ - Arrow keys navigate between header cells and body cells.
143
+ - `Home` / `End` move to the start/end of the current row.
144
+ - `Ctrl/Cmd + Home` and `Ctrl/Cmd + End` may be evaluated as an enhancement if the 2D engine can support them without extra complexity.
145
+ - `Enter` / `Space` should trigger row selection when appropriate.
146
+ - Sortable headers should respond to keyboard input to change sort order.
147
+
148
+ ### Recommended Tabbability Strategy
149
+
150
+ - `Table.Root` should not be tabbable under normal conditions.
151
+ - Only one `Table.ColumnHeaderCell` or `Table.Cell` should have `tabindex="0"` at a time.
152
+ - All other navigable cells should have `tabindex="-1"`.
153
+ - When entering with `Tab` from outside:
154
+ - if there is a previously focused cell, focus is restored there
155
+ - otherwise focus enters the first navigable header cell
156
+ - if no header is navigable, focus enters the first navigable body cell
157
+ - `Shift+Tab` and `Tab` should allow the browser to leave the grid naturally from the active cell.
158
+
159
+ This preserves the roving tabindex pattern and avoids making `Root` compete with cells as a focus target.
160
+
161
+ ### Decision for `Footer`
162
+
163
+ - In v1, `Table.Footer` is mainly semantic/structural.
164
+ - It will not participate in the main grid focus flow unless a clear need appears during implementation.
165
+
166
+ ## Key Types
167
+
168
+ ```ts
169
+ /** Sort direction */
170
+ type SortDirection = 'ascending' | 'descending';
171
+
172
+ /** Active sort descriptor */
173
+ type SortDescriptor = {
174
+ column: string;
175
+ direction: SortDirection;
176
+ };
177
+
178
+ /** Row selection mode */
179
+ type SelectionMode = 'none' | 'single' | 'multiple';
180
+
181
+ /** Selectable key (Row ids) */
182
+ type SelectionKey = string | number;
183
+
184
+ /** Set of selected keys (Row ids) */
185
+ type SelectionSet = Set<SelectionKey>;
186
+
187
+ /** Coordinate within the 2D grid (global, header row 0 = row 0) */
188
+ type GridCoord = {
189
+ row: number;
190
+ col: number;
191
+ };
192
+ ```
193
+
194
+ ## Proposed V1 API
195
+
196
+ ### `Table.Root`
197
+
198
+ Responsibilities:
199
+
200
+ - register columns, rows, and cells
201
+ - maintain the 2D focus cursor
202
+ - expose controlled/uncontrolled selection state
203
+ - expose controlled/uncontrolled sorting state
204
+ - coordinate ARIA attributes and focus/selection data attributes
205
+
206
+ Tentative API:
207
+
208
+ - `aria-label` / `aria-labelledby` — required accessible name
209
+ - `selectionMode?: SelectionMode` — default `'none'`
210
+ - `selectedKeys?: SelectionSet`
211
+ - `defaultSelectedKeys?: SelectionSet`
212
+ - `onSelectionChange?: (keys: SelectionSet) => void`
213
+ - `sortDescriptor?: SortDescriptor`
214
+ - `defaultSortDescriptor?: SortDescriptor`
215
+ - `onSortChange?: (descriptor: SortDescriptor) => void`
216
+ - `disabledKeys?: Set<SelectionKey>` — disabled row ids
217
+ - `children`
218
+
219
+ ### `Table.Column`
220
+
221
+ > Logical component — does not render its own DOM element. Registers column metadata in `Root` context and wraps `ColumnHeaderCell`.
222
+
223
+ Responsibilities:
224
+
225
+ - define stable column identity
226
+ - register column metadata in context (sorting, row header)
227
+ - register column resize metadata in context when enabled
228
+ - serve as the anchor for sorting and row header semantics
229
+ - serve as the anchor for width/resizing behavior
230
+
231
+ Tentative API:
232
+
233
+ - `id: string` — stable column identity
234
+ - `allowsSorting?: boolean`
235
+ - `allowsResizing?: boolean`
236
+ - `isRowHeader?: boolean`
237
+ - `textValue?: string`
238
+ - `width?: number | string`
239
+ - `defaultWidth?: number | string`
240
+ - `minWidth?: number`
241
+ - `maxWidth?: number`
242
+
243
+ ### `Table.Header`
244
+
245
+ Responsibilities:
246
+
247
+ - contain header rows
248
+ - coordinate header semantics
249
+
250
+ ### `Table.Body`
251
+
252
+ Responsibilities:
253
+
254
+ - contain data rows
255
+ - coordinate `Table.EmptyState` visibility when there are no rows
256
+
257
+ Tentative API:
258
+
259
+ - `children`
260
+
261
+ > The empty state is handled through the dedicated `Table.EmptyState` part inside `Body`, not through a prop.
262
+
263
+ ### `Table.EmptyState`
264
+
265
+ Responsibilities:
266
+
267
+ - render empty-state content when `Body` has no rows
268
+ - hide itself automatically when rows exist
269
+ - not participate in the grid navigation model
270
+
271
+ Tentative API:
272
+
273
+ - `children`
274
+
275
+ Recommended semantics:
276
+
277
+ - `Table.EmptyState` should be a convenience part that internally renders a row and an empty-state cell.
278
+ - Its output should be equivalent to:
279
+
280
+ ```svelte
281
+ <tr role="row" data-empty>
282
+ <td role="gridcell" colspan={columnCount}>
283
+ {children}
284
+ </td>
285
+ </tr>
286
+ ```
287
+
288
+ - `colspan` should be resolved automatically from the number of registered columns.
289
+ - It should not be focusable or participate in 2D navigation.
290
+ - It should only be allowed inside `Table.Body`.
291
+
292
+ This avoids invalid markup inside `<tbody>` and keeps the API convenient for consumers.
293
+
294
+ ### `Table.Footer`
295
+
296
+ Responsibilities:
297
+
298
+ - contain summary/metadata rows
299
+ - not interfere with the main v1 navigation model
300
+
301
+ ### `Table.Row`
302
+
303
+ Responsibilities:
304
+
305
+ - row identity
306
+ - disabled state
307
+ - derived selection state
308
+ - derived focus/selection styling
309
+
310
+ Tentative API:
311
+
312
+ - `id`
313
+ - `isDisabled?: boolean`
314
+ - `textValue?: string`
315
+
316
+ ### `Table.ColumnHeaderCell`
317
+
318
+ Responsibilities:
319
+
320
+ - focus target in the header
321
+ - sorting trigger when the column allows it
322
+ - apply `aria-sort` automatically from `Root.sortDescriptor` and `Column.allowsSorting`
323
+ - `aria-sort` values: `ascending` | `descending` | `none`
324
+ - host the resize affordance when the consumer composes a `Table.ColumnResizer` inside it
325
+
326
+ ### `Table.ColumnResizer`
327
+
328
+ Responsibilities:
329
+
330
+ - interactive resize handle for the current `Table.Column`
331
+ - consume column identity from `Table.Column` context rather than matching by visual position
332
+ - support pointer drag and keyboard resizing
333
+ - expose resize state for styling through data attributes
334
+
335
+ Tentative API:
336
+
337
+ - `step?: number` — keyboard delta in px, default `16`
338
+ - `shiftStep?: number` — larger keyboard delta in px, default `48`
339
+ - `children?`
340
+ - `class?`
341
+
342
+ ### `Table.Cell`
343
+
344
+ Responsibilities:
345
+
346
+ - focus target within the body
347
+ - reflect derived row selection state
348
+ - support simple textual content in v1
349
+
350
+ ## Data Attributes by Part
351
+
352
+ | Part | Attribute | Values | Description |
353
+ | ------------------ | --------------------- | ---------------------------- | ---------------------------------------- |
354
+ | `Root` | `data-selection-mode` | `none`, `single`, `multiple` | Active selection mode |
355
+ | `Header` | `data-table-header` | `''` | Semantic marker |
356
+ | `Body` | `data-table-body` | `''` | Semantic marker |
357
+ | `Body` | `data-empty` | `''` | Present when Body has no rows |
358
+ | `Footer` | `data-table-footer` | `''` | Semantic marker |
359
+ | `Row` (body) | `data-focused` | `''` | The row contains the focused cell |
360
+ | `Row` (body) | `data-selected` | `''` | Selected row |
361
+ | `Row` (body) | `data-disabled` | `''` | Disabled row |
362
+ | `ColumnHeaderCell` | `data-focused` | `''` | Focused header cell |
363
+ | `ColumnHeaderCell` | `data-sortable` | `''` | The column allows sorting |
364
+ | `ColumnHeaderCell` | `data-sort-direction` | `ascending`, `descending` | Active sort direction |
365
+ | `Cell` | `data-focused` | `''` | Focused cell |
366
+ | `Cell` | `data-row-selected` | `''` | The row containing this cell is selected |
367
+
368
+ ## ARIA Attributes by Part
369
+
370
+ | Part | Attribute | Value | Description |
371
+ | ------------------ | -------------------------------- | --------------------------------- | ------------------------------------------------ |
372
+ | `Root` | `role` | `grid` | Interactive table |
373
+ | `Root` | `aria-label` / `aria-labelledby` | string | Accessible name (required) |
374
+ | `Root` | `aria-multiselectable` | `true` | Present when `selectionMode='multiple'` |
375
+ | `Header` | `role` | `rowgroup` | Header row group |
376
+ | `Body` | `role` | `rowgroup` | Data row group |
377
+ | `Footer` | `role` | `rowgroup` | Footer row group |
378
+ | `Row` | `role` | `row` | Row |
379
+ | `Row` (body) | `aria-selected` | `true` / `false` | Selection state (when `selectionMode != 'none'`) |
380
+ | `Row` (body) | `aria-disabled` | `true` | Disabled row |
381
+ | `ColumnHeaderCell` | `role` | `columnheader` | Column header |
382
+ | `ColumnHeaderCell` | `aria-sort` | `ascending`, `descending`, `none` | Sort direction (only when `allowsSorting`) |
383
+ | `Cell` | `role` | `gridcell` or `rowheader` | `rowheader` if the column has `isRowHeader` |
384
+ | `EmptyState` | `role` | `row` + internal `gridcell` | Semantic, non-navigable empty-state row |
385
+
386
+ ## What V1 Does Not Include
387
+
388
+ ### Out of Scope
389
+
390
+ - drag and drop
391
+ - async loading / load more
392
+ - API pública dinámica con `items` y `columns`
393
+ - row actions / row links
394
+ - typeahead
395
+ - focus management para elementos interactivos dentro de `Cell`
396
+ - focus management for interactive elements inside `Cell`
397
+ - nested headers / grouped columns
398
+ - cell selection
399
+ - cell selection
400
+ - virtualización
401
+ - virtualization
402
+ - soporte complejo de `colSpan` / `rowSpan`
403
+ - complex `colSpan` / `rowSpan` support
404
+ - footer navegable como parte del grid principal
405
+ - footer navigation as part of the main grid
406
+
407
+ ## Advanced Feature Matrix
408
+
409
+ | Feature | Main Complexity | Risk | Recommendation |
410
+ | ---------------------------------- | ---------------------------------------------------------- | ----------- | ------------------ |
411
+ | Column resizing | width state, handles, pointer + keyboard, persistence | high | next planned phase |
412
+ | Drag and drop | reorder, drop targets, SR + keyboard + pointer | very high | keep out of v1 |
413
+ | Async loading / load more | scroll state, sentinel rows, partial states | high | keep out of v1 |
414
+ | Dynamic `items` / `columns` API | collection, stable ids, render functions, memoization | high | defer |
415
+ | Row actions / row links | conflicts between actions, selection, and HTML limitations | medium/high | defer |
416
+ | Interactive content inside `Cell` | focus handoff between grid and nested controls | very high | keep out of v1 |
417
+ | Typeahead | depends on stable collection and consistent `textValue` | medium | defer |
418
+ | Nested headers / column groups | spans, navigation, and complex semantics | high | keep out of v1 |
419
+ | Cell selection | changes the entire interaction model | high | keep out of v1 |
420
+ | Full `selectionBehavior="replace"` | modifiers and fine-grained focus/selection semantics | medium/high | defer |
421
+ | Virtualization | strong decoupling between collection and DOM | very high | keep out of v1 |
422
+ | Integrated select-all | useful UX but depends on mature selection behavior | medium | phase 2 |
423
+ | Complex `colSpan` / `rowSpan` | breaks the rectangular grid model | high | defer |
424
+ | Navigable footer | adds another region to the focus model | medium | avoid in v1 |
425
+
426
+ ## Proposed Internal Architecture
427
+
428
+ ## Root State
429
+
430
+ Create `packages/svelte/src/lib/table/root/context.ts` with responsibilities for:
431
+
432
+ - column registration
433
+ - row registration
434
+ - cell registration by coordinate
435
+ - current focus resolution
436
+ - 2D navigation
437
+ - row selection state
438
+ - sorting state
439
+ - derived utilities for each part
440
+
441
+ ## 2D Navigation Engine
442
+
443
+ It is not enough to depend only on `createKeyboardNavigation()` because it currently solves linear navigation. `Table` needs a dedicated engine able to:
444
+
445
+ - know rows and columns by index
446
+ - move focus in two dimensions
447
+ - distinguish header and body
448
+ - resolve missing or disabled cells
449
+ - derive the active row from the active cell
450
+
451
+ ### Proposed Internal Interface
452
+
453
+ ```ts
454
+ interface GridNavigation {
455
+ /** Currently focused coordinate (global, header row 0 = row 0) */
456
+ focusedCoord: GridCoord;
457
+
458
+ /** Moves focus in the given direction, skipping non-navigable cells */
459
+ move(direction: 'up' | 'down' | 'left' | 'right'): void;
460
+
461
+ /** Moves focus to the first cell in the current row */
462
+ moveToRowStart(): void;
463
+
464
+ /** Moves focus to the last cell in the current row */
465
+ moveToRowEnd(): void;
466
+
467
+ /** Moves focus to the first cell in the grid (header[0][0]) */
468
+ moveToGridStart(): void;
469
+
470
+ /** Moves focus to the last cell in the body */
471
+ moveToGridEnd(): void;
472
+
473
+ /** Registers a navigable cell in the grid */
474
+ register(coord: GridCoord, element: HTMLElement): void;
475
+
476
+ /** Unregisters a cell from the grid */
477
+ unregister(coord: GridCoord): void;
478
+
479
+ /** Checks whether a coordinate is navigable (exists and is not disabled) */
480
+ isNavigable(coord: GridCoord): boolean;
481
+
482
+ /** Returns the body row index from a global coordinate */
483
+ toBodyRowIndex(globalRow: number): number | null;
484
+
485
+ /** Applies DOM focus to the element at the given coordinate */
486
+ focusCell(coord: GridCoord): void;
487
+ }
488
+ ```
489
+
490
+ ### Key-to-Action Mapping
491
+
492
+ | Key | Action |
493
+ | ------------------------ | -------------------------------------------------------- |
494
+ | `ArrowUp` | `move('up')` |
495
+ | `ArrowDown` | `move('down')` |
496
+ | `ArrowLeft` | `move('left')` |
497
+ | `ArrowRight` | `move('right')` |
498
+ | `Home` | `moveToRowStart()` |
499
+ | `End` | `moveToRowEnd()` |
500
+ | `Ctrl+Home` / `Cmd+Home` | `moveToGridStart()` |
501
+ | `Ctrl+End` / `Cmd+End` | `moveToGridEnd()` |
502
+ | `Tab` | Leaves the grid (focus to the next tabbable element) |
503
+ | `Shift+Tab` | Leaves the grid (focus to the previous tabbable element) |
504
+ | `Enter` / `Space` | Select row (body) or toggle sort (sortable header) |
505
+
506
+ ## Selection
507
+
508
+ - V1 selection is row-based.
509
+ - Focus must not be equivalent to selection.
510
+ - `single` and `multiple` must work in both controlled and uncontrolled mode.
511
+ - Disabled rows must not be selectable.
512
+
513
+ ## Sorting
514
+
515
+ - Sort state should live in `Root`.
516
+ - `Column` defines whether a column allows sorting.
517
+ - `ColumnHeaderCell` triggers sort changes.
518
+ - The component reflects state, but does not necessarily mutate data automatically; that remains the consumer's responsibility.
519
+
520
+ ## Planned File Structure
521
+
522
+ ### New Component Files
523
+
524
+ - `packages/svelte/src/lib/table/index.ts`
525
+ - `packages/svelte/src/lib/table/index.parts.ts`
526
+ - `packages/svelte/src/lib/table/README.md`
527
+ - `packages/svelte/src/lib/table/TODO.md`
528
+
529
+ ### Root
530
+
531
+ - `packages/svelte/src/lib/table/root/table-root.svelte`
532
+ - `packages/svelte/src/lib/table/root/context.ts`
533
+ - `packages/svelte/src/lib/table/root/table-root.test.ts`
534
+ - `packages/svelte/src/lib/table/root/table-test.svelte`
535
+ - `packages/svelte/src/lib/table/root/README.md`
536
+
537
+ ### Public Parts
538
+
539
+ - `packages/svelte/src/lib/table/column/table-column.svelte`
540
+ - `packages/svelte/src/lib/table/column/README.md`
541
+ - `packages/svelte/src/lib/table/column/table-column.test.ts`
542
+
543
+ ## Phase 2: Column Resizing Plan
544
+
545
+ ### Resize Goal
546
+
547
+ Add column resizing in a way that follows the React Aria Components mental model while preserving the repository's existing `Table` architecture: logical `Table.Column`, state in `Table.Root`, typed context, native table semantics, and composable parts.
548
+
549
+ ### Functional Contract
550
+
551
+ - Resizing must target the column whose composition includes the resize handle.
552
+ - The resize handle must live inside the header composition for that column, not in a parallel list of handlers.
553
+ - The active column is resolved from `Table.Column` context, never by visual index guessing alone.
554
+ - Functional behavior should mirror RAC:
555
+ - a column opts into resizing via column metadata
556
+ - a dedicated resizer part provides the interactive affordance
557
+ - widths can be controlled or uncontrolled
558
+ - pointer and keyboard resizing are both supported
559
+
560
+ ### Recommended Public Composition
561
+
562
+ ```svelte
563
+ <Table.Root aria-label="Users" bind:columnWidths>
564
+ <Table.Header>
565
+ <Table.Row>
566
+ <Table.Column
567
+ id="email"
568
+ isRowHeader
569
+ allowsSorting
570
+ allowsResizing
571
+ defaultWidth={280}
572
+ minWidth={180}
573
+ >
574
+ <Table.ColumnHeaderCell>
575
+ <span>Email</span>
576
+ <Table.ColumnResizer />
577
+ </Table.ColumnHeaderCell>
578
+ </Table.Column>
579
+
580
+ <Table.Column id="group" allowsSorting allowsResizing defaultWidth={180} minWidth={140}>
581
+ <Table.ColumnHeaderCell>
582
+ <span>Group</span>
583
+ <Table.ColumnResizer />
584
+ </Table.ColumnHeaderCell>
585
+ </Table.Column>
586
+ </Table.Row>
587
+ </Table.Header>
588
+
589
+ <Table.Body>
590
+ <!-- rows -->
591
+ </Table.Body>
592
+ </Table.Root>
593
+ ```
594
+
595
+ ### API Recommendation
596
+
597
+ #### Resize Props on `Table.Column`
598
+
599
+ Add the following props:
600
+
601
+ - `allowsResizing?: boolean`
602
+ - `width?: number | string`
603
+ - `defaultWidth?: number | string`
604
+ - `minWidth?: number`
605
+ - `maxWidth?: number`
606
+
607
+ Notes:
608
+
609
+ - `width` is the controlled width for the column.
610
+ - `defaultWidth` is the uncontrolled initial width.
611
+ - `allowsResizing` is required for resize behavior, even if a `Table.ColumnResizer` is rendered.
612
+ - `Table.ColumnResizer` without `allowsResizing` should be ignored in production and warn in dev.
613
+
614
+ #### Width State on `Table.Root`
615
+
616
+ Add root-level width state APIs:
617
+
618
+ - `columnWidths?: Map<string, number>`
619
+ - `defaultColumnWidths?: Map<string, number>`
620
+ - `onColumnWidthsChange?: (widths: Map<string, number>) => void`
621
+ - `onColumnResizeStart?: (columnId: string) => void`
622
+ - `onColumnResizeEnd?: (widths: Map<string, number>) => void`
623
+
624
+ Notes:
625
+
626
+ - Controlled/uncontrolled width state should mirror the existing `selectedKeys` and `sortDescriptor` contracts.
627
+ - Widths in root state should be normalized to px numbers even if consumer input allows string forms.
628
+
629
+ #### `Table.ColumnResizer` Part
630
+
631
+ Public part to place inside `Table.ColumnHeaderCell`.
632
+
633
+ Tentative props:
634
+
635
+ - `step?: number`
636
+ - `shiftStep?: number`
637
+ - `class?: string`
638
+ - `children?: Snippet`
639
+
640
+ No `columnId` prop should be needed; it must use `Table.Column` context.
641
+
642
+ ### Why Not a Parallel `ColumnHandler`
643
+
644
+ This API should explicitly avoid a separate sibling structure like:
645
+
646
+ ```svelte
647
+ <Table.Column id="email" />
648
+ <Table.Column id="group" />
649
+ <Table.ColumnHandler index={0} />
650
+ <Table.ColumnHandler index={1} />
651
+ ```
652
+
653
+ Reasons:
654
+
655
+ - position-based matching becomes fragile with dynamic columns
656
+ - it duplicates the concept of column identity
657
+ - it becomes harder to keep sorting, row-header semantics, and resizing anchored to the same column contract
658
+ - it does not follow the RAC model, where resizing is column-owned and the handle is colocated with the header content
659
+
660
+ ### Width Model
661
+
662
+ #### Effective Width Resolution
663
+
664
+ For each registered column, compute the effective width from highest to lowest precedence:
665
+
666
+ 1. `Table.Root.columnWidths.get(columnId)`
667
+ 2. `Table.Column.width`
668
+ 3. `Table.Root.defaultColumnWidths.get(columnId)`
669
+ 4. `Table.Column.defaultWidth`
670
+ 5. no explicit width
671
+
672
+ Then clamp the result against:
673
+
674
+ - `minWidth`
675
+ - `maxWidth`
676
+
677
+ #### Initial Implementation Constraint
678
+
679
+ For the first resizing implementation, normalize widths to px values.
680
+
681
+ - Accept `number` as px.
682
+ - Optionally accept `"123px"` and normalize it to `123`.
683
+ - Defer `%`, `fr`, and more advanced layout math until the base feature is stable.
684
+
685
+ This keeps the state model simple and reduces layout bugs.
686
+
687
+ ### Rendering Strategy
688
+
689
+ The recommended implementation is to generate a `<colgroup>` inside `Table.Root` from the registered columns and the effective widths.
690
+
691
+ Reasons:
692
+
693
+ - widths apply consistently to both header and body cells
694
+ - native table layout remains intact
695
+ - it avoids pushing per-cell width styles into every `Table.Cell`
696
+ - it scales better as the feature grows
697
+
698
+ Planned approach:
699
+
700
+ - `Table.Root` renders a managed `<colgroup>` before children
701
+ - each registered column maps to one `<col>`
702
+ - effective width is applied to the `<col>`
703
+ - body and header cells keep their semantic markup unchanged
704
+
705
+ Fallback if `<colgroup>` proves insufficient for some cases:
706
+
707
+ - apply inline width/min-width styles to header cells and derived styles to body cells by column index
708
+
709
+ But `<colgroup>` should be the default strategy.
710
+
711
+ ### Interaction Model
712
+
713
+ #### Pointer
714
+
715
+ - pointer down on `Table.ColumnResizer` starts resizing for its current column
716
+ - movement computes a new width in px relative to the starting header width
717
+ - width updates continuously during drag
718
+ - pointer up finalizes the interaction and calls `onColumnResizeEnd`
719
+
720
+ #### Keyboard
721
+
722
+ - the resizer is focusable
723
+ - `ArrowLeft` reduces width by `step`
724
+ - `ArrowRight` increases width by `step`
725
+ - `Shift+ArrowLeft` / `Shift+ArrowRight` use `shiftStep`
726
+ - resize keyboard handling should not hijack the existing table cell navigation when the resizer itself is not focused
727
+
728
+ ### Accessibility Contract
729
+
730
+ `Table.ColumnResizer` should behave like a column separator/resizer control.
731
+
732
+ Recommended attributes:
733
+
734
+ - `role="separator"`
735
+ - `aria-orientation="vertical"`
736
+ - `aria-valuenow`
737
+ - `aria-valuemin`
738
+ - `aria-valuemax`
739
+ - accessible label derived from the current column, for example `Resize Email column`
740
+
741
+ Derived data attributes:
742
+
743
+ - `data-resizing`
744
+ - `data-focused`
745
+ - `data-focus-visible`
746
+ - `data-resizable-direction="right"`
747
+
748
+ ### Internal Architecture Additions
749
+
750
+ #### `root/context.ts`
751
+
752
+ Extend column registration to include:
753
+
754
+ - `allowsResizing`
755
+ - `width`
756
+ - `defaultWidth`
757
+ - `minWidth`
758
+ - `maxWidth`
759
+
760
+ Add root-level APIs for:
761
+
762
+ - resolving effective column widths
763
+ - updating a column width by `columnId`
764
+ - starting/ending resize interactions
765
+ - reading resize state for a column
766
+
767
+ #### New Part Context Usage
768
+
769
+ `Table.ColumnResizer` should consume:
770
+
771
+ - `Table.Column` context for `columnId`
772
+ - `Table.Root` context for width state and resize actions
773
+
774
+ It should not require positional props like `index` or `for`.
775
+
776
+ ### Planned File Additions
777
+
778
+ - `packages/svelte/src/lib/table/column-resizer/table-column-resizer.svelte`
779
+ - `packages/svelte/src/lib/table/column-resizer/README.md`
780
+ - `packages/svelte/src/lib/table/column-resizer/table-column-resizer.test.ts`
781
+
782
+ Planned touched files:
783
+
784
+ - `packages/svelte/src/lib/table/index.parts.ts`
785
+ - `packages/svelte/src/lib/table/index.ts`
786
+ - `packages/svelte/src/lib/table/root/context.ts`
787
+ - `packages/svelte/src/lib/table/root/table-root.svelte`
788
+ - `packages/svelte/src/lib/table/column/table-column.svelte`
789
+ - `packages/svelte/src/lib/table/column-header-cell/table-column-header-cell.svelte`
790
+ - `packages/svelte/src/lib/table/root/table-root.test.ts`
791
+ - `docs/src/routes/docs/table/+page.svelte`
792
+
793
+ ### Testing Plan
794
+
795
+ Minimum regression coverage:
796
+
797
+ - renders a resize handle only for columns composed with `Table.ColumnResizer`
798
+ - dragging the resizer changes the associated column width only
799
+ - resizing one column does not corrupt neighboring column identity
800
+ - controlled `columnWidths` updates are reflected in the DOM
801
+ - uncontrolled `defaultWidth` is honored on mount
802
+ - keyboard resizing updates width in deterministic steps
803
+ - min/max constraints are enforced
804
+ - focus and pointer interactions on the resizer do not break table navigation
805
+
806
+ ### Recommended Implementation Order
807
+
808
+ 1. Add API fields to `Table.Column` and root context registration.
809
+ 2. Add root width state and effective width resolution.
810
+ 3. Render managed `<colgroup>` from `Table.Root`.
811
+ 4. Add `Table.ColumnResizer` pointer interaction.
812
+ 5. Add keyboard and ARIA support for the resizer.
813
+ 6. Add docs/demo and controlled/uncontrolled tests.
814
+
815
+ ### Non-Goals for the First Resize Release
816
+
817
+ - percentage/fr width math
818
+ - column resize persistence outside consumer-provided state
819
+ - multi-column proportional redistribution
820
+ - double-click auto-fit
821
+ - resize in nested/grouped headers
822
+ - resizable footer-specific behavior
823
+ - `packages/svelte/src/lib/table/header/table-header.svelte`
824
+ - `packages/svelte/src/lib/table/header/README.md`
825
+ - `packages/svelte/src/lib/table/header/table-header.test.ts`
826
+ - `packages/svelte/src/lib/table/body/table-body.svelte`
827
+ - `packages/svelte/src/lib/table/body/README.md`
828
+ - `packages/svelte/src/lib/table/body/table-body.test.ts`
829
+ - `packages/svelte/src/lib/table/empty-state/table-empty-state.svelte`
830
+ - `packages/svelte/src/lib/table/empty-state/README.md`
831
+ - `packages/svelte/src/lib/table/empty-state/table-empty-state.test.ts`
832
+ - `packages/svelte/src/lib/table/footer/table-footer.svelte`
833
+ - `packages/svelte/src/lib/table/footer/README.md`
834
+ - `packages/svelte/src/lib/table/footer/table-footer.test.ts`
835
+ - `packages/svelte/src/lib/table/row/table-row.svelte`
836
+ - `packages/svelte/src/lib/table/row/README.md`
837
+ - `packages/svelte/src/lib/table/row/table-row.test.ts`
838
+ - `packages/svelte/src/lib/table/column-header-cell/table-column-header-cell.svelte`
839
+ - `packages/svelte/src/lib/table/column-header-cell/README.md`
840
+ - `packages/svelte/src/lib/table/column-header-cell/table-column-header-cell.test.ts`
841
+ - `packages/svelte/src/lib/table/cell/table-cell.svelte`
842
+ - `packages/svelte/src/lib/table/cell/README.md`
843
+ - `packages/svelte/src/lib/table/cell/table-cell.test.ts`
844
+
845
+ ### Package Integrations
846
+
847
+ - `packages/svelte/src/lib/index.ts`
848
+ - `packages/svelte/package.json`
849
+ - `docs/src/routes/docs/table/+page.svelte`
850
+ - `README.md`
851
+ - `.changeset/*`
852
+
853
+ ## Implementation Strategy
854
+
855
+ ### Phase 1: Public Contract
856
+
857
+ - finalize the public anatomy
858
+ - finalize prop naming
859
+ - define exact responsibilities for each part
860
+ - document minimal examples
861
+
862
+ ### Phase 2: State and Navigation
863
+
864
+ - implement root context
865
+ - implement column/row/cell registration
866
+ - build 2D navigation
867
+ - add data attributes and focus contract
868
+
869
+ ### Phase 3: Selection and Sorting
870
+
871
+ - add controlled/uncontrolled selection
872
+ - add disabled rows
873
+ - add per-column sorting
874
+ - validate keyboard behavior
875
+
876
+ ### Phase 4: Docs and Tests
877
+
878
+ - tests for each public part
879
+ - integral harness
880
+ - base README and per-part READMEs
881
+ - docs demo page
882
+ - changeset
883
+
884
+ ## Resize Testing Plan
885
+
886
+ ### Minimum Cases
887
+
888
+ - correct semantic rendering
889
+ - required accessible label
890
+ - arrow-key navigation between cells
891
+ - row-level `Home` / `End`
892
+ - entering/leaving the grid with `Tab`
893
+ - correct focus-visible and focus attributes
894
+ - single selection
895
+ - multiple selection
896
+ - disabled rows
897
+ - sorting via keyboard and pointer
898
+ - empty state
899
+ - `Footer` renders without breaking main navigation
900
+
901
+ ### Test Inspirations
902
+
903
+ - `ListBox` tests
904
+ - `Calendar` tests
905
+ - focus contract tests
906
+
907
+ ## Main Risks
908
+
909
+ - without a clear 2D engine, the component may end up stuck between a passive table and an interactive grid
910
+ - mixing cell focus with row selection requires very explicit rules
911
+ - if `Footer` joins the focus flow too early, the model becomes unnecessarily complex
912
+ - allowing interactive content inside cells in v1 may break the keyboard experience
913
+
914
+ ## V1 Success Criteria
915
+
916
+ - the public API is clear and consistent with the rest of the library
917
+ - keyboard navigation feels close to React Aria Components in the core cases
918
+ - sorting and selection work without ambiguity
919
+ - tests cover critical behavior
920
+ - the implementation leaves real room for future phases without breaking the API
921
+
922
+ ## Recommended Next Step
923
+
924
+ Turn this plan into a more concrete API specification, part by part and prop by prop, before creating implementation files.