snice 7.8.0 → 7.9.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 (302) hide show
  1. package/README.md +1 -1
  2. package/adapters/react/SniceProvider.js +1 -1
  3. package/adapters/react/SniceRouter.js +1 -1
  4. package/adapters/react/matchRoute.js +1 -1
  5. package/adapters/react/table.d.ts +1 -0
  6. package/adapters/react/table.d.ts.map +1 -1
  7. package/adapters/react/table.js +1 -1
  8. package/adapters/react/table.js.map +1 -1
  9. package/adapters/react/table.tsx +2 -1
  10. package/adapters/react/useRequestHandler.js +1 -1
  11. package/bin/analyzer-contracts.js +17 -1
  12. package/custom-elements.json +57 -3
  13. package/dist/cdn/accordion/snice-accordion.js +1 -1
  14. package/dist/cdn/action-bar/snice-action-bar.js +1 -1
  15. package/dist/cdn/activity-feed/snice-activity-feed.js +1 -1
  16. package/dist/cdn/alert/snice-alert.js +1 -1
  17. package/dist/cdn/app-tiles/snice-app-tiles.js +1 -1
  18. package/dist/cdn/approval-flow/snice-approval-flow.js +1 -1
  19. package/dist/cdn/audio-recorder/snice-audio-recorder.js +1 -1
  20. package/dist/cdn/availability/snice-availability.js +1 -1
  21. package/dist/cdn/avatar/snice-avatar.js +1 -1
  22. package/dist/cdn/avatar-group/snice-avatar-group.js +1 -1
  23. package/dist/cdn/badge/snice-badge.js +1 -1
  24. package/dist/cdn/banner/snice-banner.js +1 -1
  25. package/dist/cdn/binpack/snice-binpack.js +1 -1
  26. package/dist/cdn/book/snice-book.js +1 -1
  27. package/dist/cdn/booking/snice-booking.js +1 -1
  28. package/dist/cdn/breadcrumbs/snice-breadcrumbs.js +1 -1
  29. package/dist/cdn/button/snice-button.js +1 -1
  30. package/dist/cdn/calendar/README.md +2 -2
  31. package/dist/cdn/calendar/snice-calendar.js +537 -44
  32. package/dist/cdn/calendar/snice-calendar.js.map +1 -1
  33. package/dist/cdn/calendar/snice-calendar.min.js +2 -2
  34. package/dist/cdn/calendar/snice-calendar.min.js.map +1 -1
  35. package/dist/cdn/camera/snice-camera.js +1 -1
  36. package/dist/cdn/camera-annotate/snice-camera-annotate.js +1 -1
  37. package/dist/cdn/candlestick/snice-candlestick.js +1 -1
  38. package/dist/cdn/card/snice-card.js +1 -1
  39. package/dist/cdn/carousel/snice-carousel.js +1 -1
  40. package/dist/cdn/cart/snice-cart.js +1 -1
  41. package/dist/cdn/chart/snice-chart.js +1 -1
  42. package/dist/cdn/chat/snice-chat.js +1 -1
  43. package/dist/cdn/checkbox/snice-checkbox.js +1 -1
  44. package/dist/cdn/chip/snice-chip.js +1 -1
  45. package/dist/cdn/code-block/README.md +2 -2
  46. package/dist/cdn/code-block/snice-code-block.js +177 -167
  47. package/dist/cdn/code-block/snice-code-block.js.map +1 -1
  48. package/dist/cdn/code-block/snice-code-block.min.js +2 -2
  49. package/dist/cdn/code-block/snice-code-block.min.js.map +1 -1
  50. package/dist/cdn/color-display/snice-color-display.js +1 -1
  51. package/dist/cdn/color-picker/snice-color-picker.js +1 -1
  52. package/dist/cdn/command-palette/README.md +1 -1
  53. package/dist/cdn/command-palette/snice-command-palette.js +2 -2
  54. package/dist/cdn/command-palette/snice-command-palette.js.map +1 -1
  55. package/dist/cdn/command-palette/snice-command-palette.min.js +1 -1
  56. package/dist/cdn/command-palette/snice-command-palette.min.js.map +1 -1
  57. package/dist/cdn/comments/snice-comments.js +1 -1
  58. package/dist/cdn/countdown/snice-countdown.js +1 -1
  59. package/dist/cdn/cropper/snice-cropper.js +1 -1
  60. package/dist/cdn/data-card/snice-data-card.js +1 -1
  61. package/dist/cdn/date-picker/snice-date-picker.js +1 -1
  62. package/dist/cdn/date-range-picker/snice-date-range-picker.js +1 -1
  63. package/dist/cdn/date-time-picker/snice-date-time-picker.js +1 -1
  64. package/dist/cdn/diff/snice-diff.js +1 -1
  65. package/dist/cdn/divider/snice-divider.js +1 -1
  66. package/dist/cdn/doc/snice-doc.js +1 -1
  67. package/dist/cdn/draw/snice-draw.js +1 -1
  68. package/dist/cdn/drawer/snice-drawer.js +1 -1
  69. package/dist/cdn/empty-state/snice-empty-state.js +1 -1
  70. package/dist/cdn/estimate/snice-estimate.js +1 -1
  71. package/dist/cdn/file-gallery/snice-file-gallery.js +1 -1
  72. package/dist/cdn/file-upload/snice-file-upload.js +1 -1
  73. package/dist/cdn/flip-card/snice-flip-card.js +1 -1
  74. package/dist/cdn/flow/snice-flow.js +1 -1
  75. package/dist/cdn/form-layout/snice-form-layout.js +1 -1
  76. package/dist/cdn/funnel/snice-funnel.js +1 -1
  77. package/dist/cdn/gantt/snice-gantt.js +1 -1
  78. package/dist/cdn/gauge/snice-gauge.js +1 -1
  79. package/dist/cdn/grid/snice-grid.js +1 -1
  80. package/dist/cdn/heatmap/snice-heatmap.js +1 -1
  81. package/dist/cdn/image/snice-image.js +1 -1
  82. package/dist/cdn/input/snice-input.js +1 -1
  83. package/dist/cdn/invoice/snice-invoice.js +1 -1
  84. package/dist/cdn/kanban/snice-kanban.js +1 -1
  85. package/dist/cdn/key-value/snice-key-value.js +1 -1
  86. package/dist/cdn/kpi/snice-kpi.js +1 -1
  87. package/dist/cdn/layout/snice-layout.js +1 -1
  88. package/dist/cdn/leaderboard/snice-leaderboard.js +1 -1
  89. package/dist/cdn/link/snice-link.js +1 -1
  90. package/dist/cdn/link-preview/snice-link-preview.js +1 -1
  91. package/dist/cdn/list/snice-list.js +1 -1
  92. package/dist/cdn/location/snice-location.js +1 -1
  93. package/dist/cdn/login/snice-login.js +1 -1
  94. package/dist/cdn/map/snice-map.js +1 -1
  95. package/dist/cdn/markdown/snice-markdown.js +1 -1
  96. package/dist/cdn/masonry/snice-masonry.js +1 -1
  97. package/dist/cdn/menu/snice-menu.js +1 -1
  98. package/dist/cdn/message-strip/snice-message-strip.js +1 -1
  99. package/dist/cdn/modal/snice-modal.js +1 -1
  100. package/dist/cdn/music-player/snice-music-player.js +1 -1
  101. package/dist/cdn/nav/snice-nav.js +1 -1
  102. package/dist/cdn/network-graph/snice-network-graph.js +1 -1
  103. package/dist/cdn/notification-center/snice-notification-center.js +1 -1
  104. package/dist/cdn/order-tracker/snice-order-tracker.js +1 -1
  105. package/dist/cdn/org-chart/snice-org-chart.js +1 -1
  106. package/dist/cdn/pagination/snice-pagination.js +1 -1
  107. package/dist/cdn/paint/snice-paint.js +1 -1
  108. package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +1 -1
  109. package/dist/cdn/permission-matrix/snice-permission-matrix.js +1 -1
  110. package/dist/cdn/podcast-player/snice-podcast-player.js +1 -1
  111. package/dist/cdn/popover/snice-popover.js +1 -1
  112. package/dist/cdn/pricing-table/snice-pricing-table.js +1 -1
  113. package/dist/cdn/product-card/snice-product-card.js +1 -1
  114. package/dist/cdn/progress/snice-progress.js +2 -2
  115. package/dist/cdn/progress/snice-progress.js.map +1 -1
  116. package/dist/cdn/progress/snice-progress.min.js +1 -1
  117. package/dist/cdn/progress/snice-progress.min.js.map +1 -1
  118. package/dist/cdn/progress-ring/snice-progress-ring.js +1 -1
  119. package/dist/cdn/qr-code/README.md +2 -2
  120. package/dist/cdn/qr-code/snice-qr-code.js +19 -3
  121. package/dist/cdn/qr-code/snice-qr-code.js.map +1 -1
  122. package/dist/cdn/qr-code/snice-qr-code.min.js +2 -2
  123. package/dist/cdn/qr-code/snice-qr-code.min.js.map +1 -1
  124. package/dist/cdn/qr-reader/snice-qr-reader.js +1 -1
  125. package/dist/cdn/radio/snice-radio.js +1 -1
  126. package/dist/cdn/range-slider/snice-range-slider.js +1 -1
  127. package/dist/cdn/rating/snice-rating.js +1 -1
  128. package/dist/cdn/receipt/snice-receipt.js +5 -2
  129. package/dist/cdn/receipt/snice-receipt.js.map +1 -1
  130. package/dist/cdn/receipt/snice-receipt.min.js +1 -1
  131. package/dist/cdn/receipt/snice-receipt.min.js.map +1 -1
  132. package/dist/cdn/recipe/snice-recipe.js +1 -1
  133. package/dist/cdn/runtime/snice-runtime.esm.js +4 -4
  134. package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
  135. package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
  136. package/dist/cdn/runtime/snice-runtime.js +4 -4
  137. package/dist/cdn/runtime/snice-runtime.js.map +1 -1
  138. package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
  139. package/dist/cdn/sankey/snice-sankey.js +1 -1
  140. package/dist/cdn/segmented-control/snice-segmented-control.js +1 -1
  141. package/dist/cdn/select/README.md +2 -2
  142. package/dist/cdn/select/snice-select.js +6 -9
  143. package/dist/cdn/select/snice-select.js.map +1 -1
  144. package/dist/cdn/select/snice-select.min.js +2 -2
  145. package/dist/cdn/select/snice-select.min.js.map +1 -1
  146. package/dist/cdn/skeleton/snice-skeleton.js +1 -1
  147. package/dist/cdn/slider/snice-slider.js +1 -1
  148. package/dist/cdn/sortable/snice-sortable.js +1 -1
  149. package/dist/cdn/sparkline/snice-sparkline.js +1 -1
  150. package/dist/cdn/spinner/snice-spinner.js +1 -1
  151. package/dist/cdn/split-button/snice-split-button.js +1 -1
  152. package/dist/cdn/split-pane/snice-split-pane.js +1 -1
  153. package/dist/cdn/spotlight/snice-spotlight.js +1 -1
  154. package/dist/cdn/stat-group/snice-stat-group.js +1 -1
  155. package/dist/cdn/step-input/snice-step-input.js +1 -1
  156. package/dist/cdn/stepper/snice-stepper.js +1 -1
  157. package/dist/cdn/switch/snice-switch.js +1 -1
  158. package/dist/cdn/table/README.md +2 -2
  159. package/dist/cdn/table/snice-table.esm.js +1940 -407
  160. package/dist/cdn/table/snice-table.esm.js.map +1 -1
  161. package/dist/cdn/table/snice-table.esm.min.js +242 -53
  162. package/dist/cdn/table/snice-table.esm.min.js.map +1 -1
  163. package/dist/cdn/table/snice-table.js +1940 -407
  164. package/dist/cdn/table/snice-table.js.map +1 -1
  165. package/dist/cdn/table/snice-table.min.js +234 -45
  166. package/dist/cdn/table/snice-table.min.js.map +1 -1
  167. package/dist/cdn/tabs/snice-tabs.js +1 -1
  168. package/dist/cdn/tag/snice-tag.js +1 -1
  169. package/dist/cdn/tag-input/snice-tag-input.js +1 -1
  170. package/dist/cdn/terminal/snice-terminal.js +1 -1
  171. package/dist/cdn/testimonial/snice-testimonial.js +1 -1
  172. package/dist/cdn/textarea/snice-textarea.js +1 -1
  173. package/dist/cdn/time-picker/snice-time-picker.js +1 -1
  174. package/dist/cdn/time-range-picker/snice-time-range-picker.js +1 -1
  175. package/dist/cdn/timeline/snice-timeline.js +1 -1
  176. package/dist/cdn/timer/snice-timer.js +1 -1
  177. package/dist/cdn/toast/snice-toast.js +1 -1
  178. package/dist/cdn/tooltip/snice-tooltip.js +1 -1
  179. package/dist/cdn/tree/snice-tree.js +1 -1
  180. package/dist/cdn/treemap/snice-treemap.js +1 -1
  181. package/dist/cdn/user-card/snice-user-card.js +1 -1
  182. package/dist/cdn/video-player/snice-video-player.js +1 -1
  183. package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +1 -1
  184. package/dist/cdn/waterfall/snice-waterfall.js +1 -1
  185. package/dist/cdn/weather/snice-weather.js +1 -1
  186. package/dist/cdn/work-order/snice-work-order.js +1 -1
  187. package/dist/components/calendar/snice-calendar.d.ts +187 -5
  188. package/dist/components/calendar/snice-calendar.js +537 -44
  189. package/dist/components/calendar/snice-calendar.js.map +1 -1
  190. package/dist/components/calendar/snice-calendar.types.d.ts +2 -1
  191. package/dist/components/code-block/formatter.d.ts +6 -0
  192. package/dist/components/code-block/formatter.js +174 -164
  193. package/dist/components/code-block/formatter.js.map +1 -1
  194. package/dist/components/code-block/grammars/snice.json +1 -1
  195. package/dist/components/code-block/grammars/typescript.json +1 -1
  196. package/dist/components/command-palette/snice-command-palette.js +1 -1
  197. package/dist/components/command-palette/snice-command-palette.js.map +1 -1
  198. package/dist/components/progress/snice-progress.js +1 -1
  199. package/dist/components/progress/snice-progress.js.map +1 -1
  200. package/dist/components/qr-code/qrcode.d.ts +1 -1
  201. package/dist/components/qr-code/qrcode.js +18 -2
  202. package/dist/components/qr-code/qrcode.js.map +1 -1
  203. package/dist/components/receipt/snice-receipt.js +4 -1
  204. package/dist/components/receipt/snice-receipt.js.map +1 -1
  205. package/dist/components/select/snice-select.d.ts +1 -1
  206. package/dist/components/select/snice-select.js +5 -8
  207. package/dist/components/select/snice-select.js.map +1 -1
  208. package/dist/components/table/snice-cell-actions.js +1 -1
  209. package/dist/components/table/snice-cell-actions.js.map +1 -1
  210. package/dist/components/table/snice-cell-color.js +1 -1
  211. package/dist/components/table/snice-cell-color.js.map +1 -1
  212. package/dist/components/table/snice-cell-date.js +20 -8
  213. package/dist/components/table/snice-cell-date.js.map +1 -1
  214. package/dist/components/table/snice-cell-email.js +1 -1
  215. package/dist/components/table/snice-cell-email.js.map +1 -1
  216. package/dist/components/table/snice-cell-image.js +1 -1
  217. package/dist/components/table/snice-cell-image.js.map +1 -1
  218. package/dist/components/table/snice-cell-json.js +1 -1
  219. package/dist/components/table/snice-cell-json.js.map +1 -1
  220. package/dist/components/table/snice-cell-link.js +1 -1
  221. package/dist/components/table/snice-cell-link.js.map +1 -1
  222. package/dist/components/table/snice-cell-location.js +1 -1
  223. package/dist/components/table/snice-cell-location.js.map +1 -1
  224. package/dist/components/table/snice-cell-phone.js +1 -1
  225. package/dist/components/table/snice-cell-phone.js.map +1 -1
  226. package/dist/components/table/snice-cell-status.js +1 -1
  227. package/dist/components/table/snice-cell-status.js.map +1 -1
  228. package/dist/components/table/snice-cell.d.ts +10 -0
  229. package/dist/components/table/snice-cell.js +59 -13
  230. package/dist/components/table/snice-cell.js.map +1 -1
  231. package/dist/components/table/snice-row.d.ts +0 -1
  232. package/dist/components/table/snice-row.js +4 -10
  233. package/dist/components/table/snice-row.js.map +1 -1
  234. package/dist/components/table/snice-table.d.ts +170 -2
  235. package/dist/components/table/snice-table.js +868 -189
  236. package/dist/components/table/snice-table.js.map +1 -1
  237. package/dist/components/table/snice-table.types.d.ts +17 -0
  238. package/dist/components/table/table-cell-presentation.d.ts +17 -4
  239. package/dist/components/table/table-cell-presentation.js +37 -11
  240. package/dist/components/table/table-cell-presentation.js.map +1 -1
  241. package/dist/components/table/table-column-manager.d.ts +153 -2
  242. package/dist/components/table/table-column-manager.js +364 -43
  243. package/dist/components/table/table-column-manager.js.map +1 -1
  244. package/dist/components/table/table-date.d.ts +29 -0
  245. package/dist/components/table/table-date.js +78 -0
  246. package/dist/components/table/table-date.js.map +1 -0
  247. package/dist/components/table/table-export.js +6 -2
  248. package/dist/components/table/table-export.js.map +1 -1
  249. package/dist/components/table/table-filter-engine.d.ts +9 -0
  250. package/dist/components/table/table-filter-engine.js +58 -14
  251. package/dist/components/table/table-filter-engine.js.map +1 -1
  252. package/dist/components/table/table-keyboard.d.ts +8 -0
  253. package/dist/components/table/table-keyboard.js +14 -3
  254. package/dist/components/table/table-keyboard.js.map +1 -1
  255. package/dist/components/table/table-toolbar.js +1 -0
  256. package/dist/components/table/table-toolbar.js.map +1 -1
  257. package/dist/components/table/table-tree-data.d.ts +2 -0
  258. package/dist/components/table/table-tree-data.js +35 -9
  259. package/dist/components/table/table-tree-data.js.map +1 -1
  260. package/dist/components/table/table-virtualizer.d.ts +44 -7
  261. package/dist/components/table/table-virtualizer.js +101 -54
  262. package/dist/components/table/table-virtualizer.js.map +1 -1
  263. package/dist/components/theme/theme.css +9 -0
  264. package/dist/index.cjs +1 -1
  265. package/dist/index.esm.js +1 -1
  266. package/dist/index.iife.js +1 -1
  267. package/dist/react/SniceProvider.js +1 -1
  268. package/dist/react/SniceRouter.js +1 -1
  269. package/dist/react/index.js +1 -1
  270. package/dist/react/matchRoute.js +1 -1
  271. package/dist/react/useRequestHandler.js +1 -1
  272. package/dist/symbols.cjs +1 -1
  273. package/dist/symbols.esm.js +1 -1
  274. package/dist/testing-dom.cjs +1 -1
  275. package/dist/testing-dom.esm.js +1 -1
  276. package/dist/transitions.cjs +1 -1
  277. package/dist/transitions.esm.js +1 -1
  278. package/docs/ai/DEVELOPMENT.md +93 -2
  279. package/docs/ai/components/calendar.md +113 -10
  280. package/docs/ai/components/code-block.md +4 -0
  281. package/docs/ai/components/doc.md +1 -1
  282. package/docs/ai/components/progress.md +1 -0
  283. package/docs/ai/components/receipt.md +1 -1
  284. package/docs/ai/components/table.md +29 -30
  285. package/docs/ai/components/theme.md +1 -0
  286. package/docs/ai/events.md +30 -0
  287. package/docs/ai/theme.md +1 -0
  288. package/docs/components/calendar.md +171 -11
  289. package/docs/components/code-block.md +2 -0
  290. package/docs/components/doc.md +1 -1
  291. package/docs/components/progress.md +13 -0
  292. package/docs/components/receipt.md +1 -0
  293. package/docs/components/table.md +274 -15
  294. package/docs/components/theme.md +5 -0
  295. package/docs/events.md +49 -0
  296. package/docs/plans/2026-03-06-date-range-picker-plan.md +4 -4
  297. package/docs/plans/2026-03-07-table-mui-parity-design.md +1 -1
  298. package/docs/plans/2026-03-09-action-bar-plan.md +11 -11
  299. package/docs/superpowers/specs/2026-07-09-table-incredible-design.md +1 -1
  300. package/docs/superpowers/specs/2026-07-09-table-phase0-implementation.md +2 -2
  301. package/package.json +3 -1
  302. package/vscode.html-custom-data.json +13 -1
@@ -51,7 +51,7 @@ The imperative `columns` and `data` properties are the most complete path. Decla
51
51
  | `searchText` | `string` | `''` | Current legacy/controller search text. This is a plain JS field so typing does not rerender and steal focus |
52
52
  | `searchDebounce` (attr: `search-debounce`) | `number` | `500` | Delay in milliseconds before the legacy search control requests data |
53
53
  | `currentSort` | `Array<{ column: string; direction: 'asc' \| 'desc' }>` | `[]` | Reactive JS-only sort model. Assignment sorts locally or requests remote data |
54
- | `selectedRows` | `number[]` | `[]` | Reactive JS-only indices into the raw `data` array, not indices into a filtered or sorted page |
54
+ | `selectedRows` | `number[]` | `[]` | Reactive JS-only indices into the raw `data` array, not indices into a filtered or sorted page. Reordering the same rows (a local sort, or a re-delivery of the same row objects in another order) re-resolves the indices so the selection stays on the rows the user picked |
55
55
  | `selectionMode` (attr: `selection-mode`) | `'none' \| 'single' \| 'multiple'` | `'multiple'` | Selection model. `'none'` removes selection controls; `'single'` shows row checkboxes without select-all; `'multiple'` enables row, range, group, and select-all selection |
56
56
  | `selector` | `string` | `''` | Comma-joined value from the legacy filter selector and the value sent in remote requests |
57
57
  | `selectorOptions` | `Array<{ value: string; label: string }>` | `[]` | JS-only options for the legacy `filterable` selector |
@@ -69,6 +69,7 @@ The imperative `columns` and `data` properties are the most complete path. Decla
69
69
  | `editable` | `boolean` | `false` | Enables the inline editing engine |
70
70
  | `editMode` (attr: `edit-mode`) | `'cell' \| 'row'` | `'cell'` | Edits one cell or every editable cell in a row |
71
71
  | `density` | `'compact' \| 'standard' \| 'comfortable'` | `'standard'` | Changes header and cell padding, rerenders rows, and emits `density-change` after post-mount assignment |
72
+ | `columnFit` (attr: `column-fit`) | `'scroll' \| 'squish'` | `'scroll'` | How columns relate to the frame's width. `scroll` keeps every column at or above its `minWidth` and lets the frame scroll horizontally; `squish` relaxes the minimums so the columns always fit the frame and never scroll sideways. See [How a column gets its width](#how-a-column-gets-its-width) |
72
73
  | `columnResize` (attr: `column-resize`) | `boolean` | `false` | Adds draggable resize handles to resizable columns |
73
74
  | `headerFilters` (attr: `header-filters`) | `boolean` | `false` | Adds debounced contains inputs below filterable headers |
74
75
  | `quickFilter` (attr: `quick-filter`) | `boolean` | `false` | Shows a debounced local/remote quick-filter input backed by the same model as `setQuickFilter(text)` |
@@ -152,12 +153,45 @@ Column capability flags default to enabled unless explicitly set to `false` when
152
153
 
153
154
  `formatter` is the row-aware display override for every built-in cell.
154
155
  `valueFormatter` is the fallback display formatter and is also used by the
155
- editing pipeline, aggregate output, and formatted clipboard export.
156
- `valueGetter` derives the working value for cell display, local sorting, and
157
- aggregation. `valueParser` and
156
+ editing pipeline, aggregate output, and formatted clipboard export. When a
157
+ column declares both, `formatter` wins on every path rendered cells, group
158
+ and table aggregate footers, declarative rows, and formatted export.
159
+ `valueGetter` derives the working value for cell display, local sorting,
160
+ aggregation, CSV/clipboard export, and declarative `<snice-row>` cells.
161
+ `valueParser` and
158
162
  `valueSetter` run during editing; a setter may return either the final field
159
163
  value or an updated row object.
160
164
 
165
+ A column that declares a display formatter renders through
166
+ `<snice-cell-text>`: the formatter output is a string, so the table resolves
167
+ the pipeline once and hands the text cell the result, keeping the column's
168
+ type-based alignment. The rendered cell's `value` (property and attribute) is
169
+ therefore always the DISPLAY value — the formatter output where one is
170
+ declared, the post-`valueGetter` working value otherwise.
171
+
172
+ The one cell that is not a typed cell element is the tree group column, which
173
+ renders its label as plain text beside the indent and chevron. It resolves the
174
+ same pipeline (`renderCell`, then `formatter`, then `valueFormatter`), but the
175
+ resulting text lives directly in the cell rather than in a `<snice-cell-text>`,
176
+ so there is no `value` attribute to read there.
177
+
178
+ When the working value is empty — the row field is `null`, or the row never
179
+ carried the field at all — a table-rendered cell falls back to the empty value
180
+ of its own type rather than to a blanket empty string:
181
+
182
+ | Column type | Empty value |
183
+ | --- | --- |
184
+ | `boolean` | `false` |
185
+ | `rating`, `progress`, `duration`, `filesize` | `0` |
186
+ | `json` | `null` |
187
+ | everything else | `''` |
188
+
189
+ This is the same per-type value semantics the standalone cells declare, so a
190
+ row that omits a boolean field renders as false — never as true — and a row
191
+ that omits a progress field renders an empty bar rather than an object. A
192
+ declarative `<snice-row>` cell instead receives the empty working value
193
+ unchanged, so it renders its own no-value state.
194
+
161
195
  The declared `ColumnType` union is:
162
196
 
163
197
  ```typescript
@@ -503,6 +537,36 @@ Standalone behavior boundaries:
503
537
  - Links auto-open HTTP(S) values externally. Location builds Google, OpenStreetMap, or Apple URLs. JSON supports collapse, toggle, depth controls, JSON text, and direct object assignment.
504
538
  - Date supports relative time, custom tokens, and optional time. Image supports fallback, `variant`, size, lazy loading, and a placeholder/error state.
505
539
 
540
+ ### Dates and timezones
541
+
542
+ A date value that carries no time is a **calendar day**, and the table reads it
543
+ at *local* midnight. `'2026-03-15'` therefore renders as March 15 in every
544
+ timezone — it is never shifted to the 14th by a negative UTC offset, as the
545
+ platform's own `new Date('2026-03-15')` (UTC midnight) would. The rule covers
546
+ the shortened ISO forms too: `'2026-03'` is March 1 and `'2026'` is January 1.
547
+
548
+ A value that *does* carry a time keeps instant semantics and is projected into
549
+ the viewer's local zone: `'2026-03-15T23:30:00Z'` is the evening of the 15th in
550
+ London and the morning of the 16th in Tokyo. Zoneless date-times
551
+ (`'2026-03-15T10:00'`), epoch numbers, and `Date` objects are already local or
552
+ absolute and pass through untouched.
553
+
554
+ One parse serves the whole date layer, so the day a cell paints is the day the
555
+ row editor holds, the day a CSV/clipboard export ships, and the day a `date`
556
+ column filter (`is`, `before`, `onOrBefore`, `after`, `onOrAfter`) compares
557
+ against.
558
+
559
+ A date-only *filter bound* names the whole day, so all five operators agree
560
+ about what falls on it. Filtering `onOrBefore` `'2026-03-15'` keeps a row
561
+ timestamped `'2026-03-15T09:00'`, and `after` `'2026-03-15'` excludes it —
562
+ exactly the rows `is` would report for that day, no more and no less. Give the
563
+ bound its own time (`'2026-03-15T12:00'`) to compare against that instant
564
+ instead.
565
+
566
+ An impossible day is not silently rounded: `'2026-02-30'` and `'2026-13-01'`
567
+ are rejected the way the platform rejects them, and the cell shows the raw
568
+ value rather than inventing March 2 or January 2027.
569
+
506
570
  ## Methods
507
571
 
508
572
  ### Data, Requests, and Rendering
@@ -527,10 +591,10 @@ Standalone behavior boundaries:
527
591
  |--------|-----------|-------------|
528
592
  | `getSelectedData()` | — | Returns row objects for the raw `selectedRows` indices |
529
593
  | `setSelectabilityCheck()` | `(row, index) => boolean` | Disables selection for rows that fail the predicate and removes them from current, range, group, and select-all selections |
530
- | `updateRowSelectionState()` | — | Synchronizes rendered rows from `selectedRows` |
531
- | `updateSelectAllState()` | — | Synchronizes the multiple-mode select-all checkbox |
594
+ | `updateRowSelectionState()` | — | Synchronizes rendered rows from `selectedRows`. Each row is matched by its own data index, so structural rows that share the body — group headers, aggregate footers, virtual spacers, and the bottom filler — are never marked selected, and a paged body cannot mark the wrong row |
595
+ | `updateSelectAllState()` | — | Synchronizes the multiple-mode select-all checkbox. Also runs on every body render, so a delivery or cleared filter that changes the selectable row count re-derives the checked/indeterminate state |
532
596
  | `toggleSort()` | `columnKey: string, multiSort = false` | Cycles ascending → descending → none. `multiSort=false` replaces other sorts; header clicks pass `true` and therefore accumulate sorts |
533
- | `setSortComparator()` | `columnKey, (a, b, direction) => number` | Installs a custom local comparator |
597
+ | `setSortComparator()` | `columnKey, (a, b, direction) => number` | Installs a custom local comparator. It receives the column's working values — the `valueGetter` result where one is declared |
534
598
  | `setColumnFilter()` | `column, operator, value` | Adds or replaces one column filter |
535
599
  | `removeColumnFilter()` | `column: string` | Removes a column filter |
536
600
  | `setQuickFilter()` | `text: string` | Searches all configured columns. Applies synchronously in local mode and requests data in remote mode |
@@ -557,11 +621,11 @@ Filter operators are type-specific:
557
621
  | `getColumnVisibility()` | — | Returns `{ [columnKey]: boolean }` |
558
622
  | `pinColumn()` | `key, side: 'left' \| 'right'` | Pins a pinnable column to a physical edge |
559
623
  | `unpinColumn()` | `key: string` | Removes a column pin |
560
- | `autoSizeColumn()` | `key: string` | Measures the rendered header and body cells |
561
- | `autoSizeAllColumns()` | — | Auto-sizes all managed columns |
624
+ | `autoSizeColumn()` | `key: string` | Fits one column to the content of its rendered header and body cells — it shrinks as readily as it grows — and repaints header and body with the measured width |
625
+ | `autoSizeAllColumns()` | — | Auto-sizes every managed column in one measuring pass |
562
626
  | `moveColumn()` | `key, toIndex: number` | Moves an unpinned, reorderable column and emits `column-order-change` |
563
627
  | `setColumnGroups()` | `Array<{ label, children, headerClass? }>` | Adds a multi-level header row. `children` contains column keys |
564
- | `scrollToRow()` | `index: number` | Scrolls the virtualized display to a raw data row, translating grouped/tree positions when needed |
628
+ | `scrollToRow()` | `index: number` | Scrolls the virtualized display to a raw data row, translating grouped/tree positions when needed. The virtual window is recomputed as part of the call, so the row is in the DOM when it returns — a programmatic scroll fires no `scroll` event to do it later |
565
629
  | `scrollToColumn()` | `columnKey: string` | Scrolls the rendered header into view |
566
630
  | `getScrollPosition()` | — | Returns the virtualizer's `{ top, left }` position |
567
631
  | `toggleFullscreen()` | — | Toggles native fullscreen with a CSS fallback |
@@ -717,7 +781,7 @@ the corresponding user-action event; density is the explicit exception.
717
781
  | `columns` | Declarative `<snice-column>` definitions |
718
782
  | `rows` | Declarative `<snice-row>` data |
719
783
  | `header` | Super-header content above the native column headers |
720
- | `empty-state` | Custom content cloned into the empty table body |
784
+ | `empty-state` | Custom content cloned into the empty table body, on the virtualized path as well |
721
785
 
722
786
  `<snice-table>` has no default slot.
723
787
 
@@ -753,11 +817,16 @@ the corresponding user-action event; density is the explicit exception.
753
817
 
754
818
  The internal native body exposes `row` and `cell` parts, so page CSS can style body rows and cells directly (e.g. `snice-table::part(cell) { ... }`). Standalone cell-component parts are not forwarded through `<snice-table>`.
755
819
 
820
+ When a sized host is taller than its rows, the table appends an inert filler row so the column grid reaches the bottom edge of the frame (this happens on the virtualized path too). The filler is not data: it carries no row identity, no ARIA, no selection state, and no `row`/`cell` part — a `::part(row)` border rule therefore stops at the last real row instead of drawing a phantom one underneath it.
821
+
822
+ **A sized host only.** The filler may only consume space the frame already has. If the host's height is content-driven — `:host { height: 100% }` resolving against an auto or max-content container, such as a stretched grid/flex item or a plain block parent — then the filler would be part of the content the frame is measured from, and it would grow the box it is trying to fill on every resize notification. The table detects this by measurement (apply the filler, confirm the frame kept its height, hand back any growth it caused), so on a content-driven host you get either a smaller filler or none at all, never a growing table. Give the host a real height (`style="height: 24rem"`, a flex/grid track, or a sized ancestor) whenever you want the grid to reach the bottom edge.
823
+
756
824
  ## CSS Custom Properties
757
825
 
758
826
  | Property | Description | Default |
759
827
  |----------|-------------|---------|
760
828
  | `--snice-table-body-bg` | Native table body background | `--snice-color-surface` |
829
+ | `--snice-table-stripe-bg` | `striped` row tint | `--snice-color-overlay-stripe` |
761
830
  | `--snice-table-group-header-bg` | Group-header background | `--snice-color-surface-container-low` |
762
831
  | `--snice-table-group-header-color` | Group-header text | `--snice-color-text` |
763
832
  | `--snice-table-group-count-bg` | Group count badge background | `--snice-color-surface-container-high` |
@@ -769,6 +838,7 @@ The internal native body exposes `row` and `cell` parts, so page CSS can style b
769
838
  | `--snice-table-cell-padding` | Body/header cell padding | `--snice-spacing-xs` `--snice-spacing-sm` |
770
839
  | `--snice-table-cell-border` | Vertical (right) cell border; set to `none` to drop grid lines | `1px solid --snice-color-border` |
771
840
  | `--snice-table-row-border` | Horizontal (bottom) row border | `1px solid --snice-color-border` |
841
+ | `--snice-table-cell-min-width` | Width floor of the typed cells (`email`, `status`, `link`, `phone`, `location`, `json`, `color`, `image`, `actions`); `column-fit="squish"` sets it to `0` on a squished cell so the content ellipsises | `6.25rem` |
772
842
 
773
843
  The component also consumes global Snice color, spacing, typography, radius, focus-ring, shadow, and transition tokens.
774
844
 
@@ -801,6 +871,51 @@ import 'snice/components/table/snice-table';
801
871
 
802
872
  `setColumns()` is equally reactive. `setData()` is deliberately non-eager for bulk loading; pair it with an explicit `renderBody()` or prefer `table.data = rows`.
803
873
 
874
+ ### The assignment is the signal, not the identity
875
+
876
+ `data` and `columns` re-render on **every** assignment, including one that hands
877
+ back the array the table already holds. Mutating rows in place and re-publishing
878
+ them is a supported pattern:
879
+
880
+ ```typescript
881
+ rows.push({ name: 'Carol Diaz', email: 'carol@example.com', age: 41 });
882
+ table.data = rows; // same array reference — still re-renders
883
+ ```
884
+
885
+ Most Snice properties use identity to decide whether anything changed, which is
886
+ why assigning an unchanged object elsewhere is a no-op. `data` and `columns`
887
+ opt out of that check, because they are bulk payloads applications routinely
888
+ edit in place. The cost is small and bounded: a genuinely redundant assignment
889
+ (`table.data = table.data` with nothing changed) buys a render pass that would
890
+ otherwise have been skipped, and renders are coalesced per microtask, so a burst
891
+ of assignments in one tick still paints once.
892
+
893
+ A `columns` assignment is a **new configuration**, not a patch. The array you
894
+ assign is the source of truth for painted order, `pinned`, `width`, and which
895
+ columns exist at all — a column the new array does not declare is dropped from
896
+ the model entirely, and a column it re-declares comes back visible at the
897
+ position and width it declares. That also means the assignment discards column
898
+ state the user established through the UI (a dragged resize, a manual reorder, a
899
+ column hidden with `setColumnVisible()`) for the columns it re-declares.
900
+
901
+ Because `columns` ignores identity too, this is true of *every* assignment —
902
+ handing back the same array, mutated or untouched, re-applies the declaration
903
+ just as a fresh array would. Mutating the definitions you already published is
904
+ therefore not a way around it. To keep user column state, do not re-assign
905
+ `columns`: drive the change through the column APIs (`setColumnVisible()`,
906
+ `moveColumn()`, `pinColumn()`, `unpinColumn()`), which update the model in place.
907
+
908
+ Rows delivered while a client-side sort is active land in that sorted order —
909
+ `currentSort` and the rendered body can never disagree. In remote mode the
910
+ server owns the ordering, so the delivered order is rendered as-is.
911
+
912
+ Row rendering recycles the `<tr>` of a row object it has already painted, and a
913
+ row is repainted when its own field values change — so patching a row in place
914
+ and re-delivering it (`table.data = [...rows]`, a poll or websocket update)
915
+ repaints that row while untouched rows keep their DOM. The comparison is
916
+ shallow: a mutation buried inside a nested object value is not detected, so
917
+ hand over a new row object for those.
918
+
804
919
  ## Examples
805
920
 
806
921
  ### Density and List Styling
@@ -881,11 +996,26 @@ Use `selectable` with `selection-mode`. In multiple mode, a plain click or Ctrl/
881
996
 
882
997
  // Runtime mode changes rebuild the selection controls.
883
998
  table.selectionMode = 'single';
999
+
1000
+ // So do runtime `selectable` changes: the header select-all cell and every
1001
+ // row checkbox are added or removed together.
1002
+ table.selectable = false;
884
1003
  </script>
885
1004
  ```
886
1005
 
887
1006
  In single mode, selecting a row replaces the previous selection; its row checkbox can clear the selection. In none mode, selection controls and row-selection behavior are removed.
888
1007
 
1008
+ Selection is anchored to the row, not to its position. Sorting locally, or
1009
+ re-delivering the same row objects in a different order, re-resolves
1010
+ `selectedRows` against those objects, so `getSelectedData()` and the highlighted
1011
+ rows stay on the rows the user picked. Delivering a different row set keeps the
1012
+ documented raw-index semantics.
1013
+
1014
+ Every selection path — a row click, a row checkbox, select-all, keyboard
1015
+ selection, or a programmatic `selectedRows` assignment — writes both
1016
+ `data-selected` and `aria-selected="true"` on the affected rows, so assistive
1017
+ technology hears the change immediately rather than at the next full render.
1018
+
889
1019
  ### Rich Cells and Currency-Looking Columns
890
1020
 
891
1021
  Use specialized runtime types for status, progress, actions, and related cells. Use `numberFormat` for currency-looking values inside the table until the table routes `currency` through its dedicated currency cell.
@@ -926,6 +1056,31 @@ table.addEventListener('cell-action', (event) => {
926
1056
  });
927
1057
  ```
928
1058
 
1059
+ #### Accounting notation
1060
+
1061
+ `type: 'accounting'` is currency notation, not a bare number: it renders the
1062
+ currency symbol, groups thousands, and puts negatives in parentheses instead of
1063
+ using a minus sign. Positives are padded with one non-breaking space so their
1064
+ last digit stays under the closing paren of the row above in a right-aligned
1065
+ column. An `accounting` cell and a `currency` cell showing the same amount
1066
+ therefore agree.
1067
+
1068
+ ```javascript
1069
+ { key: 'balance', label: 'Balance', type: 'accounting' }
1070
+ // -12840.5 -> ($12,840.50) 95000 -> $95,000.00
1071
+ ```
1072
+
1073
+ A generic `<snice-cell>` with no declared `align` keeps its type's alignment
1074
+ (right, for every numeric type) instead of forcing left, so the padding lines the
1075
+ figures up against the currency column beside it.
1076
+
1077
+ Defaults are two decimals, grouping on, and `USD`/`en-US`. Override with
1078
+ `numberFormat.decimals` and `numberFormat.thousandsSeparator`, or with
1079
+ `currencyFormat.currency`, `currencyFormat.locale`, and
1080
+ `currencyFormat.currencyDisplay`. Setting `numberFormat.prefix` or
1081
+ `numberFormat.suffix` opts out of the symbol entirely — the caller is naming the
1082
+ unit — so `{ prefix: 'CR ' }` renders `(CR 12,840.50)`.
1083
+
929
1084
  ### Custom Cell and Editor Renderers
930
1085
 
931
1086
  Use `renderCell` and `renderEditor` when a built-in type is not enough.
@@ -1039,6 +1194,8 @@ Use `pagination-mode="client"` to page the local filtered display model.
1039
1194
 
1040
1195
  When grouping or tree data is active, a client page contains flattened visible display items, including structural rows, rather than exactly `pageSize` raw data rows.
1041
1196
 
1197
+ If the data behind the current page shrinks — a filter, a `data` reassignment, or a shorter re-delivery — the table re-clamps `currentPage` to the new last page before slicing, exactly as `goToPage()` would. The body therefore always shows the rows the "Showing a–b of n" summary claims, with no page interaction required. An empty result set is the one exception: the declared page is left alone so a table rendered before its rows arrive keeps the starting page the host asked for.
1198
+
1042
1199
  ### Virtualization and Lazy Loading
1043
1200
 
1044
1201
  Use `virtualize` on a fixed-height table and append data in response to `lazy-load`.
@@ -1071,6 +1228,8 @@ Use `virtualize` on a fixed-height table and append data in response to `lazy-lo
1071
1228
 
1072
1229
  Guard the handler because continued scrolling can emit more than once.
1073
1230
 
1231
+ A virtualized body renders the same zero-row states as an ordinary one: the loading spinner while a remote request is in flight, the `⚠️` row for a failed load, and the `empty-state` slot (or the default "No data" placeholder) when the delivery is empty. `scrollToRow()` recomputes the window as part of the call, so the requested row is rendered immediately instead of waiting for the next scroll or delivery.
1232
+
1074
1233
  ### Master-Detail Rows
1075
1234
 
1076
1235
  Use `setDetailPanel()` to add an expand control and render detail content below a row.
@@ -1090,7 +1249,7 @@ String detail content is parsed as HTML; use trusted strings or return an `HTMLE
1090
1249
 
1091
1250
  ### Tree Data
1092
1251
 
1093
- Call `setTreeData()` before assigning `data` reactively. Each row's path identifies its place in the hierarchy; missing ancestors become generated gap nodes.
1252
+ Call `setTreeData()` before assigning `data` reactively. Each row's path identifies its place in the hierarchy; missing ancestors become generated gap nodes, at every level a path skips — a row at `['Eng', 'Team', 'Alice']` delivered on its own renders `Eng` and `Team` as gap nodes above it.
1094
1253
 
1095
1254
  ```javascript
1096
1255
  const table = document.querySelector('#org-tree');
@@ -1115,7 +1274,9 @@ table.data = [
1115
1274
  ];
1116
1275
  ```
1117
1276
 
1118
- `defaultExpansionDepth: 0` starts collapsed; `1` expands root nodes; `Infinity` expands all levels. Tree node keys are slash-joined paths, such as `Engineering/Alice`.
1277
+ `defaultExpansionDepth: 0` starts collapsed; `1` expands root nodes; `Infinity` expands all levels. It describes the *starting* expansion only: it is applied once, when the first rows arrive, so collapsing every node with `collapseTreeNode()` or `collapseAllTreeNodes()` — sticks across later renders and re-deliveries instead of springing back to the default depth.
1278
+
1279
+ The group column is a normal cell: `renderCell` owns it outright, and otherwise `formatter` (then `valueFormatter` as the fallback) produces its label, rendered next to the indent and chevron. Tree rows also compose with master-detail — with `setDetailPanel()` configured, expanding a tree row renders its panel below the row. Generated gap nodes carry no data row, so they show only the label their path implies and have no detail panel.
1119
1280
 
1120
1281
  ### Column Groups and Layout Controls
1121
1282
 
@@ -1136,6 +1297,81 @@ table.moveColumn('department', 1);
1136
1297
 
1137
1298
  Enable `column-resize`, `column-reorder`, and `column-menu` for equivalent pointer controls. Pinned headers stay at their physical edges and are not draggable.
1138
1299
 
1300
+ #### How a column gets its width
1301
+
1302
+ A column that declares no `width` **shares the frame**. On every header render,
1303
+ and whenever the frame is resized, the space left over after the fixed tool
1304
+ columns and the explicitly sized columns is split evenly between them. They
1305
+ therefore grow into a wide frame and shrink into a narrow one — a table inside a
1306
+ half-width card fits its card instead of running past the edge.
1307
+
1308
+ The floor is the column's `minWidth` (50px by default). Once the frame is too
1309
+ narrow even for the minimums, the columns stop shrinking and `.table-frame`
1310
+ scrolls horizontally; nothing is ever collapsed away.
1311
+
1312
+ Four things count as an **authored** width and are honoured verbatim, never
1313
+ refitted: `width` on the column definition, a drag-resize on the header edge,
1314
+ `autoSizeColumn()`, and `autoSizeAllColumns()`. Give a column `width` when its
1315
+ content has a natural size (an ID, a status chip); leave it off when the column
1316
+ should absorb whatever room is going.
1317
+
1318
+ #### `column-fit`: scroll or squish
1319
+
1320
+ The paragraphs above describe `column-fit="scroll"`, the default: `minWidth` is
1321
+ inviolable and the frame gives way. `column-fit="squish"` inverts the priority —
1322
+ the frame is inviolable and `minWidth` gives way:
1323
+
1324
+ ```html
1325
+ <!-- Default: columns keep their minimums, the frame scrolls sideways. -->
1326
+ <snice-table column-fit="scroll"></snice-table>
1327
+
1328
+ <!-- Every column shares the frame; the table never scrolls horizontally. -->
1329
+ <snice-table column-fit="squish"></snice-table>
1330
+ ```
1331
+
1332
+ ```javascript
1333
+ table.columnFit = 'squish'; // reactive; re-fits the painted columns
1334
+ ```
1335
+
1336
+ In squish mode:
1337
+
1338
+ - **Every visible column shares the available frame width.** `minWidth` is
1339
+ relaxed down to a 24px legibility floor, so eight columns still fit a
1340
+ half-width card.
1341
+ - **Cell content and header labels ellipsise** instead of wrapping the row
1342
+ taller or spilling past the column boundary. This covers the typed cells too
1343
+ (`email`, `status`, `link`, `phone`, `location`, `json`, `color`, `image`,
1344
+ `actions`), whose 100px default width floor squish relaxes through the
1345
+ `--snice-table-cell-min-width` custom property — without that they refuse to shrink
1346
+ and the column clips them mid-glyph instead of ellipsising.
1347
+ - **The table never scrolls horizontally.** The rightmost column edge sits on
1348
+ the frame's inner edge at any width, and `.table-frame` sets
1349
+ `overflow-x: hidden` so a sub-pixel rounding error can never produce a
1350
+ scrollbar. Vertical scrolling is unaffected.
1351
+ - **Resizing still works and rebalances.** A drag is honoured, but the columns
1352
+ beside the dragged one give up (or take back) the difference so the total
1353
+ stays inside the frame — capped so no single column can squeeze the others
1354
+ below the floor.
1355
+ - **Declared widths are still honoured** while there is an unsized column left
1356
+ to absorb the difference. When the authored widths alone overflow the frame,
1357
+ every column is scaled down by the same factor instead: squish has no
1358
+ scrollbar to fall back on.
1359
+
1360
+ Choose `squish` for a table that has to live in a card, a dashboard tile, or a
1361
+ split pane, where a horizontal scrollbar reads as a layout bug. Choose the
1362
+ default `scroll` for a wide dataset the reader is expected to explore sideways —
1363
+ pinned columns, in particular, only pay off when something scrolls past them.
1364
+
1365
+ #### Pinned column affordance
1366
+
1367
+ A pinned cell renders with `pinned-cell` plus `pinned-cell--left` or
1368
+ `pinned-cell--right`. The inner-most pinned column on each side — the boundary
1369
+ between the frozen region and the scrolling one — also gets `pinned-cell--edge`,
1370
+ which paints a 2px divider and a soft shadow cast into the scrolling area. Pinned
1371
+ headers additionally carry a `.pin-indicator` pin glyph and say "pinned left" /
1372
+ "pinned right" in their accessible name, so a frozen column is still legible when
1373
+ the table happens to be wide enough that nothing scrolls.
1374
+
1139
1375
  ### Row Reordering and Pinning
1140
1376
 
1141
1377
  Use `row-reorder` for local drag-and-drop, and pin independent summary rows with the row-pinning methods.
@@ -1163,6 +1399,18 @@ template, bind it with `.listRenderer=${fn}`. The imperative
1163
1399
  `setListViewRenderer()` equivalent remains available. Tool cells such as
1164
1400
  selection and detail toggles remain available.
1165
1401
 
1402
+ An active `listRenderer` also **suppresses the columnar header**: the group
1403
+ header row, the per-column `<th>` cells, and the header-filter row are all
1404
+ dropped, because a card row has no column geometry for them to line up with.
1405
+ The tool-column headers stay, so `selectable` keeps its select-all checkbox
1406
+ above the cards. Clearing the renderer (`listRenderer = null`) or leaving list
1407
+ mode (`list = false`) brings the full header back. `list` on its own — with no
1408
+ renderer — still paints normal columnar cells and keeps its header.
1409
+
1410
+ Because there is no header to click, sorting in list mode runs through the
1411
+ toolbar (`setToolbar({ showSort: true })`) or the `toggleSort()` / `currentSort`
1412
+ API; the rows re-order exactly as they do in table mode.
1413
+
1166
1414
  ```javascript
1167
1415
  table.list = true;
1168
1416
  table.listRenderer = (row) => {
@@ -1182,6 +1430,13 @@ Use `loading` for progress and the `empty-state` slot for a custom zero-row mess
1182
1430
  </snice-table>
1183
1431
  ```
1184
1432
 
1433
+ While `loading` with no rows, the body shows a centred spinner **and the word
1434
+ "Loading…"**, wrapped in a `role="status" aria-live="polite"` block so the state
1435
+ is announced rather than merely drawn. With rows already on screen the body dims
1436
+ and the `loading-overlay` part appears instead, carrying the same announcement
1437
+ as an `aria-label`. Both spinners raise `--progress-track-opacity` so their track
1438
+ ring stays visible on dark surfaces.
1439
+
1185
1440
  The slotted empty content is **cloned** into the table body on each zero-row render — the light-DOM original is only a template, so event listeners or state on the slotted node do not carry over, and updating the slotted copy only takes effect on the next zero-row render. Remove `loading` after the request completes so the empty state can appear.
1186
1441
 
1187
1442
  ### Remote Data with Request/Response Events
@@ -1222,6 +1477,8 @@ Use `mode="remote"` with server pagination. The request payload contains `search
1222
1477
 
1223
1478
  Only the newest overlapping request may update the table. A rejected latest request emits `table-load-error` and renders its error message.
1224
1479
 
1480
+ A control change that re-requests (sort, filter, page size, or page in server mode) supersedes any in-flight request **at the moment the change is made**, not 150 ms later when the debounce fires. A response that lands inside that window is therefore discarded rather than painted — no flash of rows belonging to the previous sort under a header that already shows the new one — and a superseded request that fails stays silent, exactly like any other stale response: only the newest request reports `table-load-error`.
1481
+
1225
1482
  ### Remote Data with a Controller
1226
1483
 
1227
1484
  Set `mode="remote"` when a controller provides `table/config` and `table/data`; local-mode controller attachment intentionally does not fetch.
@@ -1345,7 +1602,9 @@ table.printTable({ pageStyles: '@page { size: landscape; }' });
1345
1602
 
1346
1603
  CSV output uses raw row values and skips columns with `exportable: false`.
1347
1604
  Clipboard output uses formatted values by default and copies every filtered row
1348
- when selection is empty. With a selection, both helpers intersect the correct
1605
+ when selection is empty. Both read the column's working value, so a
1606
+ `valueGetter` column exports what the table displays rather than a row field it
1607
+ may not even have. With a selection, both helpers intersect the correct
1349
1608
  raw row identities with the filtered view.
1350
1609
 
1351
1610
  ## Limits Worth Knowing
@@ -1355,7 +1614,7 @@ contract before adopting the table:
1355
1614
 
1356
1615
  - **Pagination labels.** The built-in `pagination` renders "Showing 1–25 of 60" with plain prev/next-style buttons; `snice-pagination` renders numbered `aria-label="Page N"` buttons. Neither produces a "Page X of Y" contract.
1357
1616
  - **Remote-mode debounce.** `mode="remote"` requests are debounced by a hard-coded 150 ms.
1358
- - **Failed remote loads keep their rows.** With data present, a failed re-request leaves the previous rows on screen; the `⚠️` warning row and the `empty-state` slot only appear when there is no data at all.
1617
+ - **Failed remote loads keep their rows.** With data present, a failed re-request leaves the previous rows on screen; the `⚠️` warning row and the `empty-state` slot only appear when there is no data at all. The error state (the `⚠️` row and the `table--error` host class) describes the last delivery *attempt*, so it clears as soon as any dataset arrives — a successful re-request, `setData()`, or a plain `table.data = rows` assignment when the page falls back to local data. Assigning an empty array clears it too, and the body then shows the empty state rather than the stale error. Re-assigning the identical array reference counts as an arrival and clears the error too — `data` re-renders on every assignment, not only on a new array. One limit remains: a request that was already in flight when the local rows arrived still reports its own failure when it settles — the error re-appears until the next assignment. Reordering rows by drag republishes the same rows and deliberately leaves the error alone.
1359
1618
  - **Remote re-request triggers.** Remote mode re-requests only on `currentPage`, `currentSort`, and `pageSize` changes. A page that owns its own filter set must reset one of those (e.g. `currentPage`) to drive a refetch.
1360
1619
  - **Local mode is client-side.** `sortable`/`searchable` in local mode sort and filter the rows already in `data` — on a server-paged list that is one page of results.
1361
1620
 
@@ -51,6 +51,11 @@ Provides all design tokens for Snice components via CSS custom properties. Inclu
51
51
  | `--snice-color-overlay-hover` | Alpha tint layered over a surface on hover |
52
52
  | `--snice-color-overlay-selected` | Primary-tinted overlay for selected rows/items |
53
53
  | `--snice-color-overlay-selected-hover` | Selected state when also hovered |
54
+ | `--snice-color-overlay-stripe` | Zebra tint for alternating rows (`hsl(0 0% 0% / 0.02)` light, `hsl(0 0% 100% / 0.11)` dark) |
55
+
56
+ Dark surfaces need a far larger step than light ones: the tint that reads as a
57
+ stripe on white is invisible on near-black, which is why the stripe token is an
58
+ alpha overlay tuned per theme rather than a step on the surface scale.
54
59
 
55
60
  ### Motion
56
61
 
package/docs/events.md CHANGED
@@ -908,6 +908,55 @@ Manual dispatch remains valid when code needs direct access to the Event
908
908
  object, a dynamic event name, the cancellation boolean returned by
909
909
  `dispatchEvent()`, or a target other than the Snice host.
910
910
 
911
+ ### Cancelable Events (Suppressible Default Actions)
912
+
913
+ `cancelable` is an `EventInit` field, so `@dispatch('my-event', { cancelable:
914
+ true })` really does produce a cancelable event and a listener really can call
915
+ `preventDefault()` on it.
916
+
917
+ What the decorator cannot give you is the answer. `@dispatch` returns the
918
+ method's own return value — the event `detail` — not the boolean
919
+ `dispatchEvent()` produces, so the component has no way to find out that a
920
+ listener cancelled the event. A component whose event carries a **default
921
+ action it must be able to suppress** therefore dispatches by hand:
922
+
923
+ ```typescript
924
+ @element('day-chip')
925
+ class DayChip extends HTMLElement {
926
+ // Returns false when a listener called preventDefault().
927
+ private emitMoreClick(date: Date, count: number): boolean {
928
+ return this.dispatchEvent(new CustomEvent('more-click', {
929
+ detail: { date, count },
930
+ bubbles: true,
931
+ composed: true,
932
+ cancelable: true
933
+ }));
934
+ }
935
+
936
+ private handleChipClick(date: Date, count: number) {
937
+ // The application gets first refusal on the day.
938
+ if (!this.emitMoreClick(date, count)) return;
939
+ this.openBuiltInPanel(date);
940
+ }
941
+ }
942
+ ```
943
+
944
+ An application then chooses between the two behaviors with one line:
945
+
946
+ ```typescript
947
+ // Keep the built-in behavior, just observe it
948
+ chip.addEventListener('more-click', (e) => log(e.detail));
949
+
950
+ // Replace the built-in behavior entirely
951
+ chip.addEventListener('more-click', (e) => {
952
+ e.preventDefault();
953
+ openMyOwnDayView(e.detail.date);
954
+ });
955
+ ```
956
+
957
+ Components in the library that use this pattern: `snice-link`'s `navigate`,
958
+ `snice-stepper`'s `step-change`, and `snice-calendar`'s `calendar-more-click`.
959
+
911
960
  ### Manual Escape Hatch
912
961
 
913
962
  ```typescript
@@ -14,8 +14,8 @@
14
14
  - Date-picker types: `components/date-picker/snice-date-picker.types.ts` (type patterns)
15
15
  - Date-picker tests: `tests/components/date-picker.test.ts` (test patterns)
16
16
  - Test utils: `tests/components/test-utils.ts` (`createComponent`, `queryShadow`, `wait`)
17
- - Coding standards: `.ai/coding-standards.md` (decorator patterns, CSS token rules)
18
- - Component checklist: `.ai/component-checklist.md` (completion criteria)
17
+ - Coding standards: `.ai/style.md` (decorator patterns, CSS token rules)
18
+ - Component checklist: `.ai/workflow.md` (completion criteria)
19
19
  - Design doc: `docs/plans/2026-03-06-date-range-picker-design.md`
20
20
 
21
21
  ---
@@ -157,7 +157,7 @@ Base calendar styles on `components/date-picker/snice-date-picker.css`. Key addi
157
157
  - `.presets` — preset button list
158
158
  - `.calendar-body` — flex container for presets + calendar grid(s)
159
159
 
160
- All CSS must use `var(--snice-*, fallback)` pattern. Spacing in rem, borders in px. See `.ai/coding-standards.md` for full token reference.
160
+ All CSS must use `var(--snice-*, fallback)` pattern. Spacing in rem, borders in px. See `.ai/style.md` for full token reference.
161
161
 
162
162
  ```css
163
163
  /* Date Range Picker Component Styles */
@@ -987,7 +987,7 @@ If anything failed, fix and commit.
987
987
 
988
988
  **Step 1: Write human docs**
989
989
 
990
- Follow the format in `.ai/component-docs-guide.md`. Section order: Title, Description, Basic Usage, Importing, Examples (basic, formats, sizes, variants, dual column, presets, constraints, states), Slots, Properties, Events, Methods, CSS Custom Properties.
990
+ Follow the format in `.ai/style.md`. Section order: Title, Description, Basic Usage, Importing, Examples (basic, formats, sizes, variants, dual column, presets, constraints, states), Slots, Properties, Events, Methods, CSS Custom Properties.
991
991
 
992
992
  **Step 2: Write AI docs**
993
993
 
@@ -159,7 +159,7 @@ Features: 7, 8, 9, 10, 12, 17, 21, 26, 33, 36, 37, 38
159
159
 
160
160
  ## Component Checklist
161
161
 
162
- Each phase must satisfy `.ai/component-checklist.md` before moving on. Key items:
162
+ Each phase must satisfy `.ai/workflow.md` before moving on. Key items:
163
163
 
164
164
  - Types in `snice-table.types.ts`
165
165
  - CSS with `var(--snice-*, fallback)`, rem for spacing, px for borders
@@ -10,7 +10,7 @@
10
10
 
11
11
  **Design doc:** `docs/plans/2026-03-09-action-bar-design.md`
12
12
 
13
- **Component checklist:** `.ai/component-checklist.md` — verify ALL items before marking complete.
13
+ **Component checklist:** `.ai/workflow.md` — verify ALL items before marking complete.
14
14
 
15
15
  ## REQUIRED READING — Complete ALL before writing any code
16
16
 
@@ -22,13 +22,13 @@
22
22
  3. `docs/ai/DEVELOPMENT.md` — build system, testing, component file structure, adding components checklist, CDN builds, React adapters, test generation, component template
23
23
 
24
24
  ### Internal coding standards (read third — these are mandatory)
25
- 4. `.ai/coding-standards.md` — REQUIRED decorator patterns (@query, @on, @dispatch, @ready, @dispose, @observe, @watch, @request/@respond), CSS theme integration with fallbacks, units (rem vs px), two-tier variable pattern, container component dual API, dos and don'ts
26
- 5. `.ai/component-checklist.md` — every item must be satisfied before component is complete
27
- 6. `.ai/component-docs-guide.md` — exact doc format for both human and AI docs, section order, writing rules
28
- 7. `.ai/notes.md` — project organization (customer-facing vs internal), file naming, camera/timer rules, scroll spy notes
29
- 8. `.ai/playwright-testing.md` — E2E testing rules, .debug/ only for temp files, always headless
25
+ 4. `.ai/style.md` — REQUIRED decorator patterns (@query, @on, @dispatch, @ready, @dispose, @observe, @watch, @request/@respond), CSS theme integration with fallbacks, units (rem vs px), two-tier variable pattern, container component dual API, dos and don'ts
26
+ 5. `.ai/workflow.md` — every item must be satisfied before component is complete
27
+ 6. `.ai/style.md` — exact doc format for both human and AI docs, section order, writing rules
28
+ 7. `.ai/gotchas.md` — project organization (customer-facing vs internal), file naming, camera/timer rules, scroll spy notes
29
+ 8. `.ai/testing.md` — E2E testing rules, .debug/ only for temp files, always headless
30
30
  9. `.ai/tasks.md` — has pending tasks list AND required snice decorator patterns reference at the bottom
31
- 10. `.ai/release-checklist.md` — release process, when to manually update llms.txt
31
+ 10. `.ai/pr-workflow.md` — release process, when to manually update llms.txt
32
32
 
33
33
  ### Design spec (read fourth)
34
34
  11. `docs/plans/2026-03-09-action-bar-design.md` — the design spec for this component
@@ -110,7 +110,7 @@ git commit -m "feat(action-bar): add type definitions"
110
110
 
111
111
  **Files:**
112
112
  - Create: `components/action-bar/snice-action-bar.css`
113
- - Reference: `.ai/coding-standards.md` for theme token patterns, units, fallbacks
113
+ - Reference: `.ai/style.md` for theme token patterns, units, fallbacks
114
114
 
115
115
  **Step 1: Read theme.css for available tokens**
116
116
 
@@ -511,11 +511,11 @@ git commit -m "feat(action-bar): add full showcase"
511
511
  **Files:**
512
512
  - Create: `docs/components/action-bar.md` (human-friendly)
513
513
  - Create: `docs/ai/components/action-bar.md` (token-efficient)
514
- - Reference: `.ai/component-docs-guide.md` for exact format
514
+ - Reference: `.ai/style.md` for exact format
515
515
 
516
516
  **Step 1: Write human docs**
517
517
 
518
- Follow the section order from component-docs-guide.md:
518
+ Follow the section order from `.ai/style.md`:
519
519
  1. Title + tag name
520
520
  2. One-sentence description
521
521
  3. Basic usage
@@ -646,7 +646,7 @@ Expected: ALL PASS
646
646
 
647
647
  **Step 6: Verify against component checklist**
648
648
 
649
- Run through every item in `.ai/component-checklist.md`:
649
+ Run through every item in `.ai/workflow.md`:
650
650
  - [ ] Pre-implementation items
651
651
  - [ ] Implementation items
652
652
  - [ ] Testing items
@@ -41,7 +41,7 @@ select-all), ~4 s full renderBody @10k, ~6.6 s per sort click (render-bound),
41
41
  snice-cell-actions.css:40 uses var(--snice-color-text, rgb(23 23 23 / .05))
42
42
  — theme token as background, tint only as unreachable fallback.
43
43
  6. **row-clicked not composed.** snice-table.ts:2191-2193 raw dispatchEvent
44
- without bubbles/composed, violating .ai/coding-standards.md; invisible
44
+ without bubbles/composed, violating .ai/style.md; invisible
45
45
  outside shadow boundaries.
46
46
  7. **Error state unwired.** Failed remote load only console.errors
47
47
  (snice-table.ts:252-254) then shows generic "No data". A designed