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
@@ -41,11 +41,24 @@ class TableVirtualizer {
41
41
  window.removeEventListener('resize', this.scrollHandler);
42
42
  cancelAnimationFrame(this.rafId);
43
43
  }
44
- setTotalRows(total) {
44
+ /**
45
+ * Publish a new row count. `repaint` false updates the model only — the
46
+ * pending re-render is still armed (lastStartIndex is reset), so the caller's
47
+ * own paint (scrollToIndex / refresh) performs it. Callers that resolve a
48
+ * display index and then scroll MUST use that form: painting here runs
49
+ * `afterRender`, and a host whose afterRender restores keyboard focus would
50
+ * re-enter this method from inside its own update.
51
+ */
52
+ setTotalRows(total, repaint = true) {
45
53
  this.options.totalRows = total;
46
54
  this.invalidateOffsets();
47
55
  this.lastStartIndex = -1; // Force re-render
48
- this.update();
56
+ if (repaint)
57
+ this.update();
58
+ }
59
+ /** Row count the virtualizer is currently windowing over. */
60
+ getTotalRows() {
61
+ return this.options.totalRows ?? 0;
49
62
  }
50
63
  /**
51
64
  * Force a full re-render of the visible range.
@@ -56,11 +69,30 @@ class TableVirtualizer {
56
69
  this.lastEndIndex = -1;
57
70
  this.update();
58
71
  }
72
+ /**
73
+ * The element that scrolls RIGHT NOW. A structural re-render of the host
74
+ * swaps the node captured at `attach()`; keep the listener and every lookup
75
+ * (notably the `<tbody>` we paint into) on the live one.
76
+ */
77
+ resolveContainer() {
78
+ const live = this.options.resolveScrollContainer?.() ?? null;
79
+ const current = this.options.scrollContainer;
80
+ if (live && live !== current) {
81
+ current?.removeEventListener('scroll', this.scrollHandler);
82
+ live.addEventListener('scroll', this.scrollHandler, { passive: true });
83
+ this.options.scrollContainer = live;
84
+ // The new container's tbody is empty — the range we believed painted is
85
+ // not on screen anymore.
86
+ this.lastStartIndex = -1;
87
+ this.lastEndIndex = -1;
88
+ }
89
+ return this.options.scrollContainer ?? null;
90
+ }
59
91
  /**
60
92
  * Calculate visible range and render if changed.
61
93
  */
62
94
  update() {
63
- if (!this.enabled || !this.options.scrollContainer)
95
+ if (!this.enabled || !this.resolveContainer())
64
96
  return;
65
97
  const { bufferPx, totalRows, scrollContainer, renderRange } = this.options;
66
98
  const scrollTop = scrollContainer.scrollTop;
@@ -74,11 +106,12 @@ class TableVirtualizer {
74
106
  // Filtering, pagination, or collapsing a group can shrink the flattened
75
107
  // model while the container is scrolled near the old end. Clamp before
76
108
  // deriving indices so startIndex never sits beyond totalRows and blanks the
77
- // body. Reflect the clamp to the element as well so its next scroll event
78
- // observes the same state.
109
+ // body. Only an offset that overruns the whole content is written back to
110
+ // the element — a browser already clamps its own scrollTop, and rewriting a
111
+ // deliberate offset (scrollToRow) would undo the caller's request.
79
112
  const maxScrollTop = Math.max(0, totalHeight - viewportHeight);
80
113
  const effectiveScrollTop = Math.min(Math.max(0, scrollTop), maxScrollTop);
81
- if (scrollTop !== effectiveScrollTop)
114
+ if (scrollTop > totalHeight || scrollTop < 0)
82
115
  scrollContainer.scrollTop = effectiveScrollTop;
83
116
  // Calculate visible range with buffer
84
117
  const startPx = Math.max(0, effectiveScrollTop - bufferPx);
@@ -88,75 +121,89 @@ class TableVirtualizer {
88
121
  // Skip if range hasn't changed
89
122
  if (startIndex === this.lastStartIndex && endIndex === this.lastEndIndex)
90
123
  return;
91
- this.lastStartIndex = startIndex;
92
- this.lastEndIndex = endIndex;
124
+ // Nothing to paint into yet (the host template has not committed its table,
125
+ // or a structural branch flip is mid-flight). Return WITHOUT recording the
126
+ // range: a dropped paint that looks like a completed one makes the guard
127
+ // above suppress every retry, and then only a viewport-height change (a
128
+ // window resize) ever repaints the rows.
129
+ const tbody = scrollContainer.querySelector('tbody');
130
+ if (!tbody)
131
+ return;
93
132
  // Update spacers
94
133
  const topHeight = offsets[startIndex] || 0;
95
134
  const bottomHeight = Math.max(0, totalHeight - (offsets[endIndex] || totalHeight));
96
135
  this.topSpacer.style.height = `${topHeight}px`;
97
136
  this.bottomSpacer.style.height = `${bottomHeight}px`;
137
+ // Span the columns that exist, never more (see resolveColumnSpan).
138
+ const columnSpan = Math.max(1, Math.round(this.options.resolveColumnSpan?.() ?? 1));
98
139
  // Render the visible range
99
140
  const fragment = renderRange(startIndex, endIndex);
100
141
  // Build the tbody content
101
- const tbody = this.options.scrollContainer.querySelector('tbody');
102
- if (tbody) {
103
- tbody.innerHTML = '';
104
- // Pinned-top rows render outside (above) the windowed range so they are
105
- // always present. Rebuilt every window recompute since tbody is wiped.
106
- const pinnedTop = this.options.renderPinnedTop?.();
107
- if (pinnedTop)
108
- tbody.appendChild(pinnedTop);
109
- if (topHeight > 0) {
110
- const topTd = document.createElement('td');
111
- topTd.colSpan = 999;
112
- topTd.style.height = `${topHeight}px`;
113
- topTd.style.padding = '0';
114
- topTd.style.border = 'none';
115
- this.topSpacer.innerHTML = '';
116
- this.topSpacer.appendChild(topTd);
117
- tbody.appendChild(this.topSpacer);
118
- }
119
- tbody.appendChild(fragment);
120
- if (bottomHeight > 0) {
121
- const bottomTd = document.createElement('td');
122
- bottomTd.colSpan = 999;
123
- bottomTd.style.height = `${bottomHeight}px`;
124
- bottomTd.style.padding = '0';
125
- bottomTd.style.border = 'none';
126
- this.bottomSpacer.innerHTML = '';
127
- this.bottomSpacer.appendChild(bottomTd);
128
- tbody.appendChild(this.bottomSpacer);
129
- }
130
- // Pinned-bottom rows render outside (below) the windowed range.
131
- const pinnedBottom = this.options.renderPinnedBottom?.();
132
- if (pinnedBottom)
133
- tbody.appendChild(pinnedBottom);
134
- this.options.afterRender?.();
142
+ tbody.innerHTML = '';
143
+ // Pinned-top rows render outside (above) the windowed range so they are
144
+ // always present. Rebuilt every window recompute since tbody is wiped.
145
+ const pinnedTop = this.options.renderPinnedTop?.();
146
+ if (pinnedTop)
147
+ tbody.appendChild(pinnedTop);
148
+ if (topHeight > 0) {
149
+ const topTd = document.createElement('td');
150
+ topTd.colSpan = columnSpan;
151
+ topTd.style.height = `${topHeight}px`;
152
+ topTd.style.padding = '0';
153
+ topTd.style.border = 'none';
154
+ this.topSpacer.innerHTML = '';
155
+ this.topSpacer.appendChild(topTd);
156
+ tbody.appendChild(this.topSpacer);
157
+ }
158
+ tbody.appendChild(fragment);
159
+ // Nothing to window over: hand the body back its empty/loading/error row
160
+ // instead of leaving the user staring at a blank frame.
161
+ if (totalRows === 0) {
162
+ const emptyBody = this.options.renderEmptyBody?.();
163
+ if (emptyBody)
164
+ tbody.appendChild(emptyBody);
135
165
  }
166
+ if (bottomHeight > 0) {
167
+ const bottomTd = document.createElement('td');
168
+ bottomTd.colSpan = columnSpan;
169
+ bottomTd.style.height = `${bottomHeight}px`;
170
+ bottomTd.style.padding = '0';
171
+ bottomTd.style.border = 'none';
172
+ this.bottomSpacer.innerHTML = '';
173
+ this.bottomSpacer.appendChild(bottomTd);
174
+ tbody.appendChild(this.bottomSpacer);
175
+ }
176
+ // Pinned-bottom rows render outside (below) the windowed range.
177
+ const pinnedBottom = this.options.renderPinnedBottom?.();
178
+ if (pinnedBottom)
179
+ tbody.appendChild(pinnedBottom);
180
+ // The window is on screen — only NOW does this range count as painted.
181
+ // Committed before afterRender so a re-entrant update() from that hook
182
+ // (keyboard focus restoration scrolls) sees the range it is looking at.
183
+ this.lastStartIndex = startIndex;
184
+ this.lastEndIndex = endIndex;
185
+ this.options.afterRender?.();
136
186
  }
137
187
  /** Get the currently visible row index range */
138
188
  getVisibleRange() {
139
189
  return { start: this.lastStartIndex, end: this.lastEndIndex };
140
190
  }
141
- /** Scroll to bring a specific row into view */
142
- scrollToRow(index) {
143
- if (!this.options.scrollContainer)
144
- return;
145
- const top = this.offsetForIndex(index);
146
- this.options.scrollContainer.scrollTop = top;
147
- }
148
191
  /**
149
- * Scroll a logical row index into view AND synchronously render its range.
150
- * Unlike `scrollToRow`, this does not rely on a scroll event (a programmatic
151
- * scrollTop assignment does not fire one) it forces the window recompute
152
- * immediately so the row is in the DOM by the time the caller focuses it.
192
+ * Scroll a row into view AND synchronously render the window around it.
193
+ * A programmatic `scrollTop` assignment fires no scroll event, so the window
194
+ * has to be recomputed hereotherwise the viewport lands on bare spacer and
195
+ * the requested row is never in the DOM.
153
196
  */
154
- scrollToIndex(index) {
197
+ scrollToRow(index) {
155
198
  if (!this.options.scrollContainer)
156
199
  return;
157
200
  this.options.scrollContainer.scrollTop = this.offsetForIndex(index);
158
201
  this.refresh();
159
202
  }
203
+ /** Scroll to a logical (flattened display) row index. */
204
+ scrollToIndex(index) {
205
+ this.scrollToRow(index);
206
+ }
160
207
  /** Get current scroll position */
161
208
  getScrollPosition() {
162
209
  const el = this.options.scrollContainer;
@@ -1 +1 @@
1
- {"version":3,"file":"table-virtualizer.js","sources":["../../../../components/table/table-virtualizer.ts"],"sourcesContent":["/**\n * Row virtualization engine for snice-table.\n * Only renders visible rows + configurable buffer.\n * Uses spacer elements to maintain scroll height.\n */\n\nexport interface VirtualizerOptions {\n /** Row height in pixels, or a per-logical-row height resolver. */\n rowHeight: number | ((index: number) => number);\n /** Buffer size in pixels above and below viewport */\n bufferPx: number;\n /** Total number of rows in the dataset */\n totalRows: number;\n /** Container element that scrolls */\n scrollContainer: HTMLElement;\n /** Callback to render a range of rows */\n renderRange: (startIndex: number, endIndex: number) => DocumentFragment;\n /**\n * Optional: render pinned-top rows. These live OUTSIDE the windowed range\n * (before the top spacer) so they are always present regardless of scroll\n * position — the same guarantee the non-virtual path provides.\n */\n renderPinnedTop?: () => DocumentFragment;\n /** Optional: render pinned-bottom rows (after the bottom spacer). */\n renderPinnedBottom?: () => DocumentFragment;\n /** Called after the new window has been inserted into tbody. */\n afterRender?: () => void;\n}\n\nexport class TableVirtualizer {\n private options: VirtualizerOptions;\n private topSpacer: HTMLElement;\n private bottomSpacer: HTMLElement;\n private contentContainer: HTMLElement;\n private scrollHandler: () => void;\n private rafId = 0;\n private lastStartIndex = -1;\n private lastEndIndex = -1;\n private enabled = false;\n private offsets: number[] = [];\n private offsetsTotalRows = -1;\n\n constructor() {\n this.topSpacer = document.createElement('tr');\n this.topSpacer.className = 'virtual-spacer virtual-spacer--top';\n this.topSpacer.style.display = 'table-row';\n\n this.bottomSpacer = document.createElement('tr');\n this.bottomSpacer.className = 'virtual-spacer virtual-spacer--bottom';\n this.bottomSpacer.style.display = 'table-row';\n\n this.contentContainer = document.createElement('tbody');\n this.options = {} as VirtualizerOptions;\n\n this.scrollHandler = () => {\n if (this.rafId) cancelAnimationFrame(this.rafId);\n this.rafId = requestAnimationFrame(() => this.update());\n };\n }\n\n attach(options: VirtualizerOptions) {\n this.options = options;\n this.enabled = true;\n this.lastStartIndex = -1;\n this.lastEndIndex = -1;\n this.invalidateOffsets();\n this.options.scrollContainer.addEventListener('scroll', this.scrollHandler, { passive: true });\n window.addEventListener('resize', this.scrollHandler, { passive: true });\n this.update();\n }\n\n detach() {\n this.enabled = false;\n this.options.scrollContainer?.removeEventListener('scroll', this.scrollHandler);\n window.removeEventListener('resize', this.scrollHandler);\n cancelAnimationFrame(this.rafId);\n }\n\n setTotalRows(total: number) {\n this.options.totalRows = total;\n this.invalidateOffsets();\n this.lastStartIndex = -1; // Force re-render\n this.update();\n }\n\n /**\n * Force a full re-render of the visible range.\n */\n refresh() {\n this.invalidateOffsets();\n this.lastStartIndex = -1;\n this.lastEndIndex = -1;\n this.update();\n }\n\n /**\n * Calculate visible range and render if changed.\n */\n private update() {\n if (!this.enabled || !this.options.scrollContainer) return;\n\n const { bufferPx, totalRows, scrollContainer, renderRange } = this.options;\n const scrollTop = scrollContainer.scrollTop;\n const offsets = this.buildOffsets(totalRows);\n const minimumViewport = totalRows > 0 ? Math.max(1, offsets[1] - offsets[0]) : 1;\n // Layoutless DOMs report a zero-height viewport; treating one row as the\n // minimum keeps range math valid and mirrors the smallest useful browser\n // viewport.\n const viewportHeight = Math.max(minimumViewport, scrollContainer.clientHeight);\n\n const totalHeight = offsets[totalRows] || 0;\n\n // Filtering, pagination, or collapsing a group can shrink the flattened\n // model while the container is scrolled near the old end. Clamp before\n // deriving indices so startIndex never sits beyond totalRows and blanks the\n // body. Reflect the clamp to the element as well so its next scroll event\n // observes the same state.\n const maxScrollTop = Math.max(0, totalHeight - viewportHeight);\n const effectiveScrollTop = Math.min(Math.max(0, scrollTop), maxScrollTop);\n if (scrollTop !== effectiveScrollTop) scrollContainer.scrollTop = effectiveScrollTop;\n\n // Calculate visible range with buffer\n const startPx = Math.max(0, effectiveScrollTop - bufferPx);\n const endPx = Math.min(totalHeight, effectiveScrollTop + viewportHeight + bufferPx);\n\n const startIndex = this.findRowAtOffset(offsets, startPx);\n const endIndex = Math.min(totalRows, this.findFirstOffsetAtOrAfter(offsets, endPx));\n\n // Skip if range hasn't changed\n if (startIndex === this.lastStartIndex && endIndex === this.lastEndIndex) return;\n\n this.lastStartIndex = startIndex;\n this.lastEndIndex = endIndex;\n\n // Update spacers\n const topHeight = offsets[startIndex] || 0;\n const bottomHeight = Math.max(0, totalHeight - (offsets[endIndex] || totalHeight));\n\n this.topSpacer.style.height = `${topHeight}px`;\n this.bottomSpacer.style.height = `${bottomHeight}px`;\n\n // Render the visible range\n const fragment = renderRange(startIndex, endIndex);\n\n // Build the tbody content\n const tbody = this.options.scrollContainer.querySelector('tbody');\n if (tbody) {\n tbody.innerHTML = '';\n // Pinned-top rows render outside (above) the windowed range so they are\n // always present. Rebuilt every window recompute since tbody is wiped.\n const pinnedTop = this.options.renderPinnedTop?.();\n if (pinnedTop) tbody.appendChild(pinnedTop);\n if (topHeight > 0) {\n const topTd = document.createElement('td');\n topTd.colSpan = 999;\n topTd.style.height = `${topHeight}px`;\n topTd.style.padding = '0';\n topTd.style.border = 'none';\n this.topSpacer.innerHTML = '';\n this.topSpacer.appendChild(topTd);\n tbody.appendChild(this.topSpacer);\n }\n tbody.appendChild(fragment);\n if (bottomHeight > 0) {\n const bottomTd = document.createElement('td');\n bottomTd.colSpan = 999;\n bottomTd.style.height = `${bottomHeight}px`;\n bottomTd.style.padding = '0';\n bottomTd.style.border = 'none';\n this.bottomSpacer.innerHTML = '';\n this.bottomSpacer.appendChild(bottomTd);\n tbody.appendChild(this.bottomSpacer);\n }\n // Pinned-bottom rows render outside (below) the windowed range.\n const pinnedBottom = this.options.renderPinnedBottom?.();\n if (pinnedBottom) tbody.appendChild(pinnedBottom);\n this.options.afterRender?.();\n }\n }\n\n /** Get the currently visible row index range */\n getVisibleRange(): { start: number; end: number } {\n return { start: this.lastStartIndex, end: this.lastEndIndex };\n }\n\n /** Scroll to bring a specific row into view */\n scrollToRow(index: number) {\n if (!this.options.scrollContainer) return;\n const top = this.offsetForIndex(index);\n this.options.scrollContainer.scrollTop = top;\n }\n\n /**\n * Scroll a logical row index into view AND synchronously render its range.\n * Unlike `scrollToRow`, this does not rely on a scroll event (a programmatic\n * scrollTop assignment does not fire one) — it forces the window recompute\n * immediately so the row is in the DOM by the time the caller focuses it.\n */\n scrollToIndex(index: number) {\n if (!this.options.scrollContainer) return;\n this.options.scrollContainer.scrollTop = this.offsetForIndex(index);\n this.refresh();\n }\n\n /** Get current scroll position */\n getScrollPosition(): { top: number; left: number } {\n const el = this.options.scrollContainer;\n return { top: el?.scrollTop ?? 0, left: el?.scrollLeft ?? 0 };\n }\n\n isEnabled() {\n return this.enabled;\n }\n\n private heightForIndex(index: number): number {\n const configured = typeof this.options.rowHeight === 'function'\n ? this.options.rowHeight(index)\n : this.options.rowHeight;\n return Number.isFinite(configured) && configured > 0 ? configured : 1;\n }\n\n private buildOffsets(totalRows: number): number[] {\n if (this.offsetsTotalRows === totalRows && this.offsets.length === totalRows + 1) {\n return this.offsets;\n }\n this.offsets = new Array(totalRows + 1);\n this.offsets[0] = 0;\n for (let index = 0; index < totalRows; index++) {\n this.offsets[index + 1] = this.offsets[index] + this.heightForIndex(index);\n }\n this.offsetsTotalRows = totalRows;\n return this.offsets;\n }\n\n private invalidateOffsets() {\n this.offsets = [];\n this.offsetsTotalRows = -1;\n }\n\n private offsetForIndex(index: number): number {\n const clamped = Math.max(0, Math.min(index, this.options.totalRows));\n return this.buildOffsets(this.options.totalRows)[clamped] || 0;\n }\n\n /** Index of the logical row containing the offset. */\n private findRowAtOffset(offsets: number[], offset: number): number {\n const totalRows = offsets.length - 1;\n if (totalRows <= 0) return 0;\n let low = 0;\n let high = totalRows;\n while (low < high) {\n const mid = Math.floor((low + high) / 2);\n if (offsets[mid + 1] <= offset) low = mid + 1;\n else high = mid;\n }\n return Math.min(totalRows, low);\n }\n\n /** First prefix offset at or after the requested pixel. */\n private findFirstOffsetAtOrAfter(offsets: number[], offset: number): number {\n let low = 0;\n let high = offsets.length - 1;\n while (low < high) {\n const mid = Math.floor((low + high) / 2);\n if (offsets[mid] < offset) low = mid + 1;\n else high = mid;\n }\n return low;\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"table-virtualizer.js","sources":["../../../../components/table/table-virtualizer.ts"],"sourcesContent":["/**\n * Row virtualization engine for snice-table.\n * Only renders visible rows + configurable buffer.\n * Uses spacer elements to maintain scroll height.\n */\n\nexport interface VirtualizerOptions {\n /** Row height in pixels, or a per-logical-row height resolver. */\n rowHeight: number | ((index: number) => number);\n /** Buffer size in pixels above and below viewport */\n bufferPx: number;\n /** Total number of rows in the dataset */\n totalRows: number;\n /** Container element that scrolls */\n scrollContainer: HTMLElement;\n /**\n * Optional: re-resolve the scrolling container on every update. A host whose\n * template flips structural branches replaces the element captured at\n * `attach()`; without this the virtualizer keeps painting into the DETACHED\n * subtree's orphaned `<tbody>` while the live one stays empty.\n */\n resolveScrollContainer?: () => HTMLElement | null;\n /** Callback to render a range of rows */\n renderRange: (startIndex: number, endIndex: number) => DocumentFragment;\n /**\n * How many columns a spacer row spans. Required in practice: a spacer that\n * over-spans invents columns, and `table-layout: fixed` then hands the\n * frame's spare width to the phantom ones — the real columns stop short of\n * the right edge with a dead strip beside them.\n */\n resolveColumnSpan?: () => number;\n /**\n * Optional: render pinned-top rows. These live OUTSIDE the windowed range\n * (before the top spacer) so they are always present regardless of scroll\n * position — the same guarantee the non-virtual path provides.\n */\n renderPinnedTop?: () => DocumentFragment;\n /** Optional: render pinned-bottom rows (after the bottom spacer). */\n renderPinnedBottom?: () => DocumentFragment;\n /**\n * Optional: the single message row a zero-row body shows (loading, error, or\n * empty state). Called only when `totalRows` is 0, so the virtualized body\n * carries the same states the ordinary render path does instead of being\n * wiped to nothing.\n */\n renderEmptyBody?: () => Node | null;\n /** Called after the new window has been inserted into tbody. */\n afterRender?: () => void;\n}\n\nexport class TableVirtualizer {\n private options: VirtualizerOptions;\n private topSpacer: HTMLElement;\n private bottomSpacer: HTMLElement;\n private contentContainer: HTMLElement;\n private scrollHandler: () => void;\n private rafId = 0;\n private lastStartIndex = -1;\n private lastEndIndex = -1;\n private enabled = false;\n private offsets: number[] = [];\n private offsetsTotalRows = -1;\n\n constructor() {\n this.topSpacer = document.createElement('tr');\n this.topSpacer.className = 'virtual-spacer virtual-spacer--top';\n this.topSpacer.style.display = 'table-row';\n\n this.bottomSpacer = document.createElement('tr');\n this.bottomSpacer.className = 'virtual-spacer virtual-spacer--bottom';\n this.bottomSpacer.style.display = 'table-row';\n\n this.contentContainer = document.createElement('tbody');\n this.options = {} as VirtualizerOptions;\n\n this.scrollHandler = () => {\n if (this.rafId) cancelAnimationFrame(this.rafId);\n this.rafId = requestAnimationFrame(() => this.update());\n };\n }\n\n attach(options: VirtualizerOptions) {\n this.options = options;\n this.enabled = true;\n this.lastStartIndex = -1;\n this.lastEndIndex = -1;\n this.invalidateOffsets();\n this.options.scrollContainer.addEventListener('scroll', this.scrollHandler, { passive: true });\n window.addEventListener('resize', this.scrollHandler, { passive: true });\n this.update();\n }\n\n detach() {\n this.enabled = false;\n this.options.scrollContainer?.removeEventListener('scroll', this.scrollHandler);\n window.removeEventListener('resize', this.scrollHandler);\n cancelAnimationFrame(this.rafId);\n }\n\n /**\n * Publish a new row count. `repaint` false updates the model only — the\n * pending re-render is still armed (lastStartIndex is reset), so the caller's\n * own paint (scrollToIndex / refresh) performs it. Callers that resolve a\n * display index and then scroll MUST use that form: painting here runs\n * `afterRender`, and a host whose afterRender restores keyboard focus would\n * re-enter this method from inside its own update.\n */\n setTotalRows(total: number, repaint = true) {\n this.options.totalRows = total;\n this.invalidateOffsets();\n this.lastStartIndex = -1; // Force re-render\n if (repaint) this.update();\n }\n\n /** Row count the virtualizer is currently windowing over. */\n getTotalRows() {\n return this.options.totalRows ?? 0;\n }\n\n /**\n * Force a full re-render of the visible range.\n */\n refresh() {\n this.invalidateOffsets();\n this.lastStartIndex = -1;\n this.lastEndIndex = -1;\n this.update();\n }\n\n /**\n * The element that scrolls RIGHT NOW. A structural re-render of the host\n * swaps the node captured at `attach()`; keep the listener and every lookup\n * (notably the `<tbody>` we paint into) on the live one.\n */\n private resolveContainer(): HTMLElement | null {\n const live = this.options.resolveScrollContainer?.() ?? null;\n const current = this.options.scrollContainer;\n if (live && live !== current) {\n current?.removeEventListener('scroll', this.scrollHandler);\n live.addEventListener('scroll', this.scrollHandler, { passive: true });\n this.options.scrollContainer = live;\n // The new container's tbody is empty — the range we believed painted is\n // not on screen anymore.\n this.lastStartIndex = -1;\n this.lastEndIndex = -1;\n }\n return this.options.scrollContainer ?? null;\n }\n\n /**\n * Calculate visible range and render if changed.\n */\n private update() {\n if (!this.enabled || !this.resolveContainer()) return;\n\n const { bufferPx, totalRows, scrollContainer, renderRange } = this.options;\n const scrollTop = scrollContainer.scrollTop;\n const offsets = this.buildOffsets(totalRows);\n const minimumViewport = totalRows > 0 ? Math.max(1, offsets[1] - offsets[0]) : 1;\n // Layoutless DOMs report a zero-height viewport; treating one row as the\n // minimum keeps range math valid and mirrors the smallest useful browser\n // viewport.\n const viewportHeight = Math.max(minimumViewport, scrollContainer.clientHeight);\n\n const totalHeight = offsets[totalRows] || 0;\n\n // Filtering, pagination, or collapsing a group can shrink the flattened\n // model while the container is scrolled near the old end. Clamp before\n // deriving indices so startIndex never sits beyond totalRows and blanks the\n // body. Only an offset that overruns the whole content is written back to\n // the element — a browser already clamps its own scrollTop, and rewriting a\n // deliberate offset (scrollToRow) would undo the caller's request.\n const maxScrollTop = Math.max(0, totalHeight - viewportHeight);\n const effectiveScrollTop = Math.min(Math.max(0, scrollTop), maxScrollTop);\n if (scrollTop > totalHeight || scrollTop < 0) scrollContainer.scrollTop = effectiveScrollTop;\n\n // Calculate visible range with buffer\n const startPx = Math.max(0, effectiveScrollTop - bufferPx);\n const endPx = Math.min(totalHeight, effectiveScrollTop + viewportHeight + bufferPx);\n\n const startIndex = this.findRowAtOffset(offsets, startPx);\n const endIndex = Math.min(totalRows, this.findFirstOffsetAtOrAfter(offsets, endPx));\n\n // Skip if range hasn't changed\n if (startIndex === this.lastStartIndex && endIndex === this.lastEndIndex) return;\n\n // Nothing to paint into yet (the host template has not committed its table,\n // or a structural branch flip is mid-flight). Return WITHOUT recording the\n // range: a dropped paint that looks like a completed one makes the guard\n // above suppress every retry, and then only a viewport-height change (a\n // window resize) ever repaints the rows.\n const tbody = scrollContainer.querySelector('tbody');\n if (!tbody) return;\n\n // Update spacers\n const topHeight = offsets[startIndex] || 0;\n const bottomHeight = Math.max(0, totalHeight - (offsets[endIndex] || totalHeight));\n\n this.topSpacer.style.height = `${topHeight}px`;\n this.bottomSpacer.style.height = `${bottomHeight}px`;\n\n // Span the columns that exist, never more (see resolveColumnSpan).\n const columnSpan = Math.max(1, Math.round(this.options.resolveColumnSpan?.() ?? 1));\n\n // Render the visible range\n const fragment = renderRange(startIndex, endIndex);\n\n // Build the tbody content\n tbody.innerHTML = '';\n // Pinned-top rows render outside (above) the windowed range so they are\n // always present. Rebuilt every window recompute since tbody is wiped.\n const pinnedTop = this.options.renderPinnedTop?.();\n if (pinnedTop) tbody.appendChild(pinnedTop);\n if (topHeight > 0) {\n const topTd = document.createElement('td');\n topTd.colSpan = columnSpan;\n topTd.style.height = `${topHeight}px`;\n topTd.style.padding = '0';\n topTd.style.border = 'none';\n this.topSpacer.innerHTML = '';\n this.topSpacer.appendChild(topTd);\n tbody.appendChild(this.topSpacer);\n }\n tbody.appendChild(fragment);\n // Nothing to window over: hand the body back its empty/loading/error row\n // instead of leaving the user staring at a blank frame.\n if (totalRows === 0) {\n const emptyBody = this.options.renderEmptyBody?.();\n if (emptyBody) tbody.appendChild(emptyBody);\n }\n if (bottomHeight > 0) {\n const bottomTd = document.createElement('td');\n bottomTd.colSpan = columnSpan;\n bottomTd.style.height = `${bottomHeight}px`;\n bottomTd.style.padding = '0';\n bottomTd.style.border = 'none';\n this.bottomSpacer.innerHTML = '';\n this.bottomSpacer.appendChild(bottomTd);\n tbody.appendChild(this.bottomSpacer);\n }\n // Pinned-bottom rows render outside (below) the windowed range.\n const pinnedBottom = this.options.renderPinnedBottom?.();\n if (pinnedBottom) tbody.appendChild(pinnedBottom);\n\n // The window is on screen — only NOW does this range count as painted.\n // Committed before afterRender so a re-entrant update() from that hook\n // (keyboard focus restoration scrolls) sees the range it is looking at.\n this.lastStartIndex = startIndex;\n this.lastEndIndex = endIndex;\n this.options.afterRender?.();\n }\n\n /** Get the currently visible row index range */\n getVisibleRange(): { start: number; end: number } {\n return { start: this.lastStartIndex, end: this.lastEndIndex };\n }\n\n /**\n * Scroll a row into view AND synchronously render the window around it.\n * A programmatic `scrollTop` assignment fires no scroll event, so the window\n * has to be recomputed here — otherwise the viewport lands on bare spacer and\n * the requested row is never in the DOM.\n */\n scrollToRow(index: number) {\n if (!this.options.scrollContainer) return;\n this.options.scrollContainer.scrollTop = this.offsetForIndex(index);\n this.refresh();\n }\n\n /** Scroll to a logical (flattened display) row index. */\n scrollToIndex(index: number) {\n this.scrollToRow(index);\n }\n\n /** Get current scroll position */\n getScrollPosition(): { top: number; left: number } {\n const el = this.options.scrollContainer;\n return { top: el?.scrollTop ?? 0, left: el?.scrollLeft ?? 0 };\n }\n\n isEnabled() {\n return this.enabled;\n }\n\n private heightForIndex(index: number): number {\n const configured = typeof this.options.rowHeight === 'function'\n ? this.options.rowHeight(index)\n : this.options.rowHeight;\n return Number.isFinite(configured) && configured > 0 ? configured : 1;\n }\n\n private buildOffsets(totalRows: number): number[] {\n if (this.offsetsTotalRows === totalRows && this.offsets.length === totalRows + 1) {\n return this.offsets;\n }\n this.offsets = new Array(totalRows + 1);\n this.offsets[0] = 0;\n for (let index = 0; index < totalRows; index++) {\n this.offsets[index + 1] = this.offsets[index] + this.heightForIndex(index);\n }\n this.offsetsTotalRows = totalRows;\n return this.offsets;\n }\n\n private invalidateOffsets() {\n this.offsets = [];\n this.offsetsTotalRows = -1;\n }\n\n private offsetForIndex(index: number): number {\n const clamped = Math.max(0, Math.min(index, this.options.totalRows));\n return this.buildOffsets(this.options.totalRows)[clamped] || 0;\n }\n\n /** Index of the logical row containing the offset. */\n private findRowAtOffset(offsets: number[], offset: number): number {\n const totalRows = offsets.length - 1;\n if (totalRows <= 0) return 0;\n let low = 0;\n let high = totalRows;\n while (low < high) {\n const mid = Math.floor((low + high) / 2);\n if (offsets[mid + 1] <= offset) low = mid + 1;\n else high = mid;\n }\n return Math.min(totalRows, low);\n }\n\n /** First prefix offset at or after the requested pixel. */\n private findFirstOffsetAtOrAfter(offsets: number[], offset: number): number {\n let low = 0;\n let high = offsets.length - 1;\n while (low < high) {\n const mid = Math.floor((low + high) / 2);\n if (offsets[mid] < offset) low = mid + 1;\n else high = mid;\n }\n return low;\n }\n}\n"],"names":[],"mappings":"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"}
@@ -129,6 +129,11 @@
129
129
  --snice-color-overlay-hover: hsl(0 0% 0% / 0.04);
130
130
  --snice-color-overlay-selected: hsl(var(--snice-color-blue-500) / 0.08);
131
131
  --snice-color-overlay-selected-hover: hsl(var(--snice-color-blue-500) / 0.12);
132
+ /* Zebra tint for alternating rows. Not a surface step: a solid surface token
133
+ lands within a couple of luminance points of the body on ONE of the two
134
+ themes and the striping disappears there. An alpha tint is tuned per
135
+ theme, and composites over pinned/selected/tinted cells alike. */
136
+ --snice-color-overlay-stripe: hsl(0 0% 0% / 0.02);
132
137
 
133
138
  --snice-color-primary-subtle: hsl(var(--snice-color-blue-50));
134
139
  --snice-color-primary-subtle-hover: hsl(var(--snice-color-blue-100));
@@ -342,6 +347,9 @@
342
347
  --snice-color-overlay-hover: hsl(0 0% 100% / 0.06);
343
348
  --snice-color-overlay-selected: hsl(var(--snice-color-blue-400) / 0.16);
344
349
  --snice-color-overlay-selected-hover: hsl(var(--snice-color-blue-400) / 0.22);
350
+ /* Dark surfaces need a much larger step than light ones: the same tint
351
+ that reads as a stripe on white is invisible on near-black. */
352
+ --snice-color-overlay-stripe: hsl(0 0% 100% / 0.11);
345
353
 
346
354
  --snice-color-primary-subtle: hsl(var(--snice-color-blue-950));
347
355
  --snice-color-primary-subtle-hover: hsl(var(--snice-color-blue-900));
@@ -405,6 +413,7 @@
405
413
  --snice-color-border-focus: hsl(var(--snice-color-blue-400));
406
414
 
407
415
  --snice-color-surface-hover: hsl(var(--snice-color-gray-700));
416
+ --snice-color-overlay-stripe: hsl(0 0% 100% / 0.11);
408
417
  --snice-color-surface-active: hsl(var(--snice-color-gray-600));
409
418
  --snice-color-surface-disabled: hsl(var(--snice-color-gray-800));
410
419
  --snice-color-text-disabled: hsl(var(--snice-color-gray-600));
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.7.0
2
+ * snice v7.8.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
package/dist/index.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.7.0
2
+ * snice v7.8.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.7.0
2
+ * snice v7.8.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.7.0
2
+ * snice v7.8.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.7.0
2
+ * snice v7.8.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.7.0
2
+ * snice v7.8.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.7.0
2
+ * snice v7.8.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.7.0
2
+ * snice v7.8.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
package/dist/symbols.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.7.0
2
+ * snice v7.8.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.7.0
2
+ * snice v7.8.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.7.0
2
+ * snice v7.8.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.7.0
2
+ * snice v7.8.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.7.0
2
+ * snice v7.8.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.7.0
2
+ * snice v7.8.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -35,6 +35,8 @@ npm run generate:react-tests # Generate test files
35
35
  ```bash
36
36
  npm test # Required complete gate (source+built+browser+site)
37
37
  npm run test:source # Tests importing package sources
38
+ npm run test:matrix # Component feature-combination matrices (fuzz tier, opt-in)
39
+ npm run test:matrix:visual # Same matrices in a real browser (on demand, chromium)
38
40
  npm run test:distribution # Fresh build, then same tests against dist/
39
41
  npm run test:cdn # Fresh CDN build + artifact/runtime tests
40
42
  npm run test:react # Fresh adapters + React tests
@@ -52,6 +54,95 @@ npm run gauntlet # Blind local-model checker gauntlet
52
54
  `repeat.ts`, `snice-element.ts`, and `template.ts`; every aggregate metric must
53
55
  be strictly greater than 90%. Browser commands manage their own local servers.
54
56
 
57
+ ## Test Tiers
58
+
59
+ | Tier | Command | Cost | When |
60
+ |------|---------|------|------|
61
+ | Everyday | `vitest run` / `npm run test:source` | ~26s for `tests/components` | Every change |
62
+ | Matrix (fuzz) | `npm run test:matrix` | Minutes; all components | Any component rendering change; once inside `npm test` |
63
+ | Visual | Playwright (`tests/live`) | Slowest | On request |
64
+ | Visual matrix | `npm run test:matrix:visual` | Minutes; every component's specs in `tests/live/matrix/` | On demand only; never in the gate |
65
+
66
+ - Canonical layout: `tests/matrix/<component>/*.test.ts` (matrix),
67
+ `tests/matrix/<component>/smoke.test.ts` (default-loop slice),
68
+ `tests/matrix/*.ts` (shared oracles),
69
+ `tests/live/matrix/<component>/<component>-visual.spec.ts` (visual tier),
70
+ `tests/live/fixtures/<component>/` (fixtures).
71
+ - `vitest.config.ts` `test.exclude` drops `tests/matrix/**/!(smoke).test.ts`
72
+ from the default include, the same way `tests/live` is dropped. Plain
73
+ `vitest run` pays for the smoke slices only.
74
+ - `npm run test:matrix` = `vitest run --config vitest.matrix.config.ts`. That
75
+ config inherits `vitest.config.ts` and replaces `exclude` with the base list
76
+ minus the matrix pattern (mergeConfig concatenates arrays, so inheriting it
77
+ would keep the entry that hides the suites). It throws if the base config
78
+ stops excluding the pattern. Smoke slices run in both tiers — intended.
79
+ - Each `smoke.test.ts` stays IN the default include: one combo per feature
80
+ family, plus the marquee regressions. Budget a few seconds per component —
81
+ add new combos to the matrix, not to the smoke file.
82
+ - Helper modules under `tests/matrix/` (`matrix-utils.ts`, the per-slice
83
+ `*-support.ts`) stay importable from outside; the exclusion removes files
84
+ from test COLLECTION, not from module resolution.
85
+ - `npm test` runs the matrices as their own `matrix suite` stage, exactly
86
+ once, source-flavoured (see `tooling/testing/run-full-tests.js`).
87
+
88
+ ## True-Visual Matrix (on demand)
89
+
90
+ `tests/live/matrix/` — the real-browser twin of `tests/matrix/`.
91
+ happy-dom does no layout, so the DOM matrix owns VALUE truth only; this tier
92
+ owns VISUAL truth.
93
+
94
+ ```bash
95
+ npm run test:matrix:visual # chromium only (default)
96
+ npm run test:matrix:visual -- --all-engines # chromium + firefox + webkit
97
+ npm run test:matrix:visual -- --project=webkit # explicit engine wins
98
+ npm run test:matrix:visual -- --grep squish # all other flags pass through
99
+ ```
100
+
101
+ - ON DEMAND. Not in `npm test`; `tests/playwright.config.ts` sets
102
+ `testIgnore: ['live/matrix/**']` so `test:browser:framework` (targets all of
103
+ `tests/live`) cannot sweep it in. Own config: `tests/playwright.matrix.config.ts`.
104
+ - Runner `tooling/testing/run-visual-matrix-tests.js` manages vite on :5566 —
105
+ probes `127.0.0.1` AND `[::1]` (servers bind v6), reuses a listener, kills
106
+ only a server it started. It then requires the fixture URL to return 200
107
+ before starting Playwright: a listener is not a server, and a wedged :5566
108
+ (vite rebuilds components synchronously on source change) otherwise surfaces
109
+ as hundreds of `net::ERR_ABORTED` specs instead of one clear message.
110
+ - Layout: `tests/live/matrix/<component>/<component>-visual.spec.ts` (the table
111
+ alone splits into `matrix-{local,remote,marquee}.spec.ts`), each driving
112
+ `tests/live/fixtures/<component>/matrix.html` (fixtures, NOT showcases). A
113
+ fixture exposes `window.matrix.mount(combo)` and owns anything
114
+ carrying functions — column definitions, callbacks — which cannot cross the
115
+ Playwright boundary.
116
+ - Size the specs to the component: the table is the ceiling, not the template.
117
+ Its combos are the FULL product `{local,remote}` x 6 pipelines x 3 delivery
118
+ patterns x 2^5 `{virtualize,height,squish,striped,selectable}` = 1152,
119
+ generated by `generateCombos()` in `tests/live/matrix/matrix-harness.ts`; a
120
+ divider's are two dozen. `--all-engines` triples the wall time.
121
+
122
+ **Two layers per component** (the split is documented in the table harness
123
+ header; every component's specs follow it):
124
+
125
+ | Layer | Scope | What it asserts |
126
+ |-------|-------|-----------------|
127
+ | 1. geometry + computed style + `elementFromPoint` | every combo, one `page.evaluate` each on a shared open page | the component's own documented visual contract. Table: row/cell boxes disjoint + ascending + contained; squish right edge on the frame's inner edge and ellipsis not mid-glyph clip; sized host filled to bottom; virtual window contiguous between spacers; `striped` = 2 row colours, unstriped = 1; selection checkbox painted at non-zero size; three hit-test probes per visible cell must land inside that cell |
128
+ | 2. real screenshots | a small pinned marquee set (table: 6 combos x 2 colour schemes) | table: striping, loading spinner, empty state, squished right edge, fill row, density — decoded in-browser, judged on WCAG contrast between painted samples; PNGs written to `test-results/matrix-visual/` |
129
+
130
+ Layer 2 exists because layer 1 cannot tell a colour that DIFFERS from one that
131
+ differs VISIBLY (`striped` once applied correctly while painting two luminance
132
+ points from the dark surface). Keep every marquee set small — screenshots are
133
+ the expensive layer. New combinations go in the layer-1 generator.
134
+
135
+ **Known defects are pinned, never softened** (`.ai/fuzzing.md` policy). A
136
+ component's specs pin theirs with `test.fail()` and the finding written above
137
+ it — the Playwright counterpart of the DOM matrix's `it.fails`. The table
138
+ harness uses `WAIVERS` in `matrix-harness.ts`, stricter still because one combo reports many
139
+ problems: a waiver names the EXACT message it excuses, every other problem in
140
+ the same combo still fails, and a waiver whose message stops appearing fails
141
+ itself so it must be deleted. Currently pinned there:
142
+ `VISUAL-MATRIX-fill-1` (96 combos) — a local, sized, non-virtualized table that
143
+ renders its body a second time appends an inert filler even though the content
144
+ already overflows the frame.
145
+
55
146
  ## Dumb-Agent Gauntlet
56
147
 
57
148
  Internal checker hardening: `npm run gauntlet` uses the comprehensive multi-file
@@ -63,7 +154,7 @@ application sample. Focused probes remain available with
63
154
  - Auto-downloads pinned llama.cpp + size/SHA-256-pinned GGUFs into `.local/`.
64
155
  - Produces blind raw output plus checker, TypeScript, and Vite logs per model.
65
156
  - No token/time cap; exact-output repetition is preserved and classified.
66
- - Full workflow/classification rules: `.ai/gauntlet.md`.
157
+ - Full workflow/classification rules: `.ai/testing.md`.
67
158
 
68
159
  ## File Structure
69
160
 
@@ -140,7 +231,7 @@ All CDN builds use the shared runtime (external `snice` imports). Load `snice-ru
140
231
  - IIFE: `.js` + `.min.js`
141
232
  - Sourcemaps + README
142
233
 
143
- **Size:** Runtime ~34KB gzip, components ~1-111KB each
234
+ **Size:** Runtime ~34KB gzip, components ~1-118KB each
144
235
 
145
236
  **Features:**
146
237
  - Shared runtime (external snice imports)