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
@@ -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.
@@ -677,7 +677,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
677
677
  }
678
678
  }
679
679
 
680
- var cssContent$k = ":host([hidden]){display:none}:host{display:inline-block;width:100%;max-width:100%;box-sizing:border-box;font-family:var(--snice-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);color-scheme:light dark}.input-wrapper{position:relative;width:100%;max-width:100%;height:100%;box-sizing:border-box}:host([align]) .input-wrapper{display:flex;flex-direction:column}:host([align=top]) .input-wrapper{justify-content:flex-start}:host([align=center]) .input-wrapper{justify-content:center}:host([align=bottom]) .input-wrapper{justify-content:flex-end}.input-container{position:relative;display:flex;align-items:center;width:100%;max-width:100%;box-sizing:border-box;transition:all var(--snice-transition-fast, 150ms) ease}:host([stretch]) .input-wrapper{display:flex;flex-direction:column}:host([stretch]) .input-container{flex:1}:host([stretch]) .input{height:100%}.input{flex:1;min-width:0;box-sizing:border-box;font-family:inherit;font-size:var(--snice-font-size-md, 1rem);background:var(--snice-color-surface-container-lowest,rgb(248 247 245));color:var(--snice-color-text,rgb(23 23 23));border:1px solid var(--snice-color-border,rgb(226 226 226));border-radius:var(--snice-border-radius-md,.25rem);transition:border-color var(--snice-transition-fast, 150ms) var(--snice-ease-out, ease),box-shadow var(--snice-transition-fast, 150ms) var(--snice-ease-out, ease),background-color var(--snice-transition-fast, 150ms) var(--snice-ease-out, ease);outline:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}.input:focus-visible{border-color:var(--snice-color-primary,rgb(37 99 235));box-shadow:0 0 0 var(--snice-focus-ring-width,2px) var(--snice-focus-ring-color,rgb(59 130 246 / .5))}.input:hover:not(:focus):not(:disabled){border-color:var(--snice-color-border-hover,rgb(204 204 204))}.input:disabled{background:var(--snice-color-surface-container-low,rgb(250 250 250));color:var(--snice-color-text-secondary,rgb(82 82 82));cursor:not-allowed;opacity:.6}.input::placeholder{color:var(--snice-color-text-secondary,rgb(82 82 82));opacity:.7}.input--small{padding:.375rem .625rem;font-size:var(--snice-font-size-sm, .875rem);min-height:2rem}.input--medium{padding:.5rem .75rem;font-size:var(--snice-font-size-md, 1rem);min-height:2.5rem}.input--large{padding:.625rem .875rem;font-size:var(--snice-font-size-lg, 1.125rem);min-height:3rem}.input--outlined{background:var(--snice-color-surface-container-lowest,rgb(248 247 245));border:1px solid var(--snice-color-border,rgb(226 226 226))}.input--filled{background:var(--snice-color-surface-container-low,rgb(250 250 250));border:1px solid transparent;border-bottom-color:var(--snice-color-border,rgb(226 226 226));border-radius:var(--snice-border-radius-md,.25rem) var(--snice-border-radius-md,.25rem) 0 0}.input--filled:focus-visible{background:var(--snice-color-surface-hover,rgb(245 245 245));border-bottom-color:var(--snice-color-primary,rgb(37 99 235))}.input--underlined{background:0 0;border:none;border-bottom:1px solid var(--snice-color-border,rgb(226 226 226));border-radius:0;padding-left:0;padding-right:0;box-shadow:none}.input-container:has(.input--underlined){position:relative}.input-container:has(.input--underlined)::after{content:'';position:absolute;left:50%;right:50%;bottom:-1px;height:2px;background:var(--snice-color-primary,rgb(37 99 235));transition:left .2s var(--snice-ease-out, ease),right .2s var(--snice-ease-out, ease);pointer-events:none}.input-container:has(.input--underlined:focus-visible)::after{left:0;right:0}.input--underlined:focus-visible{border-bottom-color:var(--snice-color-border,rgb(226 226 226));box-shadow:none}.input--invalid{border-color:var(--snice-color-danger,rgb(220 38 38))}.input--invalid:focus-visible{border-color:var(--snice-color-danger,rgb(220 38 38));box-shadow:0 0 0 var(--snice-focus-ring-width,2px) var(--snice-color-danger-alpha,rgb(220 38 38 / .4)),var(--snice-shadow-sm,0 1px 2px 0 rgb(0 0 0 / .05))}.input::-webkit-calendar-picker-indicator{cursor:pointer;opacity:.7}.input::-webkit-calendar-picker-indicator:hover{opacity:1}.input--with-prefix-icon{padding-left:2.5rem}.input--clearable,.input--with-suffix-icon{padding-right:2.5rem}.input--with-suffix-icon.input--clearable{padding-right:4rem}.label{display:block;margin-bottom:.25rem;font-size:var(--snice-font-size-sm, .875rem);font-weight:var(--snice-font-weight-medium,500);line-height:var(--snice-line-height-normal, 1.5);color:var(--snice-color-text,rgb(23 23 23));text-align:left}:host([label-align=center]) .label{text-align:center}:host([label-align=right]) .label{text-align:right}.label--required::after{content:' *';color:var(--snice-color-danger,rgb(220 38 38))}.error-text,.helper-text{display:block;margin-top:.25rem;font-size:var(--snice-font-size-xs, .75rem)}.helper-text{color:var(--snice-color-text-secondary,rgb(82 82 82))}.error-text{color:var(--snice-color-danger,rgb(220 38 38))}.icon-slot{position:absolute;top:50%;transform:translateY(-50%);display:flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;color:var(--snice-color-text-secondary,rgb(82 82 82));pointer-events:none;opacity:.7}.icon-slot:empty{display:none}.icon-slot--prefix{left:.625rem}.icon-slot--suffix{right:.625rem}.icon{display:flex;align-items:center;justify-content:center;overflow:hidden;width:1.5rem;height:1.5rem;font-size:1.25rem;line-height:1;color:var(--snice-color-text-secondary,rgb(82 82 82));pointer-events:none}.icon-slot ::slotted(*){display:flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;font-size:1.25rem;line-height:1}.icon-slot ::slotted(svg){width:1.5rem;height:1.5rem}.icon-slot ::slotted(img){width:1.5rem;height:1.5rem;object-fit:contain}.clear-button{position:absolute;right:.5rem;display:flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;padding:.25rem;background:0 0;border:none;border-radius:var(--snice-border-radius-sm,.125rem);color:var(--snice-color-text-secondary,rgb(82 82 82));cursor:pointer;transition:all var(--snice-transition-fast, 150ms) ease;opacity:0;visibility:hidden}.clear-button:hover{background:var(--snice-color-surface-hover,rgb(245 245 245));color:var(--snice-color-text,rgb(23 23 23))}.input-container:hover .clear-button--visible,.input:focus-visible~.clear-button--visible{opacity:1;visibility:visible}.clear-button--with-suffix{right:2rem}.password-toggle{position:absolute;right:.5rem;display:flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;padding:.25rem;background:0 0;border:none;border-radius:var(--snice-border-radius-sm,.125rem);color:var(--snice-color-text-secondary,rgb(82 82 82));cursor:pointer;transition:all var(--snice-transition-fast, 150ms) ease}.password-toggle:hover{background:var(--snice-color-surface-hover,rgb(245 245 245));color:var(--snice-color-text,rgb(23 23 23))}.input[type=number]::-webkit-inner-spin-button,.input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.input[type=number]{-moz-appearance:textfield}.input[type=search]::-webkit-search-cancel-button,.input[type=search]::-webkit-search-decoration,.input[type=search]::-webkit-search-results-button,.input[type=search]::-webkit-search-results-decoration{-webkit-appearance:none}.input-container:focus-within{z-index:1}.input--loading{cursor:wait}.spinner{position:absolute;right:.75rem;width:1em;height:1em;pointer-events:none}.input--with-suffix-icon .spinner{right:2.5rem}.spinner::after{content:'';display:block;width:100%;height:100%;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:.6s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){*{transition-duration:0s!important;animation-duration:0s!important}}";
680
+ var cssContent$l = ":host([hidden]){display:none}:host{display:inline-block;width:100%;max-width:100%;box-sizing:border-box;font-family:var(--snice-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);color-scheme:light dark}.input-wrapper{position:relative;width:100%;max-width:100%;height:100%;box-sizing:border-box}:host([align]) .input-wrapper{display:flex;flex-direction:column}:host([align=top]) .input-wrapper{justify-content:flex-start}:host([align=center]) .input-wrapper{justify-content:center}:host([align=bottom]) .input-wrapper{justify-content:flex-end}.input-container{position:relative;display:flex;align-items:center;width:100%;max-width:100%;box-sizing:border-box;transition:all var(--snice-transition-fast, 150ms) ease}:host([stretch]) .input-wrapper{display:flex;flex-direction:column}:host([stretch]) .input-container{flex:1}:host([stretch]) .input{height:100%}.input{flex:1;min-width:0;box-sizing:border-box;font-family:inherit;font-size:var(--snice-font-size-md, 1rem);background:var(--snice-color-surface-container-lowest,rgb(248 247 245));color:var(--snice-color-text,rgb(23 23 23));border:1px solid var(--snice-color-border,rgb(226 226 226));border-radius:var(--snice-border-radius-md,.25rem);transition:border-color var(--snice-transition-fast, 150ms) var(--snice-ease-out, ease),box-shadow var(--snice-transition-fast, 150ms) var(--snice-ease-out, ease),background-color var(--snice-transition-fast, 150ms) var(--snice-ease-out, ease);outline:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}.input:focus-visible{border-color:var(--snice-color-primary,rgb(37 99 235));box-shadow:0 0 0 var(--snice-focus-ring-width,2px) var(--snice-focus-ring-color,rgb(59 130 246 / .5))}.input:hover:not(:focus):not(:disabled){border-color:var(--snice-color-border-hover,rgb(204 204 204))}.input:disabled{background:var(--snice-color-surface-container-low,rgb(250 250 250));color:var(--snice-color-text-secondary,rgb(82 82 82));cursor:not-allowed;opacity:.6}.input::placeholder{color:var(--snice-color-text-secondary,rgb(82 82 82));opacity:.7}.input--small{padding:.375rem .625rem;font-size:var(--snice-font-size-sm, .875rem);min-height:2rem}.input--medium{padding:.5rem .75rem;font-size:var(--snice-font-size-md, 1rem);min-height:2.5rem}.input--large{padding:.625rem .875rem;font-size:var(--snice-font-size-lg, 1.125rem);min-height:3rem}.input--outlined{background:var(--snice-color-surface-container-lowest,rgb(248 247 245));border:1px solid var(--snice-color-border,rgb(226 226 226))}.input--filled{background:var(--snice-color-surface-container-low,rgb(250 250 250));border:1px solid transparent;border-bottom-color:var(--snice-color-border,rgb(226 226 226));border-radius:var(--snice-border-radius-md,.25rem) var(--snice-border-radius-md,.25rem) 0 0}.input--filled:focus-visible{background:var(--snice-color-surface-hover,rgb(245 245 245));border-bottom-color:var(--snice-color-primary,rgb(37 99 235))}.input--underlined{background:0 0;border:none;border-bottom:1px solid var(--snice-color-border,rgb(226 226 226));border-radius:0;padding-left:0;padding-right:0;box-shadow:none}.input-container:has(.input--underlined){position:relative}.input-container:has(.input--underlined)::after{content:'';position:absolute;left:50%;right:50%;bottom:-1px;height:2px;background:var(--snice-color-primary,rgb(37 99 235));transition:left .2s var(--snice-ease-out, ease),right .2s var(--snice-ease-out, ease);pointer-events:none}.input-container:has(.input--underlined:focus-visible)::after{left:0;right:0}.input--underlined:focus-visible{border-bottom-color:var(--snice-color-border,rgb(226 226 226));box-shadow:none}.input--invalid{border-color:var(--snice-color-danger,rgb(220 38 38))}.input--invalid:focus-visible{border-color:var(--snice-color-danger,rgb(220 38 38));box-shadow:0 0 0 var(--snice-focus-ring-width,2px) var(--snice-color-danger-alpha,rgb(220 38 38 / .4)),var(--snice-shadow-sm,0 1px 2px 0 rgb(0 0 0 / .05))}.input::-webkit-calendar-picker-indicator{cursor:pointer;opacity:.7}.input::-webkit-calendar-picker-indicator:hover{opacity:1}.input--with-prefix-icon{padding-left:2.5rem}.input--clearable,.input--with-suffix-icon{padding-right:2.5rem}.input--with-suffix-icon.input--clearable{padding-right:4rem}.label{display:block;margin-bottom:.25rem;font-size:var(--snice-font-size-sm, .875rem);font-weight:var(--snice-font-weight-medium,500);line-height:var(--snice-line-height-normal, 1.5);color:var(--snice-color-text,rgb(23 23 23));text-align:left}:host([label-align=center]) .label{text-align:center}:host([label-align=right]) .label{text-align:right}.label--required::after{content:' *';color:var(--snice-color-danger,rgb(220 38 38))}.error-text,.helper-text{display:block;margin-top:.25rem;font-size:var(--snice-font-size-xs, .75rem)}.helper-text{color:var(--snice-color-text-secondary,rgb(82 82 82))}.error-text{color:var(--snice-color-danger,rgb(220 38 38))}.icon-slot{position:absolute;top:50%;transform:translateY(-50%);display:flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;color:var(--snice-color-text-secondary,rgb(82 82 82));pointer-events:none;opacity:.7}.icon-slot:empty{display:none}.icon-slot--prefix{left:.625rem}.icon-slot--suffix{right:.625rem}.icon{display:flex;align-items:center;justify-content:center;overflow:hidden;width:1.5rem;height:1.5rem;font-size:1.25rem;line-height:1;color:var(--snice-color-text-secondary,rgb(82 82 82));pointer-events:none}.icon-slot ::slotted(*){display:flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;font-size:1.25rem;line-height:1}.icon-slot ::slotted(svg){width:1.5rem;height:1.5rem}.icon-slot ::slotted(img){width:1.5rem;height:1.5rem;object-fit:contain}.clear-button{position:absolute;right:.5rem;display:flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;padding:.25rem;background:0 0;border:none;border-radius:var(--snice-border-radius-sm,.125rem);color:var(--snice-color-text-secondary,rgb(82 82 82));cursor:pointer;transition:all var(--snice-transition-fast, 150ms) ease;opacity:0;visibility:hidden}.clear-button:hover{background:var(--snice-color-surface-hover,rgb(245 245 245));color:var(--snice-color-text,rgb(23 23 23))}.input-container:hover .clear-button--visible,.input:focus-visible~.clear-button--visible{opacity:1;visibility:visible}.clear-button--with-suffix{right:2rem}.password-toggle{position:absolute;right:.5rem;display:flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;padding:.25rem;background:0 0;border:none;border-radius:var(--snice-border-radius-sm,.125rem);color:var(--snice-color-text-secondary,rgb(82 82 82));cursor:pointer;transition:all var(--snice-transition-fast, 150ms) ease}.password-toggle:hover{background:var(--snice-color-surface-hover,rgb(245 245 245));color:var(--snice-color-text,rgb(23 23 23))}.input[type=number]::-webkit-inner-spin-button,.input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.input[type=number]{-moz-appearance:textfield}.input[type=search]::-webkit-search-cancel-button,.input[type=search]::-webkit-search-decoration,.input[type=search]::-webkit-search-results-button,.input[type=search]::-webkit-search-results-decoration{-webkit-appearance:none}.input-container:focus-within{z-index:1}.input--loading{cursor:wait}.spinner{position:absolute;right:.75rem;width:1em;height:1em;pointer-events:none}.input--with-suffix-icon .spinner{right:2.5rem}.spinner::after{content:'';display:block;width:100%;height:100%;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:.6s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){*{transition-duration:0s!important;animation-duration:0s!important}}";
681
681
 
682
682
  (() => {
683
683
  let _classDecorators = [snice.element('snice-input', { formAssociated: true, delegatesFocus: true })];
@@ -1143,7 +1143,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
1143
1143
  `;
1144
1144
  }
1145
1145
  styles() {
1146
- return snice.css /*css*/ `${cssContent$k}`;
1146
+ return snice.css /*css*/ `${cssContent$l}`;
1147
1147
  }
1148
1148
  init() {
1149
1149
  // Preserve a value assigned before custom-element upgrade. The own data
@@ -1612,7 +1612,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
1612
1612
  return _classThis;
1613
1613
  })();
1614
1614
 
1615
- var cssContent$j = ":host([hidden]){display:none}:host{display:inline-block;font-family:var(--snice-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);position:relative;width:100%;max-width:18.75rem}[hidden]{display:none!important}img[src=\"\"]{display:none}.select-wrapper{position:relative;width:100%}.select-label{display:block;margin-bottom:.25rem;font-size:var(--snice-font-size-sm, .875rem);font-weight:var(--snice-font-weight-medium,500);line-height:var(--snice-line-height-normal, 1.5);color:var(--snice-color-text,rgb(23 23 23));text-align:left}.select-label--required::after{content:' *';color:var(--snice-color-danger,rgb(220 38 38))}.select-error-text,.select-helper-text{display:block;margin-top:var(--snice-spacing-2xs,.25rem);font-size:var(--snice-font-size-xs, .75rem);font-weight:var(--snice-font-weight-normal,400);line-height:var(--snice-line-height-normal, 1.5)}.select-helper-text{color:var(--snice-color-text-secondary,rgb(82 82 82))}.select-error-text{color:var(--snice-color-danger,rgb(220 38 38))}.select-trigger{box-sizing:border-box;min-width:0;width:100%;display:flex;align-items:center;justify-content:space-between;background:var(--snice-color-surface-container-lowest,rgb(248 247 245));border:1px solid var(--snice-color-border,rgb(226 226 226));border-radius:var(--snice-border-radius-md,.25rem);cursor:pointer;transition:all var(--snice-transition-fast, 150ms) ease;position:relative;text-align:left;font-family:inherit;color:var(--snice-color-text,rgb(23 23 23))}.select-trigger--small{padding:.375rem 2rem .375rem .625rem;font-size:var(--snice-font-size-sm, .875rem);min-height:2rem}.select-trigger--medium{padding:.5rem 2.25rem .5rem .75rem;font-size:var(--snice-font-size-md, 1rem);min-height:var(--snice-select-min-height,2.5rem)}.select-trigger--large{padding:.625rem 2.5rem .625rem .875rem;font-size:var(--snice-font-size-lg, 1.125rem);min-height:3rem}.select-trigger:hover:not(.select-trigger--disabled){border-color:var(--snice-color-border-hover,rgb(180 180 180))}.select-trigger:focus-visible{outline:2px solid var(--snice-color-primary,rgb(37 99 235));outline-offset:2px;border-color:var(--snice-color-primary,rgb(37 99 235))}.select-trigger--open{border-color:var(--snice-color-primary,rgb(37 99 235))}.select-trigger--disabled{background:var(--snice-color-surface-container-low,rgb(245 245 245));cursor:not-allowed;opacity:.6}.select-trigger--readonly{background:var(--snice-color-surface-container-low,rgb(245 245 245));cursor:default}.select-trigger--invalid{border-color:var(--snice-color-danger,rgb(220 38 38))}.select-placeholder{color:var(--snice-color-text-secondary,rgb(82 82 82))}.select-value{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select-value--single{display:flex;align-items:center;gap:.5rem}.select-value-icon{width:1rem;height:1rem;object-fit:contain;flex-shrink:0}.select-value--multiple{display:flex;gap:.25rem;flex-wrap:wrap}.select-tag{display:inline-flex;align-items:center;gap:.25rem;padding:.125rem .375rem;background:var(--snice-color-primary-subtle,rgb(219 234 254));color:var(--snice-color-primary,rgb(37 99 235));border-radius:var(--snice-border-radius-sm,.125rem);font-size:var(--snice-font-size-xs, .75rem)}.select-tag-icon{width:.75rem;height:.75rem;object-fit:contain;flex-shrink:0}.select-tag-remove{cursor:pointer;padding:0;background:0 0;border:none;color:inherit;font-size:.75rem;line-height:1}.select-tag-remove:hover{opacity:.7}.select-icons{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);display:flex;align-items:center;gap:.25rem;pointer-events:none;z-index:1}.select-clear{padding:.125rem;background:0 0;border:none;cursor:pointer;color:var(--snice-color-text-secondary,rgb(82 82 82));display:flex;align-items:center;justify-content:center;border-radius:var(--snice-border-radius-sm,.125rem);pointer-events:auto}.select-clear:hover{background:var(--snice-color-surface-container-low,rgb(245 245 245));color:var(--snice-color-text,rgb(23 23 23))}.select-arrow{transition:transform var(--snice-transition-fast, 150ms) ease;color:var(--snice-color-text-secondary,rgb(82 82 82));pointer-events:none}.select-arrow--open{transform:rotate(180deg)}.select-dropdown{position:absolute;top:100%;margin-top:.25rem;left:0;right:0;background:var(--snice-color-surface-container-high,rgb(252 251 249));border:1px solid var(--snice-color-border,rgb(226 226 226));border-radius:var(--snice-border-radius-md,.25rem);box-shadow:var(--snice-shadow-lg,0 10px 15px -3px rgb(0 0 0 / .1),0 4px 6px -4px rgb(0 0 0 / .1));z-index:1000;opacity:0;transform:translateY(-8px) scaleY(.95);transform-origin:top;pointer-events:none;transition:opacity var(--snice-transition-fast, 150ms) ease,transform var(--snice-transition-fast, 150ms) ease;display:flex;flex-direction:column;overflow:hidden}.select-dropdown--open{opacity:1;transform:translateY(0) scaleY(1);pointer-events:auto}.select-search{padding:.5rem;border-bottom:1px solid var(--snice-color-border,rgb(226 226 226));flex-shrink:0}.select-search-input{width:100%;padding:.375rem .625rem;background:var(--snice-color-surface-container-lowest,rgb(248 247 245));border:1px solid var(--snice-color-border,rgb(226 226 226));border-radius:var(--snice-border-radius-sm,.125rem);font-size:var(--snice-font-size-sm, .875rem);font-family:inherit;color:var(--snice-color-text,rgb(23 23 23));box-sizing:border-box}.select-search-input:focus-visible{outline:0;border-color:var(--snice-color-primary,rgb(37 99 235))}.select-options{max-height:12.5rem;overflow-y:auto;overflow-x:hidden;padding:.25rem;scrollbar-gutter:stable;scrollbar-width:thin;scrollbar-color:var(--snice-color-border,rgb(226 226 226)) var(--snice-color-surface-container-low,rgb(245 245 245))}.select-options::-webkit-scrollbar-track{background:var(--snice-color-surface-container-low,rgb(245 245 245));border-radius:var(--snice-border-radius-md,.25rem);margin:.25rem 0}.select-options::-webkit-scrollbar-thumb{background:var(--snice-color-border,rgb(226 226 226));border-radius:var(--snice-border-radius-md,.25rem);border:2px solid var(--snice-color-surface,rgb(255 255 255))}.select-option{padding:.5rem .75rem;cursor:pointer;border-radius:var(--snice-border-radius-sm,.125rem);transition:background var(--snice-transition-fast, 150ms) ease;display:flex;align-items:center;gap:.5rem}.select-option:hover:not(.select-option--disabled){background:var(--snice-color-surface-container-low,rgb(245 245 245))}.select-option--selected{background:var(--snice-color-primary-subtle,rgb(219 234 254));color:var(--snice-color-primary,rgb(37 99 235));font-weight:var(--snice-font-weight-medium,500)}.select-option--focused{background:var(--snice-color-surface-container-low,rgb(245 245 245));outline:2px solid var(--snice-color-primary,rgb(37 99 235));outline-offset:-2px}.select-option--disabled{opacity:.5;cursor:not-allowed}.select-option-icon{width:1rem;height:1rem;object-fit:contain;flex-shrink:0}.select-option-check{width:1rem;height:1rem;display:flex;align-items:center;justify-content:center;flex-shrink:0}.select-option-label{flex:1}.select-no-options{padding:1rem;text-align:center;color:var(--snice-color-text-secondary,rgb(82 82 82));font-size:var(--snice-font-size-sm, .875rem)}.select-native{position:absolute;opacity:0;pointer-events:none;width:0;height:0}.select-options::-webkit-scrollbar{width:6px}.select-options::-webkit-scrollbar-track{background:var(--snice-color-surface-container-low,rgb(245 245 245));border-radius:3px}.select-options::-webkit-scrollbar-thumb{background:var(--snice-color-border,rgb(226 226 226));border-radius:3px}.select-options::-webkit-scrollbar-thumb:hover{background:var(--snice-color-border-hover,rgb(180 180 180))}.select-trigger--loading{cursor:wait;opacity:.7}.select-spinner{display:inline-block;width:.875rem;height:.875rem;margin-right:.25rem}.select-spinner::after{content:'';display:block;width:100%;height:100%;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:.6s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}.select-freetext-input{flex:1;border:none;outline:0;background:0 0;font-family:inherit;font-size:inherit;color:var(--snice-color-text,rgb(23 23 23));padding:0;min-width:0;width:100%}.select-freetext-input::placeholder{color:var(--snice-color-text-secondary,rgb(82 82 82))}.select-editable-container{position:relative;display:flex;align-items:center;width:100%}.select-editable-input{width:100%;box-sizing:border-box;font-family:inherit;background:var(--snice-color-surface-container-lowest,rgb(248 247 245));color:var(--snice-color-text,rgb(23 23 23));border:1px solid var(--snice-color-border,rgb(226 226 226));border-radius:var(--snice-border-radius-md,.25rem);transition:all var(--snice-transition-fast, 150ms) ease;outline:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}.select-editable-input--small{padding:.375rem 2rem .375rem .625rem;font-size:var(--snice-font-size-sm, .875rem);min-height:2rem}.select-editable-input--medium{padding:.5rem 2.25rem .5rem .75rem;font-size:var(--snice-font-size-md, 1rem);min-height:2.5rem}.select-editable-input--large{padding:.625rem 2.5rem .625rem .875rem;font-size:var(--snice-font-size-lg, 1.125rem);min-height:3rem}.select-editable-input:hover:not(:disabled){border-color:var(--snice-color-border-hover,rgb(180 180 180))}.select-editable-input:focus-visible{border-color:var(--snice-color-primary,rgb(37 99 235));box-shadow:0 0 0 var(--snice-focus-ring-width,2px) var(--snice-focus-ring-color,rgb(59 130 246 / .5))}.select-editable-input--open{border-color:var(--snice-color-primary,rgb(37 99 235))}.select-editable-input--invalid{border-color:var(--snice-color-danger,rgb(220 38 38))}.select-editable-input:disabled{background:var(--snice-color-surface-container-low,rgb(245 245 245));color:var(--snice-color-text-secondary,rgb(82 82 82));cursor:not-allowed;opacity:.6}.select-editable-input:read-only{background:var(--snice-color-surface-container-low,rgb(245 245 245));cursor:default}.select-editable-input::placeholder{color:var(--snice-color-text-secondary,rgb(82 82 82));opacity:.7}.select-editable-arrow{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);display:flex;align-items:center;justify-content:center;color:var(--snice-color-text-secondary,rgb(82 82 82));pointer-events:none;transition:transform var(--snice-transition-fast, 150ms) ease}.select-editable-arrow--open{transform:translateY(-50%) rotate(180deg)}.select-loading-indicator{display:flex;align-items:center;justify-content:center;gap:.5rem;padding:.75rem 1rem;color:var(--snice-color-text-secondary,rgb(82 82 82));font-size:var(--snice-font-size-sm, .875rem)}.select-loading-spinner{display:inline-block;width:1rem;height:1rem;flex-shrink:0}.select-loading-spinner::after{content:'';display:block;width:100%;height:100%;border:2px solid var(--snice-color-border,rgb(226 226 226));border-top-color:var(--snice-color-primary,rgb(37 99 235));border-radius:50%;animation:.6s linear infinite spin}@media (prefers-reduced-motion:reduce){*{transition-duration:0s!important;animation-duration:0s!important}}";
1615
+ var cssContent$k = ":host([hidden]){display:none}:host{display:inline-block;font-family:var(--snice-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);position:relative;width:100%;max-width:18.75rem}[hidden]{display:none!important}img[src=\"\"]{display:none}.select-wrapper{position:relative;width:100%}.select-label{display:block;margin-bottom:.25rem;font-size:var(--snice-font-size-sm, .875rem);font-weight:var(--snice-font-weight-medium,500);line-height:var(--snice-line-height-normal, 1.5);color:var(--snice-color-text,rgb(23 23 23));text-align:left}.select-label--required::after{content:' *';color:var(--snice-color-danger,rgb(220 38 38))}.select-error-text,.select-helper-text{display:block;margin-top:var(--snice-spacing-2xs,.25rem);font-size:var(--snice-font-size-xs, .75rem);font-weight:var(--snice-font-weight-normal,400);line-height:var(--snice-line-height-normal, 1.5)}.select-helper-text{color:var(--snice-color-text-secondary,rgb(82 82 82))}.select-error-text{color:var(--snice-color-danger,rgb(220 38 38))}.select-trigger{box-sizing:border-box;min-width:0;width:100%;display:flex;align-items:center;justify-content:space-between;background:var(--snice-color-surface-container-lowest,rgb(248 247 245));border:1px solid var(--snice-color-border,rgb(226 226 226));border-radius:var(--snice-border-radius-md,.25rem);cursor:pointer;transition:all var(--snice-transition-fast, 150ms) ease;position:relative;text-align:left;font-family:inherit;color:var(--snice-color-text,rgb(23 23 23))}.select-trigger--small{padding:.375rem 2rem .375rem .625rem;font-size:var(--snice-font-size-sm, .875rem);min-height:2rem}.select-trigger--medium{padding:.5rem 2.25rem .5rem .75rem;font-size:var(--snice-font-size-md, 1rem);min-height:var(--snice-select-min-height,2.5rem)}.select-trigger--large{padding:.625rem 2.5rem .625rem .875rem;font-size:var(--snice-font-size-lg, 1.125rem);min-height:3rem}.select-trigger:hover:not(.select-trigger--disabled){border-color:var(--snice-color-border-hover,rgb(180 180 180))}.select-trigger:focus-visible{outline:2px solid var(--snice-color-primary,rgb(37 99 235));outline-offset:2px;border-color:var(--snice-color-primary,rgb(37 99 235))}.select-trigger--open{border-color:var(--snice-color-primary,rgb(37 99 235))}.select-trigger--disabled{background:var(--snice-color-surface-container-low,rgb(245 245 245));cursor:not-allowed;opacity:.6}.select-trigger--readonly{background:var(--snice-color-surface-container-low,rgb(245 245 245));cursor:default}.select-trigger--invalid{border-color:var(--snice-color-danger,rgb(220 38 38))}.select-placeholder{color:var(--snice-color-text-secondary,rgb(82 82 82))}.select-value{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select-value--single{display:flex;align-items:center;gap:.5rem}.select-value-icon{width:1rem;height:1rem;object-fit:contain;flex-shrink:0}.select-value--multiple{display:flex;gap:.25rem;flex-wrap:wrap}.select-tag{display:inline-flex;align-items:center;gap:.25rem;padding:.125rem .375rem;background:var(--snice-color-primary-subtle,rgb(219 234 254));color:var(--snice-color-primary,rgb(37 99 235));border-radius:var(--snice-border-radius-sm,.125rem);font-size:var(--snice-font-size-xs, .75rem)}.select-tag-icon{width:.75rem;height:.75rem;object-fit:contain;flex-shrink:0}.select-tag-remove{cursor:pointer;padding:0;background:0 0;border:none;color:inherit;font-size:.75rem;line-height:1}.select-tag-remove:hover{opacity:.7}.select-icons{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);display:flex;align-items:center;gap:.25rem;pointer-events:none;z-index:1}.select-clear{padding:.125rem;background:0 0;border:none;cursor:pointer;color:var(--snice-color-text-secondary,rgb(82 82 82));display:flex;align-items:center;justify-content:center;border-radius:var(--snice-border-radius-sm,.125rem);pointer-events:auto}.select-clear:hover{background:var(--snice-color-surface-container-low,rgb(245 245 245));color:var(--snice-color-text,rgb(23 23 23))}.select-arrow{transition:transform var(--snice-transition-fast, 150ms) ease;color:var(--snice-color-text-secondary,rgb(82 82 82));pointer-events:none}.select-arrow--open{transform:rotate(180deg)}.select-dropdown{position:absolute;top:100%;margin-top:.25rem;left:0;right:0;background:var(--snice-color-surface-container-high,rgb(252 251 249));border:1px solid var(--snice-color-border,rgb(226 226 226));border-radius:var(--snice-border-radius-md,.25rem);box-shadow:var(--snice-shadow-lg,0 10px 15px -3px rgb(0 0 0 / .1),0 4px 6px -4px rgb(0 0 0 / .1));z-index:1000;opacity:0;transform:translateY(-8px) scaleY(.95);transform-origin:top;pointer-events:none;transition:opacity var(--snice-transition-fast, 150ms) ease,transform var(--snice-transition-fast, 150ms) ease;display:flex;flex-direction:column;overflow:hidden}.select-dropdown--open{opacity:1;transform:translateY(0) scaleY(1);pointer-events:auto}.select-search{padding:.5rem;border-bottom:1px solid var(--snice-color-border,rgb(226 226 226));flex-shrink:0}.select-search-input{width:100%;padding:.375rem .625rem;background:var(--snice-color-surface-container-lowest,rgb(248 247 245));border:1px solid var(--snice-color-border,rgb(226 226 226));border-radius:var(--snice-border-radius-sm,.125rem);font-size:var(--snice-font-size-sm, .875rem);font-family:inherit;color:var(--snice-color-text,rgb(23 23 23));box-sizing:border-box}.select-search-input:focus-visible{outline:0;border-color:var(--snice-color-primary,rgb(37 99 235))}.select-options{max-height:12.5rem;overflow-y:auto;overflow-x:hidden;padding:.25rem;scrollbar-gutter:stable;scrollbar-width:thin;scrollbar-color:var(--snice-color-border,rgb(226 226 226)) var(--snice-color-surface-container-low,rgb(245 245 245))}.select-options::-webkit-scrollbar-track{background:var(--snice-color-surface-container-low,rgb(245 245 245));border-radius:var(--snice-border-radius-md,.25rem);margin:.25rem 0}.select-options::-webkit-scrollbar-thumb{background:var(--snice-color-border,rgb(226 226 226));border-radius:var(--snice-border-radius-md,.25rem);border:2px solid var(--snice-color-surface,rgb(255 255 255))}.select-option{padding:.5rem .75rem;cursor:pointer;border-radius:var(--snice-border-radius-sm,.125rem);transition:background var(--snice-transition-fast, 150ms) ease;display:flex;align-items:center;gap:.5rem}.select-option:hover:not(.select-option--disabled){background:var(--snice-color-surface-container-low,rgb(245 245 245))}.select-option--selected{background:var(--snice-color-primary-subtle,rgb(219 234 254));color:var(--snice-color-primary,rgb(37 99 235));font-weight:var(--snice-font-weight-medium,500)}.select-option--focused{background:var(--snice-color-surface-container-low,rgb(245 245 245));outline:2px solid var(--snice-color-primary,rgb(37 99 235));outline-offset:-2px}.select-option--disabled{opacity:.5;cursor:not-allowed}.select-option-icon{width:1rem;height:1rem;object-fit:contain;flex-shrink:0}.select-option-check{width:1rem;height:1rem;display:flex;align-items:center;justify-content:center;flex-shrink:0}.select-option-label{flex:1}.select-no-options{padding:1rem;text-align:center;color:var(--snice-color-text-secondary,rgb(82 82 82));font-size:var(--snice-font-size-sm, .875rem)}.select-native{position:absolute;opacity:0;pointer-events:none;width:0;height:0}.select-options::-webkit-scrollbar{width:6px}.select-options::-webkit-scrollbar-track{background:var(--snice-color-surface-container-low,rgb(245 245 245));border-radius:3px}.select-options::-webkit-scrollbar-thumb{background:var(--snice-color-border,rgb(226 226 226));border-radius:3px}.select-options::-webkit-scrollbar-thumb:hover{background:var(--snice-color-border-hover,rgb(180 180 180))}.select-trigger--loading{cursor:wait;opacity:.7}.select-spinner{display:inline-block;width:.875rem;height:.875rem;margin-right:.25rem}.select-spinner::after{content:'';display:block;width:100%;height:100%;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:.6s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}.select-freetext-input{flex:1;border:none;outline:0;background:0 0;font-family:inherit;font-size:inherit;color:var(--snice-color-text,rgb(23 23 23));padding:0;min-width:0;width:100%}.select-freetext-input::placeholder{color:var(--snice-color-text-secondary,rgb(82 82 82))}.select-editable-container{position:relative;display:flex;align-items:center;width:100%}.select-editable-input{width:100%;box-sizing:border-box;font-family:inherit;background:var(--snice-color-surface-container-lowest,rgb(248 247 245));color:var(--snice-color-text,rgb(23 23 23));border:1px solid var(--snice-color-border,rgb(226 226 226));border-radius:var(--snice-border-radius-md,.25rem);transition:all var(--snice-transition-fast, 150ms) ease;outline:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}.select-editable-input--small{padding:.375rem 2rem .375rem .625rem;font-size:var(--snice-font-size-sm, .875rem);min-height:2rem}.select-editable-input--medium{padding:.5rem 2.25rem .5rem .75rem;font-size:var(--snice-font-size-md, 1rem);min-height:2.5rem}.select-editable-input--large{padding:.625rem 2.5rem .625rem .875rem;font-size:var(--snice-font-size-lg, 1.125rem);min-height:3rem}.select-editable-input:hover:not(:disabled){border-color:var(--snice-color-border-hover,rgb(180 180 180))}.select-editable-input:focus-visible{border-color:var(--snice-color-primary,rgb(37 99 235));box-shadow:0 0 0 var(--snice-focus-ring-width,2px) var(--snice-focus-ring-color,rgb(59 130 246 / .5))}.select-editable-input--open{border-color:var(--snice-color-primary,rgb(37 99 235))}.select-editable-input--invalid{border-color:var(--snice-color-danger,rgb(220 38 38))}.select-editable-input:disabled{background:var(--snice-color-surface-container-low,rgb(245 245 245));color:var(--snice-color-text-secondary,rgb(82 82 82));cursor:not-allowed;opacity:.6}.select-editable-input:read-only{background:var(--snice-color-surface-container-low,rgb(245 245 245));cursor:default}.select-editable-input::placeholder{color:var(--snice-color-text-secondary,rgb(82 82 82));opacity:.7}.select-editable-arrow{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);display:flex;align-items:center;justify-content:center;color:var(--snice-color-text-secondary,rgb(82 82 82));pointer-events:none;transition:transform var(--snice-transition-fast, 150ms) ease}.select-editable-arrow--open{transform:translateY(-50%) rotate(180deg)}.select-loading-indicator{display:flex;align-items:center;justify-content:center;gap:.5rem;padding:.75rem 1rem;color:var(--snice-color-text-secondary,rgb(82 82 82));font-size:var(--snice-font-size-sm, .875rem)}.select-loading-spinner{display:inline-block;width:1rem;height:1rem;flex-shrink:0}.select-loading-spinner::after{content:'';display:block;width:100%;height:100%;border:2px solid var(--snice-color-border,rgb(226 226 226));border-top-color:var(--snice-color-primary,rgb(37 99 235));border-radius:50%;animation:.6s linear infinite spin}@media (prefers-reduced-motion:reduce){*{transition-duration:0s!important;animation-duration:0s!important}}";
1616
1616
 
1617
1617
  (() => {
1618
1618
  let _classDecorators = [snice.element('snice-select', { formAssociated: true, delegatesFocus: true })];
@@ -1955,14 +1955,11 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
1955
1955
  get interactionDisabled() {
1956
1956
  return this.disabled || this.formDisabled || this.loading;
1957
1957
  }
1958
- /** Merged options: programmatic `options` array + child `<snice-option>` elements */
1958
+ /** The documented option list: child `<snice-option>` elements win over the `options` array */
1959
1959
  get mergedOptions() {
1960
- // Child options take precedence if both provided (slot wins)
1961
- if (this.childOptions.length > 0 && this.options.length > 0) {
1962
- // Merge: children first, then programmatic options not already present
1963
- const childValues = new Set(this.childOptions.map(o => o.value));
1964
- return [...this.childOptions, ...this.options.filter(o => !childValues.has(o.value))];
1965
- }
1960
+ // "Children take precedence over `options` array" when children are
1961
+ // present they are the list, so a later `options` assignment cannot
1962
+ // append to or reorder what the author declared in markup.
1966
1963
  return this.childOptions.length > 0 ? this.childOptions : this.options;
1967
1964
  }
1968
1965
  /**
@@ -2173,7 +2170,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
2173
2170
  option.setAttribute('data-value', opt.value);
2174
2171
  option.setAttribute('role', 'option');
2175
2172
  option.setAttribute('aria-selected', String(isSelected));
2176
- option.setAttribute('aria-disabled', String(opt.disabled));
2173
+ option.setAttribute('aria-disabled', String(Boolean(opt.disabled)));
2177
2174
  option.setAttribute('part', 'option');
2178
2175
  const check = document.createElement('span');
2179
2176
  check.className = 'select-option-check';
@@ -2193,7 +2190,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
2193
2190
  return fragment;
2194
2191
  }
2195
2192
  styles() {
2196
- return snice.css `${cssContent$j}`;
2193
+ return snice.css `${cssContent$k}`;
2197
2194
  }
2198
2195
  init() {
2199
2196
  // Read options from child snice-option elements
@@ -3053,7 +3050,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
3053
3050
  return _classThis;
3054
3051
  })();
3055
3052
 
3056
- var cssContent$i = ":host([hidden]){display:none}:host{display:inline-block;position:relative;width:auto;cursor:pointer}:host([disabled]){cursor:not-allowed}:host([justify-text=start]) .button{justify-content:flex-start}:host([justify-text=end]) .button{justify-content:flex-end}:host([justify-text=center]) .button{justify-content:center}.button{display:inline-flex;align-items:center;justify-content:center;width:100%;border:1px solid;font-family:var(--snice-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);font-weight:var(--snice-font-weight-medium,500);text-decoration:none;user-select:none;white-space:nowrap;vertical-align:middle;padding:0;box-shadow:var(--snice-shadow-xs,0 1px 2px 0 rgb(0 0 0 / .03));transition:color var(--snice-transition-fast, 150ms) var(--snice-ease-out, cubic-bezier(.22,1,.36,1)),background-color var(--snice-transition-fast, 150ms) var(--snice-ease-out, cubic-bezier(.22,1,.36,1)),border-color var(--snice-transition-fast, 150ms) var(--snice-ease-out, cubic-bezier(.22,1,.36,1)),box-shadow var(--snice-transition-fast, 150ms) var(--snice-ease-out, cubic-bezier(.22,1,.36,1)),transform 180ms var(--snice-ease-spring, cubic-bezier(.34,1.56,.64,1));cursor:inherit;position:relative}.button::before{content:'';position:absolute;inset:0;border-radius:inherit;background:var(--snice-gradient-convex,none);pointer-events:none}.button:active:not(.button--disabled){transition:color 50ms var(--snice-ease-out, ease),background-color 50ms var(--snice-ease-out, ease),border-color 50ms var(--snice-ease-out, ease),box-shadow 50ms var(--snice-ease-out, ease),transform 80ms var(--snice-ease-out, ease);transform:translateY(0) scale(.97)}.button:focus-visible{outline:0;box-shadow:0 0 0 var(--snice-focus-ring-width,2px) var(--snice-focus-ring-color,rgb(59 130 246 / .5))}.button--disabled{opacity:.5;cursor:not-allowed;box-shadow:none}.button--small{font-size:var(--snice-font-size-sm, .875rem);height:2rem;padding:0 var(--snice-spacing-sm,.75rem);border-radius:var(--snice-border-radius-md,.25rem)}.button--medium{font-size:var(--snice-font-size-md, 1rem);height:2.5rem;padding:0 var(--snice-spacing-md,1rem);border-radius:var(--snice-border-radius-md,.25rem)}.button--large{font-size:var(--snice-font-size-lg, 1.125rem);height:3rem;padding:0 var(--snice-spacing-lg,1.5rem);border-radius:var(--snice-border-radius-md,.25rem)}.button--default{background-color:var(--snice-color-surface,rgb(255 255 255));color:var(--snice-color-text,rgb(23 23 23));border-color:var(--snice-color-border,rgb(226 226 226))}.button--default:hover:not(.button--disabled){background-color:var(--snice-color-surface-container-low,rgb(250 250 250));border-color:var(--snice-color-border-hover,rgb(204 204 204));box-shadow:var(--snice-shadow-sm,0 1px 2px 0 rgb(0 0 0 / .05));transform:translateY(-1px)}.button--default:active:not(.button--disabled){background-color:var(--snice-color-surface-container,rgb(248 248 248));border-color:var(--snice-color-border-hover,rgb(204 204 204));box-shadow:var(--snice-shadow-inset-sm,inset 0 1px 2px rgb(0 0 0 / .05));transform:translateY(0) scale(.97)}.button--primary{background-color:var(--snice-color-primary,rgb(37 99 235));color:var(--snice-color-text-inverse,rgb(250 250 250));border-color:var(--snice-color-primary,rgb(37 99 235))}.button--primary:hover:not(.button--disabled){background-color:var(--snice-color-primary-hover,rgb(29 78 216));border-color:var(--snice-color-primary-hover,rgb(29 78 216));box-shadow:var(--snice-shadow-sm,0 1px 2px 0 rgb(0 0 0 / .05));transform:translateY(-1px)}.button--primary:active:not(.button--disabled){background-color:color-mix(in srgb,var(--snice-color-primary,rgb(37 99 235)) 85%,#fff);border-color:color-mix(in srgb,var(--snice-color-primary,rgb(37 99 235)) 85%,#fff);box-shadow:var(--snice-shadow-inset-sm,inset 0 1px 2px rgb(0 0 0 / .05));transform:translateY(0) scale(.97)}.button--success{background-color:var(--snice-color-success,rgb(22 163 74));color:var(--snice-color-text-inverse,rgb(250 250 250));border-color:var(--snice-color-success,rgb(22 163 74))}.button--success:hover:not(.button--disabled){background-color:var(--snice-color-success-hover,rgb(21 128 61));border-color:var(--snice-color-success-hover,rgb(21 128 61));box-shadow:var(--snice-shadow-sm,0 1px 2px 0 rgb(0 0 0 / .05));transform:translateY(-1px)}.button--success:active:not(.button--disabled){background-color:color-mix(in srgb,var(--snice-color-success,rgb(22 163 74)) 85%,#fff);border-color:color-mix(in srgb,var(--snice-color-success,rgb(22 163 74)) 85%,#fff);box-shadow:var(--snice-shadow-inset-sm,inset 0 1px 2px rgb(0 0 0 / .05));transform:translateY(0) scale(.97)}.button--warning{background-color:var(--snice-color-warning,rgb(202 138 4));color:var(--button-warning-text,rgb(69 26 3));border-color:var(--snice-color-warning,rgb(202 138 4))}.button--warning:hover:not(.button--disabled){background-color:var(--snice-color-warning-hover,rgb(161 98 7));border-color:var(--snice-color-warning-hover,rgb(161 98 7));box-shadow:var(--snice-shadow-sm,0 1px 2px 0 rgb(0 0 0 / .05));transform:translateY(-1px)}.button--warning:active:not(.button--disabled){background-color:color-mix(in srgb,var(--snice-color-warning,rgb(202 138 4)) 85%,#fff);border-color:color-mix(in srgb,var(--snice-color-warning,rgb(202 138 4)) 85%,#fff);box-shadow:var(--snice-shadow-inset-sm,inset 0 1px 2px rgb(0 0 0 / .05));transform:translateY(0) scale(.97)}.button--danger{background-color:var(--snice-color-danger,rgb(220 38 38));color:var(--snice-color-text-inverse,rgb(250 250 250));border-color:var(--snice-color-danger,rgb(220 38 38))}.button--danger:hover:not(.button--disabled){background-color:var(--snice-color-danger-hover,rgb(185 28 28));border-color:var(--snice-color-danger-hover,rgb(185 28 28));box-shadow:var(--snice-shadow-sm,0 1px 2px 0 rgb(0 0 0 / .05));transform:translateY(-1px)}.button--danger:active:not(.button--disabled){background-color:color-mix(in srgb,var(--snice-color-danger,rgb(220 38 38)) 85%,#fff);border-color:color-mix(in srgb,var(--snice-color-danger,rgb(220 38 38)) 85%,#fff);box-shadow:var(--snice-shadow-inset-sm,inset 0 1px 2px rgb(0 0 0 / .05));transform:translateY(0) scale(.97)}.button--text{background-color:transparent;color:var(--snice-color-primary,rgb(37 99 235));border-color:transparent;box-shadow:none}.button--text::before{display:none}.button--text:hover:not(.button--disabled){background-color:var(--snice-color-primary-alpha,rgb(37 99 235 / .1));color:var(--snice-color-primary-hover,rgb(29 78 216));box-shadow:none;transform:none}.button--text:active:not(.button--disabled){background-color:var(--snice-color-primary-alpha-strong,rgb(37 99 235 / .15));box-shadow:none;transform:none}.button--outline{box-shadow:none}.button--outline::before{display:none}.button--outline.button--default{background-color:transparent;color:var(--snice-color-text,rgb(23 23 23))}.button--outline.button--primary{background-color:transparent;color:var(--snice-color-primary,rgb(37 99 235))}.button--outline.button--success{background-color:transparent;color:var(--snice-color-success,rgb(22 163 74))}.button--outline.button--warning{background-color:transparent;color:var(--snice-color-warning,rgb(202 138 4))}.button--outline.button--danger{background-color:transparent;color:var(--snice-color-danger,rgb(220 38 38))}.button--outline.button--default:hover:not(.button--disabled){background-color:var(--snice-color-surface-hover,rgb(245 245 245));box-shadow:none;transform:none}.button--outline.button--primary:hover:not(.button--disabled){background-color:var(--snice-color-primary-subtle,rgb(239 246 255));color:var(--snice-color-primary-hover,rgb(29 78 216));box-shadow:none;transform:none}.button--outline.button--success:hover:not(.button--disabled){background-color:var(--snice-color-success-subtle,rgb(220 252 231));color:var(--snice-color-success-hover,rgb(21 128 61));box-shadow:none;transform:none}.button--outline.button--warning:hover:not(.button--disabled){background-color:var(--snice-color-warning-subtle,rgb(254 249 195));color:var(--snice-color-warning-hover,rgb(161 98 7));box-shadow:none;transform:none}.button--outline.button--danger:hover:not(.button--disabled){background-color:var(--snice-color-danger-subtle,rgb(254 226 226));color:var(--snice-color-danger-hover,rgb(185 28 28));box-shadow:none;transform:none}.button--outline:active:not(.button--disabled){background-color:var(--snice-color-primary-subtle-hover,var(--snice-color-surface-active,rgb(235 235 235)));box-shadow:none;transform:none}.button--pill{border-radius:var(--snice-border-radius-pill,9999px)}.button--circle{border-radius:var(--snice-border-radius-circle,50%);width:var(--size);padding:0}.button--small.button--circle{--size:2rem}.button--medium.button--circle{--size:2.5rem}.button--large.button--circle{--size:3rem}.button--loading{position:relative;cursor:wait}.button--loading .label{visibility:hidden}.spinner{display:none;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:1em;height:1em}.button--loading .spinner{display:block}.spinner::after{content:'';display:block;width:100%;height:100%;box-sizing:border-box;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:.6s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}.icon-slot{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:1.25em;height:1.25em;line-height:1}.icon-slot:empty{display:none}.icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:1.25em;height:1.25em;line-height:1}.icon img{width:100%;height:100%;object-fit:contain}.icon svg{width:100%;height:100%}.icon-slot ::slotted(*){display:flex;align-items:center;justify-content:center;font-size:1.25em;line-height:1}.icon-slot ::slotted(svg){width:1.25em;height:1.25em}.icon-slot ::slotted(img){width:1.25em;height:1.25em;object-fit:contain}.button--icon-start .icon,.button--icon-start .icon-slot{margin-right:.5em}.button--icon-end .icon,.button--icon-end .icon-slot{margin-left:.5em}.button--circle .icon,.button--circle .icon-slot{margin:0;transform:none;width:auto;height:auto}.button--circle .icon svg{width:1.25em;height:1.25em}.button--circle .icon-slot ::slotted(*){font-size:1.25em;line-height:1;display:flex;align-items:center;justify-content:center}.button--circle .icon-slot ::slotted(svg){width:1.25em;height:1.25em}@media (prefers-reduced-motion:reduce){.icon-slot,.spinner,button,button:active{transition-duration:0s!important;animation-duration:0s!important}button:active{transform:none}}";
3053
+ var cssContent$j = ":host([hidden]){display:none}:host{display:inline-block;position:relative;width:auto;cursor:pointer}:host([disabled]){cursor:not-allowed}:host([justify-text=start]) .button{justify-content:flex-start}:host([justify-text=end]) .button{justify-content:flex-end}:host([justify-text=center]) .button{justify-content:center}.button{display:inline-flex;align-items:center;justify-content:center;width:100%;border:1px solid;font-family:var(--snice-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);font-weight:var(--snice-font-weight-medium,500);text-decoration:none;user-select:none;white-space:nowrap;vertical-align:middle;padding:0;box-shadow:var(--snice-shadow-xs,0 1px 2px 0 rgb(0 0 0 / .03));transition:color var(--snice-transition-fast, 150ms) var(--snice-ease-out, cubic-bezier(.22,1,.36,1)),background-color var(--snice-transition-fast, 150ms) var(--snice-ease-out, cubic-bezier(.22,1,.36,1)),border-color var(--snice-transition-fast, 150ms) var(--snice-ease-out, cubic-bezier(.22,1,.36,1)),box-shadow var(--snice-transition-fast, 150ms) var(--snice-ease-out, cubic-bezier(.22,1,.36,1)),transform 180ms var(--snice-ease-spring, cubic-bezier(.34,1.56,.64,1));cursor:inherit;position:relative}.button::before{content:'';position:absolute;inset:0;border-radius:inherit;background:var(--snice-gradient-convex,none);pointer-events:none}.button:active:not(.button--disabled){transition:color 50ms var(--snice-ease-out, ease),background-color 50ms var(--snice-ease-out, ease),border-color 50ms var(--snice-ease-out, ease),box-shadow 50ms var(--snice-ease-out, ease),transform 80ms var(--snice-ease-out, ease);transform:translateY(0) scale(.97)}.button:focus-visible{outline:0;box-shadow:0 0 0 var(--snice-focus-ring-width,2px) var(--snice-focus-ring-color,rgb(59 130 246 / .5))}.button--disabled{opacity:.5;cursor:not-allowed;box-shadow:none}.button--small{font-size:var(--snice-font-size-sm, .875rem);height:2rem;padding:0 var(--snice-spacing-sm,.75rem);border-radius:var(--snice-border-radius-md,.25rem)}.button--medium{font-size:var(--snice-font-size-md, 1rem);height:2.5rem;padding:0 var(--snice-spacing-md,1rem);border-radius:var(--snice-border-radius-md,.25rem)}.button--large{font-size:var(--snice-font-size-lg, 1.125rem);height:3rem;padding:0 var(--snice-spacing-lg,1.5rem);border-radius:var(--snice-border-radius-md,.25rem)}.button--default{background-color:var(--snice-color-surface,rgb(255 255 255));color:var(--snice-color-text,rgb(23 23 23));border-color:var(--snice-color-border,rgb(226 226 226))}.button--default:hover:not(.button--disabled){background-color:var(--snice-color-surface-container-low,rgb(250 250 250));border-color:var(--snice-color-border-hover,rgb(204 204 204));box-shadow:var(--snice-shadow-sm,0 1px 2px 0 rgb(0 0 0 / .05));transform:translateY(-1px)}.button--default:active:not(.button--disabled){background-color:var(--snice-color-surface-container,rgb(248 248 248));border-color:var(--snice-color-border-hover,rgb(204 204 204));box-shadow:var(--snice-shadow-inset-sm,inset 0 1px 2px rgb(0 0 0 / .05));transform:translateY(0) scale(.97)}.button--primary{background-color:var(--snice-color-primary,rgb(37 99 235));color:var(--snice-color-text-inverse,rgb(250 250 250));border-color:var(--snice-color-primary,rgb(37 99 235))}.button--primary:hover:not(.button--disabled){background-color:var(--snice-color-primary-hover,rgb(29 78 216));border-color:var(--snice-color-primary-hover,rgb(29 78 216));box-shadow:var(--snice-shadow-sm,0 1px 2px 0 rgb(0 0 0 / .05));transform:translateY(-1px)}.button--primary:active:not(.button--disabled){background-color:color-mix(in srgb,var(--snice-color-primary,rgb(37 99 235)) 85%,#fff);border-color:color-mix(in srgb,var(--snice-color-primary,rgb(37 99 235)) 85%,#fff);box-shadow:var(--snice-shadow-inset-sm,inset 0 1px 2px rgb(0 0 0 / .05));transform:translateY(0) scale(.97)}.button--success{background-color:var(--snice-color-success,rgb(22 163 74));color:var(--snice-color-text-inverse,rgb(250 250 250));border-color:var(--snice-color-success,rgb(22 163 74))}.button--success:hover:not(.button--disabled){background-color:var(--snice-color-success-hover,rgb(21 128 61));border-color:var(--snice-color-success-hover,rgb(21 128 61));box-shadow:var(--snice-shadow-sm,0 1px 2px 0 rgb(0 0 0 / .05));transform:translateY(-1px)}.button--success:active:not(.button--disabled){background-color:color-mix(in srgb,var(--snice-color-success,rgb(22 163 74)) 85%,#fff);border-color:color-mix(in srgb,var(--snice-color-success,rgb(22 163 74)) 85%,#fff);box-shadow:var(--snice-shadow-inset-sm,inset 0 1px 2px rgb(0 0 0 / .05));transform:translateY(0) scale(.97)}.button--warning{background-color:var(--snice-color-warning,rgb(202 138 4));color:var(--button-warning-text,rgb(69 26 3));border-color:var(--snice-color-warning,rgb(202 138 4))}.button--warning:hover:not(.button--disabled){background-color:var(--snice-color-warning-hover,rgb(161 98 7));border-color:var(--snice-color-warning-hover,rgb(161 98 7));box-shadow:var(--snice-shadow-sm,0 1px 2px 0 rgb(0 0 0 / .05));transform:translateY(-1px)}.button--warning:active:not(.button--disabled){background-color:color-mix(in srgb,var(--snice-color-warning,rgb(202 138 4)) 85%,#fff);border-color:color-mix(in srgb,var(--snice-color-warning,rgb(202 138 4)) 85%,#fff);box-shadow:var(--snice-shadow-inset-sm,inset 0 1px 2px rgb(0 0 0 / .05));transform:translateY(0) scale(.97)}.button--danger{background-color:var(--snice-color-danger,rgb(220 38 38));color:var(--snice-color-text-inverse,rgb(250 250 250));border-color:var(--snice-color-danger,rgb(220 38 38))}.button--danger:hover:not(.button--disabled){background-color:var(--snice-color-danger-hover,rgb(185 28 28));border-color:var(--snice-color-danger-hover,rgb(185 28 28));box-shadow:var(--snice-shadow-sm,0 1px 2px 0 rgb(0 0 0 / .05));transform:translateY(-1px)}.button--danger:active:not(.button--disabled){background-color:color-mix(in srgb,var(--snice-color-danger,rgb(220 38 38)) 85%,#fff);border-color:color-mix(in srgb,var(--snice-color-danger,rgb(220 38 38)) 85%,#fff);box-shadow:var(--snice-shadow-inset-sm,inset 0 1px 2px rgb(0 0 0 / .05));transform:translateY(0) scale(.97)}.button--text{background-color:transparent;color:var(--snice-color-primary,rgb(37 99 235));border-color:transparent;box-shadow:none}.button--text::before{display:none}.button--text:hover:not(.button--disabled){background-color:var(--snice-color-primary-alpha,rgb(37 99 235 / .1));color:var(--snice-color-primary-hover,rgb(29 78 216));box-shadow:none;transform:none}.button--text:active:not(.button--disabled){background-color:var(--snice-color-primary-alpha-strong,rgb(37 99 235 / .15));box-shadow:none;transform:none}.button--outline{box-shadow:none}.button--outline::before{display:none}.button--outline.button--default{background-color:transparent;color:var(--snice-color-text,rgb(23 23 23))}.button--outline.button--primary{background-color:transparent;color:var(--snice-color-primary,rgb(37 99 235))}.button--outline.button--success{background-color:transparent;color:var(--snice-color-success,rgb(22 163 74))}.button--outline.button--warning{background-color:transparent;color:var(--snice-color-warning,rgb(202 138 4))}.button--outline.button--danger{background-color:transparent;color:var(--snice-color-danger,rgb(220 38 38))}.button--outline.button--default:hover:not(.button--disabled){background-color:var(--snice-color-surface-hover,rgb(245 245 245));box-shadow:none;transform:none}.button--outline.button--primary:hover:not(.button--disabled){background-color:var(--snice-color-primary-subtle,rgb(239 246 255));color:var(--snice-color-primary-hover,rgb(29 78 216));box-shadow:none;transform:none}.button--outline.button--success:hover:not(.button--disabled){background-color:var(--snice-color-success-subtle,rgb(220 252 231));color:var(--snice-color-success-hover,rgb(21 128 61));box-shadow:none;transform:none}.button--outline.button--warning:hover:not(.button--disabled){background-color:var(--snice-color-warning-subtle,rgb(254 249 195));color:var(--snice-color-warning-hover,rgb(161 98 7));box-shadow:none;transform:none}.button--outline.button--danger:hover:not(.button--disabled){background-color:var(--snice-color-danger-subtle,rgb(254 226 226));color:var(--snice-color-danger-hover,rgb(185 28 28));box-shadow:none;transform:none}.button--outline:active:not(.button--disabled){background-color:var(--snice-color-primary-subtle-hover,var(--snice-color-surface-active,rgb(235 235 235)));box-shadow:none;transform:none}.button--pill{border-radius:var(--snice-border-radius-pill,9999px)}.button--circle{border-radius:var(--snice-border-radius-circle,50%);width:var(--size);padding:0}.button--small.button--circle{--size:2rem}.button--medium.button--circle{--size:2.5rem}.button--large.button--circle{--size:3rem}.button--loading{position:relative;cursor:wait}.button--loading .label{visibility:hidden}.spinner{display:none;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:1em;height:1em}.button--loading .spinner{display:block}.spinner::after{content:'';display:block;width:100%;height:100%;box-sizing:border-box;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:.6s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}.icon-slot{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:1.25em;height:1.25em;line-height:1}.icon-slot:empty{display:none}.icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:1.25em;height:1.25em;line-height:1}.icon img{width:100%;height:100%;object-fit:contain}.icon svg{width:100%;height:100%}.icon-slot ::slotted(*){display:flex;align-items:center;justify-content:center;font-size:1.25em;line-height:1}.icon-slot ::slotted(svg){width:1.25em;height:1.25em}.icon-slot ::slotted(img){width:1.25em;height:1.25em;object-fit:contain}.button--icon-start .icon,.button--icon-start .icon-slot{margin-right:.5em}.button--icon-end .icon,.button--icon-end .icon-slot{margin-left:.5em}.button--circle .icon,.button--circle .icon-slot{margin:0;transform:none;width:auto;height:auto}.button--circle .icon svg{width:1.25em;height:1.25em}.button--circle .icon-slot ::slotted(*){font-size:1.25em;line-height:1;display:flex;align-items:center;justify-content:center}.button--circle .icon-slot ::slotted(svg){width:1.25em;height:1.25em}@media (prefers-reduced-motion:reduce){.icon-slot,.spinner,button,button:active{transition-duration:0s!important;animation-duration:0s!important}button:active{transform:none}}";
3057
3054
 
3058
3055
  (() => {
3059
3056
  let _classDecorators = [snice.element('snice-button', { formAssociated: true, delegatesFocus: true })];
@@ -3316,7 +3313,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
3316
3313
  this.formDisabled = disabled;
3317
3314
  }
3318
3315
  styles() {
3319
- return snice.css /*css*/ `${cssContent$i}`;
3316
+ return snice.css /*css*/ `${cssContent$j}`;
3320
3317
  }
3321
3318
  // Keep these methods for backwards compatibility if needed
3322
3319
  setLoading(loading) {
@@ -3353,7 +3350,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
3353
3350
  return _classThis;
3354
3351
  })();
3355
3352
 
3356
- var cssContent$h = ":host([hidden]){display:none}:host{display:inline-flex;align-items:center;align-self:flex-end;min-height:2.5rem;font-family:var(--snice-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif)}:host([size=small]){min-height:2rem;align-self:center}:host([size=large]){min-height:3rem;align-self:center}:host([compact]){min-height:0;align-self:center}.checkbox-wrapper{display:inline-flex;align-items:center;position:relative;cursor:pointer;user-select:none;min-height:1.25rem}.checkbox-wrapper--disabled{cursor:not-allowed;opacity:.6}.checkbox-input{position:absolute;opacity:0;left:0;top:0;width:100%;height:100%;margin:0;cursor:pointer;z-index:1;pointer-events:all}.checkbox{position:relative;display:inline-flex;align-items:center;justify-content:center;background:var(--snice-color-surface-container-lowest,rgb(248 247 245));border:2px solid var(--snice-color-border,rgb(226 226 226));border-radius:var(--snice-border-radius-sm,.125rem);transition:all var(--snice-transition-fast, 150ms) ease}.checkbox--small{width:1rem;height:1rem}.checkbox--medium{width:1.25rem;height:1.25rem}.checkbox--large{width:1.5rem;height:1.5rem}.checkbox-input:checked~.checkbox{background:var(--snice-color-primary,rgb(37 99 235));border-color:var(--snice-color-primary,rgb(37 99 235))}.checkbox-input:focus-visible~.checkbox{box-shadow:0 0 0 3px var(--snice-focus-ring-color,rgb(59 130 246 / .5));outline:2px solid var(--snice-color-primary,rgb(37 99 235));outline-offset:2px}.checkbox-input:disabled~.checkbox{background:var(--snice-color-surface-container-low,rgb(250 250 250));border-color:var(--snice-color-border,rgb(226 226 226))}.checkbox-input:disabled:checked~.checkbox{background:var(--snice-color-text-secondary,rgb(82 82 82));border-color:var(--snice-color-text-secondary,rgb(82 82 82))}.checkbox--invalid{border-color:var(--snice-color-danger,rgb(220 38 38))}.checkbox-input:checked~.checkbox--invalid{background:var(--snice-color-danger,rgb(220 38 38));border-color:var(--snice-color-danger,rgb(220 38 38))}.checkbox-icon{position:absolute;display:none;color:var(--snice-color-text-inverse,rgb(250 250 250))}.checkbox-input:checked~.checkbox .checkbox-icon--check{display:block}.checkbox--indeterminate .checkbox-icon--check{display:none!important}.checkbox--indeterminate .checkbox-icon--indeterminate{display:block!important}.checkbox--small .checkbox-icon{width:.75rem;height:.75rem}.checkbox--medium .checkbox-icon{width:.875rem;height:.875rem}.checkbox--large .checkbox-icon{width:1.125rem;height:1.125rem}.checkbox-label{margin-left:.5rem;color:var(--snice-color-text,rgb(23 23 23));cursor:pointer}.checkbox-label--small{font-size:var(--snice-font-size-sm, .875rem)}.checkbox-label--medium{font-size:var(--snice-font-size-md, 1rem)}.checkbox-label--large{font-size:var(--snice-font-size-lg, 1.125rem)}.checkbox-wrapper--disabled .checkbox-label{color:var(--snice-color-text-secondary,rgb(82 82 82));cursor:not-allowed}.checkbox-label--required::after{content:' *';color:var(--snice-color-danger,rgb(220 38 38))}.checkbox-wrapper:not(.checkbox-wrapper--disabled):hover .checkbox{border-color:var(--snice-color-primary,rgb(37 99 235))}.checkbox-wrapper:not(.checkbox-wrapper--disabled):hover .checkbox-input:checked~.checkbox{background:var(--snice-color-primary-hover,rgb(29 78 216));border-color:var(--snice-color-primary-hover,rgb(29 78 216))}.checkbox-wrapper--loading{cursor:wait;opacity:.7}.checkbox--loading{cursor:wait}.checkbox-spinner{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;width:100%;height:100%;pointer-events:none}.checkbox-spinner::after{content:'';display:block;width:.75em;height:.75em;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:.6s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){*{transition-duration:0s!important;animation-duration:0s!important}}";
3353
+ var cssContent$i = ":host([hidden]){display:none}:host{display:inline-flex;align-items:center;align-self:flex-end;min-height:2.5rem;font-family:var(--snice-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif)}:host([size=small]){min-height:2rem;align-self:center}:host([size=large]){min-height:3rem;align-self:center}:host([compact]){min-height:0;align-self:center}.checkbox-wrapper{display:inline-flex;align-items:center;position:relative;cursor:pointer;user-select:none;min-height:1.25rem}.checkbox-wrapper--disabled{cursor:not-allowed;opacity:.6}.checkbox-input{position:absolute;opacity:0;left:0;top:0;width:100%;height:100%;margin:0;cursor:pointer;z-index:1;pointer-events:all}.checkbox{position:relative;display:inline-flex;align-items:center;justify-content:center;background:var(--snice-color-surface-container-lowest,rgb(248 247 245));border:2px solid var(--snice-color-border,rgb(226 226 226));border-radius:var(--snice-border-radius-sm,.125rem);transition:all var(--snice-transition-fast, 150ms) ease}.checkbox--small{width:1rem;height:1rem}.checkbox--medium{width:1.25rem;height:1.25rem}.checkbox--large{width:1.5rem;height:1.5rem}.checkbox-input:checked~.checkbox{background:var(--snice-color-primary,rgb(37 99 235));border-color:var(--snice-color-primary,rgb(37 99 235))}.checkbox-input:focus-visible~.checkbox{box-shadow:0 0 0 3px var(--snice-focus-ring-color,rgb(59 130 246 / .5));outline:2px solid var(--snice-color-primary,rgb(37 99 235));outline-offset:2px}.checkbox-input:disabled~.checkbox{background:var(--snice-color-surface-container-low,rgb(250 250 250));border-color:var(--snice-color-border,rgb(226 226 226))}.checkbox-input:disabled:checked~.checkbox{background:var(--snice-color-text-secondary,rgb(82 82 82));border-color:var(--snice-color-text-secondary,rgb(82 82 82))}.checkbox--invalid{border-color:var(--snice-color-danger,rgb(220 38 38))}.checkbox-input:checked~.checkbox--invalid{background:var(--snice-color-danger,rgb(220 38 38));border-color:var(--snice-color-danger,rgb(220 38 38))}.checkbox-icon{position:absolute;display:none;color:var(--snice-color-text-inverse,rgb(250 250 250))}.checkbox-input:checked~.checkbox .checkbox-icon--check{display:block}.checkbox--indeterminate .checkbox-icon--check{display:none!important}.checkbox--indeterminate .checkbox-icon--indeterminate{display:block!important}.checkbox--small .checkbox-icon{width:.75rem;height:.75rem}.checkbox--medium .checkbox-icon{width:.875rem;height:.875rem}.checkbox--large .checkbox-icon{width:1.125rem;height:1.125rem}.checkbox-label{margin-left:.5rem;color:var(--snice-color-text,rgb(23 23 23));cursor:pointer}.checkbox-label--small{font-size:var(--snice-font-size-sm, .875rem)}.checkbox-label--medium{font-size:var(--snice-font-size-md, 1rem)}.checkbox-label--large{font-size:var(--snice-font-size-lg, 1.125rem)}.checkbox-wrapper--disabled .checkbox-label{color:var(--snice-color-text-secondary,rgb(82 82 82));cursor:not-allowed}.checkbox-label--required::after{content:' *';color:var(--snice-color-danger,rgb(220 38 38))}.checkbox-wrapper:not(.checkbox-wrapper--disabled):hover .checkbox{border-color:var(--snice-color-primary,rgb(37 99 235))}.checkbox-wrapper:not(.checkbox-wrapper--disabled):hover .checkbox-input:checked~.checkbox{background:var(--snice-color-primary-hover,rgb(29 78 216));border-color:var(--snice-color-primary-hover,rgb(29 78 216))}.checkbox-wrapper--loading{cursor:wait;opacity:.7}.checkbox--loading{cursor:wait}.checkbox-spinner{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;width:100%;height:100%;pointer-events:none}.checkbox-spinner::after{content:'';display:block;width:.75em;height:.75em;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:.6s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){*{transition-duration:0s!important;animation-duration:0s!important}}";
3357
3354
 
3358
3355
  (() => {
3359
3356
  let _classDecorators = [snice.element('snice-checkbox', { formAssociated: true, delegatesFocus: true })];
@@ -3769,7 +3766,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
3769
3766
  };
3770
3767
  }
3771
3768
  styles() {
3772
- return snice.css /*css*/ `${cssContent$h}`;
3769
+ return snice.css /*css*/ `${cssContent$i}`;
3773
3770
  }
3774
3771
  // Public API
3775
3772
  focus() {
@@ -3831,7 +3828,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
3831
3828
  return _classThis;
3832
3829
  })();
3833
3830
 
3834
- var cssContent$g = ":host{--modal-z-index:1000;--modal-backdrop-bg:var(--snice-modal-backdrop, rgb(0 0 0 / 0.6));--modal-backdrop-blur:6px;--modal-panel-bg:var(--snice-color-surface-container-highest, var(--snice-color-surface, rgb(255 255 255)));--modal-panel-shadow:var(--snice-shadow-2xl, 0 25px 50px -12px rgb(0 0 0 / 0.25));--modal-border-radius:var(--snice-border-radius-lg, 8px);--modal-padding:var(--snice-spacing-lg, 1.5rem);--modal-transition-duration:200ms}.modal{position:fixed;top:0;left:0;right:0;bottom:0;z-index:var(--modal-z-index);display:flex;align-items:center;justify-content:center;padding:var(--modal-padding);pointer-events:none;opacity:0;visibility:hidden}.modal--open{pointer-events:auto;opacity:1;visibility:visible}.modal__backdrop{position:absolute;top:0;left:0;right:0;bottom:0;background:var(--modal-backdrop-bg);backdrop-filter:blur(var(--modal-backdrop-blur));-webkit-backdrop-filter:blur(var(--modal-backdrop-blur));opacity:0;transition:opacity 180ms var(--snice-ease-out, ease)}.modal--open .modal__backdrop{opacity:1}.modal__panel{position:relative;background:var(--modal-panel-bg);border-radius:var(--modal-border-radius);box-shadow:var(--modal-panel-shadow);display:flex;flex-direction:column;max-height:calc(100vh - 2 * var(--modal-padding));opacity:0;transform:scale(.96) translateY(1rem);transition:opacity 260ms var(--snice-ease-out, ease) 80ms,transform 320ms var(--snice-ease-spring, cubic-bezier(.34,1.56,.64,1)) 80ms;z-index:1}.modal--open .modal__panel{opacity:1;transform:scale(1) translateY(0)}.modal--open .modal__panel>*{animation:snice-modal-content-in 240ms var(--snice-ease-out,ease) backwards}.modal--open .modal__header{animation-delay:140ms}.modal--open .modal__body{animation-delay:180ms}.modal--open .modal__footer{animation-delay:220ms}@keyframes snice-modal-content-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.modal__panel--small{width:100%;max-width:25rem}.modal__panel--medium{width:100%;max-width:37.5rem}.modal__panel--large{width:100%;max-width:56.25rem}.modal__panel--fullscreen{width:calc(100vw - 2 * var(--modal-padding));height:calc(100vh - 2 * var(--modal-padding));max-width:none}.modal__header{font-family:var(--snice-font-family-display, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);display:flex;align-items:center;justify-content:space-between;padding:var(--modal-padding);padding-bottom:var(--snice-spacing-sm,.5rem)}.modal__header--empty{font-family:var(--snice-font-family-display, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);display:none}::slotted([slot=header]){flex:1;margin:0;font-size:var(--snice-font-size-lg, 1.125rem);font-weight:var(--snice-font-weight-semibold,600);color:var(--snice-color-text,rgb(23 23 23))}.modal__close{display:flex;align-items:center;justify-content:center;width:2rem;height:2rem;padding:0;margin-left:auto;background:0 0;border:none;border-radius:var(--snice-border-radius-md,6px);color:var(--snice-color-text-secondary,rgb(82 82 82));cursor:pointer;transition:background-color var(--snice-transition-fast, 150ms) ease,color var(--snice-transition-fast, 150ms) ease}.modal__close:hover{background:var(--snice-color-surface-hover,rgb(241 241 241));color:var(--snice-color-text,rgb(23 23 23))}.modal__close:focus-visible{outline:2px solid var(--snice-color-primary,rgb(37 99 235));outline-offset:2px}.modal__close svg{width:1.125rem;height:1.125rem}.modal__body{flex:1;padding:var(--modal-padding);overflow-y:auto;color:var(--snice-color-text,rgb(23 23 23))}.modal__footer{display:flex;align-items:center;justify-content:flex-end;gap:var(--snice-spacing-sm,.5rem);padding:var(--modal-padding);padding-top:var(--snice-spacing-sm,.5rem)}.modal__footer--empty{display:none}.modal__header--empty+.modal__body{font-family:var(--snice-font-family-display, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);padding-top:var(--modal-padding)}.modal__body:has(+ .modal__footer--empty){padding-bottom:var(--modal-padding)}@keyframes slideUp{from{opacity:0;transform:translateY(6.25rem)}to{opacity:1;transform:translateY(0)}}@keyframes fadeOut{from{opacity:1}to{opacity:0}}@media (max-width:640px){.modal{padding:0}.modal__panel:not(.modal__panel--fullscreen){width:100%;height:100%;max-width:none;max-height:none;border-radius:0}}@media (prefers-reduced-motion:reduce){*{transition-duration:0s!important;animation-duration:0s!important}}";
3831
+ var cssContent$h = ":host{--modal-z-index:1000;--modal-backdrop-bg:var(--snice-modal-backdrop, rgb(0 0 0 / 0.6));--modal-backdrop-blur:6px;--modal-panel-bg:var(--snice-color-surface-container-highest, var(--snice-color-surface, rgb(255 255 255)));--modal-panel-shadow:var(--snice-shadow-2xl, 0 25px 50px -12px rgb(0 0 0 / 0.25));--modal-border-radius:var(--snice-border-radius-lg, 8px);--modal-padding:var(--snice-spacing-lg, 1.5rem);--modal-transition-duration:200ms}.modal{position:fixed;top:0;left:0;right:0;bottom:0;z-index:var(--modal-z-index);display:flex;align-items:center;justify-content:center;padding:var(--modal-padding);pointer-events:none;opacity:0;visibility:hidden}.modal--open{pointer-events:auto;opacity:1;visibility:visible}.modal__backdrop{position:absolute;top:0;left:0;right:0;bottom:0;background:var(--modal-backdrop-bg);backdrop-filter:blur(var(--modal-backdrop-blur));-webkit-backdrop-filter:blur(var(--modal-backdrop-blur));opacity:0;transition:opacity 180ms var(--snice-ease-out, ease)}.modal--open .modal__backdrop{opacity:1}.modal__panel{position:relative;background:var(--modal-panel-bg);border-radius:var(--modal-border-radius);box-shadow:var(--modal-panel-shadow);display:flex;flex-direction:column;max-height:calc(100vh - 2 * var(--modal-padding));opacity:0;transform:scale(.96) translateY(1rem);transition:opacity 260ms var(--snice-ease-out, ease) 80ms,transform 320ms var(--snice-ease-spring, cubic-bezier(.34,1.56,.64,1)) 80ms;z-index:1}.modal--open .modal__panel{opacity:1;transform:scale(1) translateY(0)}.modal--open .modal__panel>*{animation:snice-modal-content-in 240ms var(--snice-ease-out,ease) backwards}.modal--open .modal__header{animation-delay:140ms}.modal--open .modal__body{animation-delay:180ms}.modal--open .modal__footer{animation-delay:220ms}@keyframes snice-modal-content-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.modal__panel--small{width:100%;max-width:25rem}.modal__panel--medium{width:100%;max-width:37.5rem}.modal__panel--large{width:100%;max-width:56.25rem}.modal__panel--fullscreen{width:calc(100vw - 2 * var(--modal-padding));height:calc(100vh - 2 * var(--modal-padding));max-width:none}.modal__header{font-family:var(--snice-font-family-display, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);display:flex;align-items:center;justify-content:space-between;padding:var(--modal-padding);padding-bottom:var(--snice-spacing-sm,.5rem)}.modal__header--empty{font-family:var(--snice-font-family-display, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);display:none}::slotted([slot=header]){flex:1;margin:0;font-size:var(--snice-font-size-lg, 1.125rem);font-weight:var(--snice-font-weight-semibold,600);color:var(--snice-color-text,rgb(23 23 23))}.modal__close{display:flex;align-items:center;justify-content:center;width:2rem;height:2rem;padding:0;margin-left:auto;background:0 0;border:none;border-radius:var(--snice-border-radius-md,6px);color:var(--snice-color-text-secondary,rgb(82 82 82));cursor:pointer;transition:background-color var(--snice-transition-fast, 150ms) ease,color var(--snice-transition-fast, 150ms) ease}.modal__close:hover{background:var(--snice-color-surface-hover,rgb(241 241 241));color:var(--snice-color-text,rgb(23 23 23))}.modal__close:focus-visible{outline:2px solid var(--snice-color-primary,rgb(37 99 235));outline-offset:2px}.modal__close svg{width:1.125rem;height:1.125rem}.modal__body{flex:1;padding:var(--modal-padding);overflow-y:auto;color:var(--snice-color-text,rgb(23 23 23))}.modal__footer{display:flex;align-items:center;justify-content:flex-end;gap:var(--snice-spacing-sm,.5rem);padding:var(--modal-padding);padding-top:var(--snice-spacing-sm,.5rem)}.modal__footer--empty{display:none}.modal__header--empty+.modal__body{font-family:var(--snice-font-family-display, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);padding-top:var(--modal-padding)}.modal__body:has(+ .modal__footer--empty){padding-bottom:var(--modal-padding)}@keyframes slideUp{from{opacity:0;transform:translateY(6.25rem)}to{opacity:1;transform:translateY(0)}}@keyframes fadeOut{from{opacity:1}to{opacity:0}}@media (max-width:640px){.modal{padding:0}.modal__panel:not(.modal__panel--fullscreen){width:100%;height:100%;max-width:none;max-height:none;border-radius:0}}@media (prefers-reduced-motion:reduce){*{transition-duration:0s!important;animation-duration:0s!important}}";
3835
3832
 
3836
3833
  (() => {
3837
3834
  let _classDecorators = [snice.element('snice-modal')];
@@ -4004,7 +4001,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
4004
4001
  `;
4005
4002
  }
4006
4003
  styles() {
4007
- return snice.css /*css*/ `${cssContent$g}`;
4004
+ return snice.css /*css*/ `${cssContent$h}`;
4008
4005
  }
4009
4006
  init() {
4010
4007
  // Make sure modal state is properly initialized
@@ -4138,7 +4135,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
4138
4135
  return _classThis;
4139
4136
  })();
4140
4137
 
4141
- var cssContent$f = ":host([hidden]){display:none}:host{display:block;font-family:var(--snice-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif)}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:var(--snice-spacing-2xl,3rem) var(--snice-spacing-xl,2rem);color:var(--snice-color-text-secondary,rgb(82 82 82));animation:snice-empty-in 360ms var(--snice-ease-out,ease) backwards}@keyframes snice-empty-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.empty-state--small{padding:var(--snice-spacing-xl,2rem) var(--snice-spacing-lg,1.5rem)}.empty-state--medium{padding:var(--snice-spacing-2xl,3rem) var(--snice-spacing-xl,2rem)}.empty-state--large{padding:var(--snice-spacing-3xl,4rem) var(--snice-spacing-2xl,2.5rem)}.empty-state__icon-wrapper{display:flex;align-items:center;justify-content:center;margin-bottom:var(--snice-spacing-lg,1.5rem);position:relative;width:calc(var(--snice-font-size-4xl,4rem) * 1.8);height:calc(var(--snice-font-size-4xl,4rem) * 1.8)}.empty-state__icon-wrapper::before{content:'';position:absolute;inset:0;border-radius:50%;border:1px dashed color-mix(in srgb,var(--snice-color-border,#ddd) 70%,transparent);animation:18s linear infinite snice-empty-halo}@keyframes snice-empty-halo{to{transform:rotate(360deg)}}.empty-state__icon{font-size:var(--snice-font-size-4xl, 4rem);opacity:.5;line-height:1}.empty-state__icon svg{width:1em;height:1em;display:block}.empty-state__icon-wrapper ::slotted(*){display:flex;align-items:center;justify-content:center;font-size:var(--snice-font-size-4xl, 4rem);opacity:.5;line-height:1}.empty-state__icon-wrapper ::slotted(svg){width:var(--snice-font-size-4xl,4rem);height:var(--snice-font-size-4xl,4rem)}.empty-state__icon-wrapper ::slotted(img){width:var(--snice-font-size-4xl,4rem);height:var(--snice-font-size-4xl,4rem);object-fit:contain}.empty-state--small .empty-state__icon-wrapper{margin-bottom:var(--snice-spacing-md,1rem);width:calc(var(--snice-font-size-3xl,3rem) * 1.8);height:calc(var(--snice-font-size-3xl,3rem) * 1.8)}.empty-state--small .empty-state__icon{font-size:var(--snice-font-size-3xl, 3rem)}.empty-state--small .empty-state__icon-wrapper ::slotted(*){font-size:var(--snice-font-size-3xl, 3rem)}.empty-state--small .empty-state__icon-wrapper ::slotted(img),.empty-state--small .empty-state__icon-wrapper ::slotted(svg){width:var(--snice-font-size-3xl,3rem);height:var(--snice-font-size-3xl,3rem)}.empty-state--large .empty-state__icon-wrapper{margin-bottom:var(--snice-spacing-xl,2rem);width:calc(var(--snice-font-size-3xl,5rem) * 1.8);height:calc(var(--snice-font-size-3xl,5rem) * 1.8)}.empty-state--large .empty-state__icon{font-size:var(--snice-font-size-3xl, 5rem)}.empty-state--large .empty-state__icon-wrapper ::slotted(*){font-size:var(--snice-font-size-3xl, 5rem)}.empty-state--large .empty-state__icon-wrapper ::slotted(img),.empty-state--large .empty-state__icon-wrapper ::slotted(svg){width:var(--snice-font-size-3xl,5rem);height:var(--snice-font-size-3xl,5rem)}.empty-state__title{font-family:var(--snice-font-family-display, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);font-size:var(--snice-heading-lg, 1.25rem);font-weight:var(--snice-font-weight-semibold,600);color:var(--snice-color-text,rgb(23 23 23));margin:0 0 var(--snice-spacing-xs,.5rem) 0}.empty-state--small .empty-state__title{font-family:var(--snice-font-family-display, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);font-size:var(--snice-heading-md, 1.125rem);margin-bottom:var(--snice-spacing-2xs,.375rem)}.empty-state--large .empty-state__title{font-family:var(--snice-font-family-display, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);font-size:var(--snice-heading-xl, 1.5rem);margin-bottom:var(--snice-spacing-sm,.75rem)}.empty-state__description{font-size:var(--snice-font-size-sm, .875rem);color:var(--snice-color-text-secondary,rgb(82 82 82));margin:0 0 var(--snice-spacing-lg,1.5rem) 0;max-width:32rem;line-height:var(--snice-line-height-normal, 1.5)}.empty-state--small .empty-state__description{font-size:var(--snice-font-size-xs, .8125rem);margin-bottom:var(--snice-spacing-md,1rem)}.empty-state--large .empty-state__description{font-size:var(--snice-font-size-md, 1rem);margin-bottom:var(--snice-spacing-xl,2rem)}.empty-state__action{display:inline-flex;align-items:center;justify-content:center;padding:var(--snice-spacing-xs,.5rem) var(--snice-spacing-md,1rem);font-size:var(--snice-font-size-sm, .875rem);font-weight:var(--snice-font-weight-medium,500);color:var(--snice-color-text-inverse,rgb(250 250 250));background:var(--snice-color-primary,rgb(37 99 235));border:none;border-radius:var(--snice-border-radius-md,.375rem);text-decoration:none;cursor:pointer;transition:background var(--snice-transition-fast, 150ms)}.empty-state__action:hover{background:var(--snice-color-primary-hover,rgb(29 78 216))}.empty-state--small .empty-state__action{padding:var(--snice-spacing-2xs,.375rem) var(--snice-spacing-sm,.75rem);font-size:var(--snice-font-size-xs, .8125rem)}.empty-state--large .empty-state__action{padding:var(--snice-spacing-sm,.625rem) var(--snice-spacing-lg,1.25rem);font-size:var(--snice-font-size-md, 1rem)}.empty-state__extra ::slotted(*){margin-top:var(--snice-spacing-md,1rem)}@media (prefers-reduced-motion:reduce){*{transition-duration:0s!important;animation-duration:0s!important}}";
4138
+ var cssContent$g = ":host([hidden]){display:none}:host{display:block;font-family:var(--snice-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif)}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:var(--snice-spacing-2xl,3rem) var(--snice-spacing-xl,2rem);color:var(--snice-color-text-secondary,rgb(82 82 82));animation:snice-empty-in 360ms var(--snice-ease-out,ease) backwards}@keyframes snice-empty-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.empty-state--small{padding:var(--snice-spacing-xl,2rem) var(--snice-spacing-lg,1.5rem)}.empty-state--medium{padding:var(--snice-spacing-2xl,3rem) var(--snice-spacing-xl,2rem)}.empty-state--large{padding:var(--snice-spacing-3xl,4rem) var(--snice-spacing-2xl,2.5rem)}.empty-state__icon-wrapper{display:flex;align-items:center;justify-content:center;margin-bottom:var(--snice-spacing-lg,1.5rem);position:relative;width:calc(var(--snice-font-size-4xl,4rem) * 1.8);height:calc(var(--snice-font-size-4xl,4rem) * 1.8)}.empty-state__icon-wrapper::before{content:'';position:absolute;inset:0;border-radius:50%;border:1px dashed color-mix(in srgb,var(--snice-color-border,#ddd) 70%,transparent);animation:18s linear infinite snice-empty-halo}@keyframes snice-empty-halo{to{transform:rotate(360deg)}}.empty-state__icon{font-size:var(--snice-font-size-4xl, 4rem);opacity:.5;line-height:1}.empty-state__icon svg{width:1em;height:1em;display:block}.empty-state__icon-wrapper ::slotted(*){display:flex;align-items:center;justify-content:center;font-size:var(--snice-font-size-4xl, 4rem);opacity:.5;line-height:1}.empty-state__icon-wrapper ::slotted(svg){width:var(--snice-font-size-4xl,4rem);height:var(--snice-font-size-4xl,4rem)}.empty-state__icon-wrapper ::slotted(img){width:var(--snice-font-size-4xl,4rem);height:var(--snice-font-size-4xl,4rem);object-fit:contain}.empty-state--small .empty-state__icon-wrapper{margin-bottom:var(--snice-spacing-md,1rem);width:calc(var(--snice-font-size-3xl,3rem) * 1.8);height:calc(var(--snice-font-size-3xl,3rem) * 1.8)}.empty-state--small .empty-state__icon{font-size:var(--snice-font-size-3xl, 3rem)}.empty-state--small .empty-state__icon-wrapper ::slotted(*){font-size:var(--snice-font-size-3xl, 3rem)}.empty-state--small .empty-state__icon-wrapper ::slotted(img),.empty-state--small .empty-state__icon-wrapper ::slotted(svg){width:var(--snice-font-size-3xl,3rem);height:var(--snice-font-size-3xl,3rem)}.empty-state--large .empty-state__icon-wrapper{margin-bottom:var(--snice-spacing-xl,2rem);width:calc(var(--snice-font-size-3xl,5rem) * 1.8);height:calc(var(--snice-font-size-3xl,5rem) * 1.8)}.empty-state--large .empty-state__icon{font-size:var(--snice-font-size-3xl, 5rem)}.empty-state--large .empty-state__icon-wrapper ::slotted(*){font-size:var(--snice-font-size-3xl, 5rem)}.empty-state--large .empty-state__icon-wrapper ::slotted(img),.empty-state--large .empty-state__icon-wrapper ::slotted(svg){width:var(--snice-font-size-3xl,5rem);height:var(--snice-font-size-3xl,5rem)}.empty-state__title{font-family:var(--snice-font-family-display, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);font-size:var(--snice-heading-lg, 1.25rem);font-weight:var(--snice-font-weight-semibold,600);color:var(--snice-color-text,rgb(23 23 23));margin:0 0 var(--snice-spacing-xs,.5rem) 0}.empty-state--small .empty-state__title{font-family:var(--snice-font-family-display, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);font-size:var(--snice-heading-md, 1.125rem);margin-bottom:var(--snice-spacing-2xs,.375rem)}.empty-state--large .empty-state__title{font-family:var(--snice-font-family-display, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);font-size:var(--snice-heading-xl, 1.5rem);margin-bottom:var(--snice-spacing-sm,.75rem)}.empty-state__description{font-size:var(--snice-font-size-sm, .875rem);color:var(--snice-color-text-secondary,rgb(82 82 82));margin:0 0 var(--snice-spacing-lg,1.5rem) 0;max-width:32rem;line-height:var(--snice-line-height-normal, 1.5)}.empty-state--small .empty-state__description{font-size:var(--snice-font-size-xs, .8125rem);margin-bottom:var(--snice-spacing-md,1rem)}.empty-state--large .empty-state__description{font-size:var(--snice-font-size-md, 1rem);margin-bottom:var(--snice-spacing-xl,2rem)}.empty-state__action{display:inline-flex;align-items:center;justify-content:center;padding:var(--snice-spacing-xs,.5rem) var(--snice-spacing-md,1rem);font-size:var(--snice-font-size-sm, .875rem);font-weight:var(--snice-font-weight-medium,500);color:var(--snice-color-text-inverse,rgb(250 250 250));background:var(--snice-color-primary,rgb(37 99 235));border:none;border-radius:var(--snice-border-radius-md,.375rem);text-decoration:none;cursor:pointer;transition:background var(--snice-transition-fast, 150ms)}.empty-state__action:hover{background:var(--snice-color-primary-hover,rgb(29 78 216))}.empty-state--small .empty-state__action{padding:var(--snice-spacing-2xs,.375rem) var(--snice-spacing-sm,.75rem);font-size:var(--snice-font-size-xs, .8125rem)}.empty-state--large .empty-state__action{padding:var(--snice-spacing-sm,.625rem) var(--snice-spacing-lg,1.25rem);font-size:var(--snice-font-size-md, 1rem)}.empty-state__extra ::slotted(*){margin-top:var(--snice-spacing-md,1rem)}@media (prefers-reduced-motion:reduce){*{transition-duration:0s!important;animation-duration:0s!important}}";
4142
4139
 
4143
4140
  (() => {
4144
4141
  let _classDecorators = [snice.element('snice-empty-state')];
@@ -4233,7 +4230,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
4233
4230
  `;
4234
4231
  }
4235
4232
  styles() {
4236
- return snice.css /*css*/ `${cssContent$f}`;
4233
+ return snice.css /*css*/ `${cssContent$g}`;
4237
4234
  }
4238
4235
  handleActionClick(e) {
4239
4236
  if (!this.actionHref) {
@@ -4258,6 +4255,265 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
4258
4255
  return _classThis;
4259
4256
  })();
4260
4257
 
4258
+ var cssContent$f = ":host([hidden]){display:none}:host{--progress-height:0.5rem;--progress-radius:4px;--progress-bg:var(--snice-color-border, rgb(226 226 226));--progress-color:var(--snice-color-primary, rgb(37 99 235));--progress-animation-duration:1.5s;display:inline-block;width:100%}:host([variant=circular]){width:auto}.progress{position:relative;width:100%;background:var(--progress-bg);border-radius:var(--progress-radius);overflow:hidden}.progress--linear{height:var(--progress-height)}.progress__bar{height:100%;background:var(--progress-color);border-radius:var(--progress-radius);transition:width .3s;position:relative;overflow:hidden}:host([size=small]){--progress-height:0.25rem;--progress-spinner-size:1.5rem;--progress-spinner-stroke:3px}:host([size=medium]){--progress-height:0.5rem;--progress-spinner-size:2.5rem;--progress-spinner-stroke:4px}:host([size=large]){--progress-height:0.75rem;--progress-spinner-size:3.5rem;--progress-spinner-stroke:5px}:host([size=xl]){--progress-height:1rem;--progress-spinner-size:5rem;--progress-spinner-stroke:6px}:host([size=xxl]){--progress-height:1.25rem;--progress-spinner-size:7.5rem;--progress-spinner-stroke:8px}:host([size=xxxl]){--progress-height:1.5rem;--progress-spinner-size:10rem;--progress-spinner-stroke:10px}:host([color=primary]){--progress-color:var(--snice-color-primary, rgb(37 99 235))}:host([color=success]){--progress-color:var(--snice-color-success, rgb(22 163 74))}:host([color=warning]){--progress-color:var(--snice-color-warning, rgb(217 119 6))}:host([color=error]){--progress-color:var(--snice-color-danger, rgb(220 38 38))}:host([color=info]){--progress-color:var(--snice-color-primary, rgb(37 99 235))}:host([striped]) .progress__bar::after{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background-image:linear-gradient(45deg,rgb(255 255 255 / .15) 25%,transparent 25%,transparent 50%,rgb(255 255 255 / .15) 50%,rgb(255 255 255 / .15) 75%,transparent 75%,transparent);background-size:1rem 1rem}:host([animated]) .progress__bar::after{animation:progress-stripes var(--progress-animation-duration) linear infinite}@keyframes progress-stripes{from{background-position:1rem 0}to{background-position:0 0}}:host([indeterminate]) .progress__bar{width:30%!important;animation:progress-indeterminate var(--progress-animation-duration) ease-in-out infinite}@keyframes progress-indeterminate{0%{transform:translateX(-100%)}100%{transform:translateX(400%)}}.progress__label{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:.75rem;font-weight:var(--snice-font-weight-semibold,600);color:var(--snice-color-text-inverse,rgb(255 255 255));white-space:nowrap;z-index:1}.progress--with-label{height:calc(var(--progress-height) + .5rem);min-height:1.25rem}.progress--circular{display:inline-block;width:var(--progress-spinner-size,2.5rem);height:var(--progress-spinner-size,2.5rem);position:relative;background:0 0}.progress__circle{transform:rotate(-90deg);width:100%;height:100%;background:0 0;shape-rendering:geometricPrecision}.progress__circle-bar,.progress__circle-bg{fill:none;stroke-width:var(--progress-spinner-stroke,4px);shape-rendering:geometricPrecision;vector-effect:non-scaling-stroke}.progress__circle-bg{stroke:var(--progress-bg);opacity:var(--progress-track-opacity, .2)}.progress__circle-bar{stroke:var(--progress-color);stroke-linecap:round;transition:stroke-dashoffset .3s}:host([variant=circular][indeterminate]) .progress__circle{animation:2s linear infinite progress-rotate}:host([variant=circular][indeterminate]) .progress__circle-bar{animation:1.5s ease-in-out infinite progress-dash}@keyframes progress-rotate{100%{transform:rotate(270deg)}}@keyframes progress-dash{0%{stroke-dasharray:1,150;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-35}100%{stroke-dasharray:90,150;stroke-dashoffset:-124}}.progress__circle-label{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:.625rem;font-weight:var(--snice-font-weight-semibold,600);color:var(--progress-color);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}:host([size=small]) .progress__circle-label{font-size:.375rem}:host([size=medium]) .progress__circle-label{font-size:.625rem}:host([size=large]) .progress__circle-label{font-size:1rem}:host([size=xl]) .progress__circle-label{font-size:1.25rem}:host([size=xxl]) .progress__circle-label{font-size:1.75rem}:host([size=xxxl]) .progress__circle-label{font-size:2.25rem}@media (prefers-reduced-motion:reduce){*{transition-duration:0s!important;animation-duration:0s!important}}";
4259
+
4260
+ (() => {
4261
+ let _classDecorators = [snice.element('snice-progress')];
4262
+ let _classDescriptor;
4263
+ let _classExtraInitializers = [];
4264
+ let _classThis;
4265
+ let _classSuper = HTMLElement;
4266
+ let _instanceExtraInitializers = [];
4267
+ let _value_decorators;
4268
+ let _value_initializers = [];
4269
+ let _value_extraInitializers = [];
4270
+ let _max_decorators;
4271
+ let _max_initializers = [];
4272
+ let _max_extraInitializers = [];
4273
+ let _variant_decorators;
4274
+ let _variant_initializers = [];
4275
+ let _variant_extraInitializers = [];
4276
+ let _size_decorators;
4277
+ let _size_initializers = [];
4278
+ let _size_extraInitializers = [];
4279
+ let _color_decorators;
4280
+ let _color_initializers = [];
4281
+ let _color_extraInitializers = [];
4282
+ let _indeterminate_decorators;
4283
+ let _indeterminate_initializers = [];
4284
+ let _indeterminate_extraInitializers = [];
4285
+ let _showLabel_decorators;
4286
+ let _showLabel_initializers = [];
4287
+ let _showLabel_extraInitializers = [];
4288
+ let _label_decorators;
4289
+ let _label_initializers = [];
4290
+ let _label_extraInitializers = [];
4291
+ let _striped_decorators;
4292
+ let _striped_initializers = [];
4293
+ let _striped_extraInitializers = [];
4294
+ let _animated_decorators;
4295
+ let _animated_initializers = [];
4296
+ let _animated_extraInitializers = [];
4297
+ let _thickness_decorators;
4298
+ let _thickness_initializers = [];
4299
+ let _thickness_extraInitializers = [];
4300
+ let _progressBar_decorators;
4301
+ let _progressBar_initializers = [];
4302
+ let _progressBar_extraInitializers = [];
4303
+ let _circleBar_decorators;
4304
+ let _circleBar_initializers = [];
4305
+ let _circleBar_extraInitializers = [];
4306
+ let _labelElement_decorators;
4307
+ let _labelElement_initializers = [];
4308
+ let _labelElement_extraInitializers = [];
4309
+ let _circleLabelElement_decorators;
4310
+ let _circleLabelElement_initializers = [];
4311
+ let _circleLabelElement_extraInitializers = [];
4312
+ let _render_decorators;
4313
+ let _styles_decorators;
4314
+ let _handleProgressChange_decorators;
4315
+ let _handleThicknessChange_decorators;
4316
+ let _handleColorChange_decorators;
4317
+ (class extends _classSuper {
4318
+ static { _classThis = this; }
4319
+ constructor() {
4320
+ super(...arguments);
4321
+ this.value = (__runInitializers(this, _instanceExtraInitializers), __runInitializers(this, _value_initializers, 0));
4322
+ this.max = (__runInitializers(this, _value_extraInitializers), __runInitializers(this, _max_initializers, 100));
4323
+ this.variant = (__runInitializers(this, _max_extraInitializers), __runInitializers(this, _variant_initializers, 'linear'));
4324
+ this.size = (__runInitializers(this, _variant_extraInitializers), __runInitializers(this, _size_initializers, 'medium'));
4325
+ this.color = (__runInitializers(this, _size_extraInitializers), __runInitializers(this, _color_initializers, 'primary'));
4326
+ this.indeterminate = (__runInitializers(this, _color_extraInitializers), __runInitializers(this, _indeterminate_initializers, false));
4327
+ this.showLabel = (__runInitializers(this, _indeterminate_extraInitializers), __runInitializers(this, _showLabel_initializers, false));
4328
+ this.label = (__runInitializers(this, _showLabel_extraInitializers), __runInitializers(this, _label_initializers, ''));
4329
+ this.striped = (__runInitializers(this, _label_extraInitializers), __runInitializers(this, _striped_initializers, false));
4330
+ this.animated = (__runInitializers(this, _striped_extraInitializers), __runInitializers(this, _animated_initializers, false));
4331
+ this.thickness = (__runInitializers(this, _animated_extraInitializers), __runInitializers(this, _thickness_initializers, 4));
4332
+ this.progressBar = (__runInitializers(this, _thickness_extraInitializers), __runInitializers(this, _progressBar_initializers, void 0));
4333
+ this.circleBar = (__runInitializers(this, _progressBar_extraInitializers), __runInitializers(this, _circleBar_initializers, void 0));
4334
+ this.labelElement = (__runInitializers(this, _circleBar_extraInitializers), __runInitializers(this, _labelElement_initializers, void 0));
4335
+ this.circleLabelElement = (__runInitializers(this, _labelElement_extraInitializers), __runInitializers(this, _circleLabelElement_initializers, void 0));
4336
+ this.radius = (__runInitializers(this, _circleLabelElement_extraInitializers), 0);
4337
+ this.circumference = 0;
4338
+ }
4339
+ static {
4340
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
4341
+ _value_decorators = [snice.property({ type: Number, })];
4342
+ _max_decorators = [snice.property({ type: Number, })];
4343
+ _variant_decorators = [snice.property({})];
4344
+ _size_decorators = [snice.property({})];
4345
+ _color_decorators = [snice.property({})];
4346
+ _indeterminate_decorators = [snice.property({ type: Boolean, })];
4347
+ _showLabel_decorators = [snice.property({ type: Boolean, attribute: 'show-label', })];
4348
+ _label_decorators = [snice.property({})];
4349
+ _striped_decorators = [snice.property({ type: Boolean, })];
4350
+ _animated_decorators = [snice.property({ type: Boolean, })];
4351
+ _thickness_decorators = [snice.property({ type: Number, })];
4352
+ _progressBar_decorators = [snice.query('.progress__bar')];
4353
+ _circleBar_decorators = [snice.query('.progress__circle-bar')];
4354
+ _labelElement_decorators = [snice.query('.progress__label')];
4355
+ _circleLabelElement_decorators = [snice.query('.progress__circle-label')];
4356
+ _render_decorators = [snice.render()];
4357
+ _styles_decorators = [snice.styles()];
4358
+ _handleProgressChange_decorators = [snice.watch('value', 'max', 'indeterminate', { immediate: false })];
4359
+ _handleThicknessChange_decorators = [snice.watch('thickness')];
4360
+ _handleColorChange_decorators = [snice.watch('color')];
4361
+ __esDecorate(this, null, _render_decorators, { kind: "method", name: "render", static: false, private: false, access: { has: obj => "render" in obj, get: obj => obj.render }, metadata: _metadata }, null, _instanceExtraInitializers);
4362
+ __esDecorate(this, null, _styles_decorators, { kind: "method", name: "styles", static: false, private: false, access: { has: obj => "styles" in obj, get: obj => obj.styles }, metadata: _metadata }, null, _instanceExtraInitializers);
4363
+ __esDecorate(this, null, _handleProgressChange_decorators, { kind: "method", name: "handleProgressChange", static: false, private: false, access: { has: obj => "handleProgressChange" in obj, get: obj => obj.handleProgressChange }, metadata: _metadata }, null, _instanceExtraInitializers);
4364
+ __esDecorate(this, null, _handleThicknessChange_decorators, { kind: "method", name: "handleThicknessChange", static: false, private: false, access: { has: obj => "handleThicknessChange" in obj, get: obj => obj.handleThicknessChange }, metadata: _metadata }, null, _instanceExtraInitializers);
4365
+ __esDecorate(this, null, _handleColorChange_decorators, { kind: "method", name: "handleColorChange", static: false, private: false, access: { has: obj => "handleColorChange" in obj, get: obj => obj.handleColorChange }, metadata: _metadata }, null, _instanceExtraInitializers);
4366
+ __esDecorate(null, null, _value_decorators, { kind: "field", name: "value", static: false, private: false, access: { has: obj => "value" in obj, get: obj => obj.value, set: (obj, value) => { obj.value = value; } }, metadata: _metadata }, _value_initializers, _value_extraInitializers);
4367
+ __esDecorate(null, null, _max_decorators, { kind: "field", name: "max", static: false, private: false, access: { has: obj => "max" in obj, get: obj => obj.max, set: (obj, value) => { obj.max = value; } }, metadata: _metadata }, _max_initializers, _max_extraInitializers);
4368
+ __esDecorate(null, null, _variant_decorators, { kind: "field", name: "variant", static: false, private: false, access: { has: obj => "variant" in obj, get: obj => obj.variant, set: (obj, value) => { obj.variant = value; } }, metadata: _metadata }, _variant_initializers, _variant_extraInitializers);
4369
+ __esDecorate(null, null, _size_decorators, { kind: "field", name: "size", static: false, private: false, access: { has: obj => "size" in obj, get: obj => obj.size, set: (obj, value) => { obj.size = value; } }, metadata: _metadata }, _size_initializers, _size_extraInitializers);
4370
+ __esDecorate(null, null, _color_decorators, { kind: "field", name: "color", static: false, private: false, access: { has: obj => "color" in obj, get: obj => obj.color, set: (obj, value) => { obj.color = value; } }, metadata: _metadata }, _color_initializers, _color_extraInitializers);
4371
+ __esDecorate(null, null, _indeterminate_decorators, { kind: "field", name: "indeterminate", static: false, private: false, access: { has: obj => "indeterminate" in obj, get: obj => obj.indeterminate, set: (obj, value) => { obj.indeterminate = value; } }, metadata: _metadata }, _indeterminate_initializers, _indeterminate_extraInitializers);
4372
+ __esDecorate(null, null, _showLabel_decorators, { kind: "field", name: "showLabel", static: false, private: false, access: { has: obj => "showLabel" in obj, get: obj => obj.showLabel, set: (obj, value) => { obj.showLabel = value; } }, metadata: _metadata }, _showLabel_initializers, _showLabel_extraInitializers);
4373
+ __esDecorate(null, null, _label_decorators, { kind: "field", name: "label", static: false, private: false, access: { has: obj => "label" in obj, get: obj => obj.label, set: (obj, value) => { obj.label = value; } }, metadata: _metadata }, _label_initializers, _label_extraInitializers);
4374
+ __esDecorate(null, null, _striped_decorators, { kind: "field", name: "striped", static: false, private: false, access: { has: obj => "striped" in obj, get: obj => obj.striped, set: (obj, value) => { obj.striped = value; } }, metadata: _metadata }, _striped_initializers, _striped_extraInitializers);
4375
+ __esDecorate(null, null, _animated_decorators, { kind: "field", name: "animated", static: false, private: false, access: { has: obj => "animated" in obj, get: obj => obj.animated, set: (obj, value) => { obj.animated = value; } }, metadata: _metadata }, _animated_initializers, _animated_extraInitializers);
4376
+ __esDecorate(null, null, _thickness_decorators, { kind: "field", name: "thickness", static: false, private: false, access: { has: obj => "thickness" in obj, get: obj => obj.thickness, set: (obj, value) => { obj.thickness = value; } }, metadata: _metadata }, _thickness_initializers, _thickness_extraInitializers);
4377
+ __esDecorate(null, null, _progressBar_decorators, { kind: "field", name: "progressBar", static: false, private: false, access: { has: obj => "progressBar" in obj, get: obj => obj.progressBar, set: (obj, value) => { obj.progressBar = value; } }, metadata: _metadata }, _progressBar_initializers, _progressBar_extraInitializers);
4378
+ __esDecorate(null, null, _circleBar_decorators, { kind: "field", name: "circleBar", static: false, private: false, access: { has: obj => "circleBar" in obj, get: obj => obj.circleBar, set: (obj, value) => { obj.circleBar = value; } }, metadata: _metadata }, _circleBar_initializers, _circleBar_extraInitializers);
4379
+ __esDecorate(null, null, _labelElement_decorators, { kind: "field", name: "labelElement", static: false, private: false, access: { has: obj => "labelElement" in obj, get: obj => obj.labelElement, set: (obj, value) => { obj.labelElement = value; } }, metadata: _metadata }, _labelElement_initializers, _labelElement_extraInitializers);
4380
+ __esDecorate(null, null, _circleLabelElement_decorators, { kind: "field", name: "circleLabelElement", static: false, private: false, access: { has: obj => "circleLabelElement" in obj, get: obj => obj.circleLabelElement, set: (obj, value) => { obj.circleLabelElement = value; } }, metadata: _metadata }, _circleLabelElement_initializers, _circleLabelElement_extraInitializers);
4381
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
4382
+ _classThis = _classDescriptor.value;
4383
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
4384
+ __runInitializers(_classThis, _classExtraInitializers);
4385
+ }
4386
+ render() {
4387
+ if (this.variant === 'circular') {
4388
+ return this.renderCircular();
4389
+ }
4390
+ return this.renderLinear();
4391
+ }
4392
+ styles() {
4393
+ return snice.css `${cssContent$f}`;
4394
+ }
4395
+ renderLinear() {
4396
+ const percentage = this.getPercentage();
4397
+ const labelText = this.label || `${Math.round(percentage)}%`;
4398
+ const progressClass = `progress progress--linear${this.showLabel ? ' progress--with-label' : ''}`;
4399
+ const barStyle = this.indeterminate ? '' : `width: ${percentage}%`;
4400
+ return snice.html /*html*/ `
4401
+ <div part="base" class="${progressClass}"
4402
+ role="progressbar"
4403
+ aria-valuenow="${this.value}"
4404
+ aria-valuemin="0"
4405
+ aria-valuemax="${this.max}"
4406
+ aria-label="${labelText}">
4407
+ <div part="bar" class="progress__bar" style="${barStyle}">
4408
+ <if ${this.showLabel}>
4409
+ <span part="label" class="progress__label">${labelText}</span>
4410
+ </if>
4411
+ </div>
4412
+ </div>
4413
+ `;
4414
+ }
4415
+ renderCircular() {
4416
+ const size = this.getSizeValue();
4417
+ const strokeWidth = this.thickness;
4418
+ const radius = (size - strokeWidth * 2) / 2;
4419
+ const circumference = radius * 2 * Math.PI;
4420
+ const percentage = this.getPercentage();
4421
+ const offset = this.indeterminate ? 0 : circumference - (percentage / 100) * circumference;
4422
+ const labelText = this.label || `${Math.round(percentage)}%`;
4423
+ this.radius = radius;
4424
+ this.circumference = circumference;
4425
+ const viewBox = `0 0 ${size} ${size}`;
4426
+ const centerX = size / 2;
4427
+ const centerY = size / 2;
4428
+ return snice.html /*html*/ `
4429
+ <div part="base" class="progress progress--circular"
4430
+ role="progressbar"
4431
+ aria-valuenow="${this.value}"
4432
+ aria-valuemin="0"
4433
+ aria-valuemax="${this.max}"
4434
+ aria-label="${labelText}">
4435
+ <svg part="circle" class="progress__circle" viewBox="${viewBox}">
4436
+ <circle
4437
+ part="circle-bg"
4438
+ class="progress__circle-bg"
4439
+ cx="${centerX}"
4440
+ cy="${centerY}"
4441
+ r="${radius}"
4442
+ />
4443
+ <circle
4444
+ part="circle-bar"
4445
+ class="progress__circle-bar"
4446
+ cx="${centerX}"
4447
+ cy="${centerY}"
4448
+ r="${radius}"
4449
+ stroke-dasharray="${circumference}"
4450
+ stroke-dashoffset="${offset}"
4451
+ />
4452
+ </svg>
4453
+ <if ${this.showLabel && !this.indeterminate}>
4454
+ <div part="label" class="progress__circle-label">${labelText}</div>
4455
+ </if>
4456
+ </div>
4457
+ `;
4458
+ }
4459
+ handleProgressChange() {
4460
+ this.dispatchProgressChange();
4461
+ }
4462
+ handleThicknessChange() {
4463
+ if (this.variant === 'circular') {
4464
+ this.style.setProperty('--progress-spinner-stroke', `${this.thickness}px`);
4465
+ }
4466
+ }
4467
+ handleColorChange() {
4468
+ const semanticColors = ['primary', 'success', 'warning', 'error', 'info'];
4469
+ if (!semanticColors.includes(this.color)) {
4470
+ // Custom color value - apply directly via CSS variable
4471
+ this.style.setProperty('--progress-color', this.color);
4472
+ }
4473
+ else {
4474
+ // Semantic color - remove inline style, let CSS handle it
4475
+ this.style.removeProperty('--progress-color');
4476
+ }
4477
+ }
4478
+ getPercentage() {
4479
+ if (this.indeterminate)
4480
+ return 0;
4481
+ if (this.max === 0)
4482
+ return 0;
4483
+ return Math.min(100, Math.max(0, (this.value / this.max) * 100));
4484
+ }
4485
+ setProgress(value, max) {
4486
+ this.value = value;
4487
+ if (max !== undefined) {
4488
+ this.max = max;
4489
+ }
4490
+ }
4491
+ getSizeValue() {
4492
+ switch (this.size) {
4493
+ case 'small': return 24;
4494
+ case 'large': return 56;
4495
+ case 'xl': return 80;
4496
+ case 'xxl': return 120;
4497
+ case 'xxxl': return 160;
4498
+ default: return 40;
4499
+ }
4500
+ }
4501
+ dispatchProgressChange() {
4502
+ this.dispatchEvent(new CustomEvent('progress-change', {
4503
+ bubbles: true,
4504
+ composed: true,
4505
+ detail: {
4506
+ value: this.value,
4507
+ max: this.max,
4508
+ percentage: this.getPercentage(),
4509
+ indeterminate: this.indeterminate
4510
+ }
4511
+ }));
4512
+ }
4513
+ });
4514
+ return _classThis;
4515
+ })();
4516
+
4261
4517
  var cssContent$e = ":host([hidden]){display:none}:host{display:block;width:100%;font-size:.875rem;line-height:1.25rem;color:var(--snice-color-text,rgb(23 23 23))}.cell-content{width:100%;min-height:1.25rem;word-wrap:break-word}:host([align=left]){text-align:left}:host([align=center]){text-align:center}:host([align=right]){text-align:right}:host([type=accounting]),:host([type=currency]),:host([type=fraction]),:host([type=number]),:host([type=percent]),:host([type=scientific]){font-variant-numeric:tabular-nums;text-align:right}:host([type=date]){font-variant-numeric:tabular-nums}:host([type=boolean]){text-align:center;font-size:1rem}.boolean--true{color:var(--snice-color-success,rgb(22 163 74))}.boolean--false{color:var(--snice-color-danger,rgb(220 38 38))}:host([type=rating]){text-align:center}:host([type=sparkline]){text-align:center}.cell-content--text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cell-content--text[data-multiline=true]{white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word}.cell-content--date,.cell-content--number{font-feature-settings:'tnum'}:host([type=boolean]) .cell-content{display:flex;align-items:center;justify-content:center}:host([type=rating]) .cell-content{display:flex;align-items:center;justify-content:center;gap:.125rem;container-type:inline-size}:host([type=rating]) snice-rating{--rating-size:clamp(0.55rem, 16cqi, 1rem)}.cell-content--progress{width:100%;min-width:4rem}.cell-content--sparkline{display:flex;align-items:center;justify-content:center;min-width:3rem}:host(.positive){color:var(--snice-color-success,rgb(22 163 74))}:host(.negative){color:var(--snice-color-danger,rgb(220 38 38))}:host(.warning){color:var(--snice-color-warning,rgb(234 179 8));background-color:rgb(254 249 195 / .3)}:host(.danger){color:var(--snice-color-danger,rgb(220 38 38));background-color:rgb(254 226 226 / .3)}:host(.success){color:var(--snice-color-success,rgb(22 163 74));background-color:rgb(220 252 231 / .3)}:host(.info){color:var(--snice-color-primary,rgb(37 99 235));background-color:rgb(219 234 254 / .3)}:host(.date--today){font-weight:var(--snice-font-weight-semibold,600);color:var(--snice-color-primary,rgb(37 99 235))}:host(.date--past){color:var(--snice-color-text-secondary,rgb(82 82 82))}:host(.date--future){color:var(--snice-color-success,rgb(22 163 74))}:host(.number--negative.number--negative-red){color:var(--snice-color-danger,rgb(220 38 38))}:host(.number--positive.number--highlighted){color:var(--snice-color-success,rgb(22 163 74));font-weight:var(--snice-font-weight-semibold,600)}:host(.number--zero){color:var(--snice-color-text-secondary,rgb(82 82 82))}:host([truncate]) .cell-content{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host([wrap]) .cell-content{white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word}:host(:not([value])) .cell-content,:host([value=\"\"]) .cell-content{color:var(--snice-color-text-secondary,rgb(82 82 82))}:host(.loading){position:relative;overflow:hidden}:host(.loading) .cell-content{opacity:.6}:host(.loading)::after{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(90deg,transparent,var(--snice-color-surface,rgb(255 255 255 / .6)),transparent);animation:1.5s infinite cell-shimmer}@keyframes cell-shimmer{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}:host(:focus-within){outline:var(--snice-focus-ring-width,2px) solid var(--snice-focus-ring-color,rgb(37 99 235 / .5));outline-offset:-2px}.cell-content button,.cell-content input,.cell-content select{font-size:inherit;line-height:inherit}.cell-content a{color:var(--snice-color-primary,rgb(37 99 235));text-decoration:none}.cell-content a:hover{text-decoration:underline}@media (max-width:768px){:host{font-size:.75rem;line-height:1rem}:host([type=boolean]){font-size:.875rem}}@media (prefers-reduced-motion:reduce){:host(.loading)::after{animation:none}}@media (prefers-contrast:high){:host(.positive){background-color:rgb(220 252 231 / .3)}:host(.negative){background-color:rgb(254 226 226 / .3)}.cell-content a{text-decoration:underline}}@media print{:host{font-size:.75rem}:host(.loading)::after{display:none}.cell-content a{text-decoration:underline}}";
4262
4518
 
4263
4519
  const presentationProperties = [
@@ -4268,6 +4524,37 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
4268
4524
  'fontSize',
4269
4525
  'textDecoration',
4270
4526
  ];
4527
+ /** The alignment a column type renders with when the column declares none. */
4528
+ function defaultCellAlign(type) {
4529
+ if (['number', 'currency', 'percent', 'percentage', 'duration', 'filesize',
4530
+ 'accounting', 'scientific', 'fraction'].includes(type || ''))
4531
+ return 'right';
4532
+ if (['boolean', 'rating', 'image'].includes(type || ''))
4533
+ return 'center';
4534
+ return 'left';
4535
+ }
4536
+ /**
4537
+ * The value a typed cell holds when the row has nothing to show — the row field
4538
+ * is null, or the row never carried the field at all.
4539
+ *
4540
+ * Each typed cell owns its value semantics: `false` for boolean, `0` for
4541
+ * rating/progress/duration/filesize, `null` for JSON, `''` for the text-shaped
4542
+ * rest. Handing every empty value the same `''` instead lets the cell's typed
4543
+ * attribute converter re-read that empty string as a value of the wrong
4544
+ * meaning — `''` on a Boolean property is an attribute that is *present*, i.e.
4545
+ * `true`, and `''` on an Object property parses to `{}`.
4546
+ */
4547
+ function emptyCellValue(type) {
4548
+ switch (type) {
4549
+ case 'boolean': return false;
4550
+ case 'rating':
4551
+ case 'progress':
4552
+ case 'duration':
4553
+ case 'filesize': return 0;
4554
+ case 'json': return null;
4555
+ default: return '';
4556
+ }
4557
+ }
4271
4558
  const installed = new WeakMap();
4272
4559
  const appliedClasses = new WeakMap();
4273
4560
  /**
@@ -4281,10 +4568,9 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
4281
4568
  * fallback, so `formatter` wins. `formatterOverride` says the cell does NOT
4282
4569
  * apply `column.formatter` itself, so this module owns both formatters for it.
4283
4570
  *
4284
- * A host marked `formatted` was handed its display text as `value` by the
4285
- * table, which already resolved the pipeline. Its content is that text
4286
- * verbatim the marker exists so a rating/progress/link/tag cell, which
4287
- * renders structure rather than text, still shows the formatted string.
4571
+ * Table-rendered cells never arrive here with a formatter: `createCellElement`
4572
+ * resolves the pipeline once and hands the text cell the formatted string. This
4573
+ * path serves the declarative and standalone cells, which own a full column.
4288
4574
  */
4289
4575
  function installCellPresentation(host, formatterOverride = false) {
4290
4576
  if (installed.has(host))
@@ -4337,12 +4623,8 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
4337
4623
  const formatter = column.formatter
4338
4624
  ? (formatterOverride ? column.formatter : null)
4339
4625
  : column.valueFormatter;
4340
- // Pre-formatted hosts read the attribute, not the property: a cell whose
4341
- // value property is typed (boolean, number) coerces the display string.
4342
- const display = host.hasAttribute('formatted')
4343
- ? (host.getAttribute('value') ?? '')
4344
- : (formatter ? String(formatter(host.value, host.rowData) ?? '') : null);
4345
- if (display !== null) {
4626
+ if (formatter) {
4627
+ const display = String(formatter(host.value, host.rowData) ?? '');
4346
4628
  const content = host.shadowRoot?.querySelector('[part~="content"]');
4347
4629
  if (content && (content.textContent !== display || content.childElementCount > 0)) {
4348
4630
  content.textContent = display;
@@ -4363,6 +4645,82 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
4363
4645
  queueMicrotask(sync);
4364
4646
  }
4365
4647
 
4648
+ /**
4649
+ * Date parsing for the table's cell layer.
4650
+ *
4651
+ * A column of type `date` usually carries calendar dates: `'2026-03-15'` names
4652
+ * a day, not an instant. `new Date('2026-03-15')` nevertheless resolves the
4653
+ * ISO date-only forms as UTC midnight (ECMA-262 21.4.3.2), so every consumer
4654
+ * in a negative-offset zone formatted the day BEFORE the authored one, while
4655
+ * an `<input type="date">` editor holding the same string showed the authored
4656
+ * day. Display, export, and filtering must all agree with the string the
4657
+ * application wrote.
4658
+ *
4659
+ * The rule: a value that carries NO time is a calendar day and is read at
4660
+ * local midnight; anything that carries a time (or an offset, or is already a
4661
+ * `Date`/epoch number) is an instant and is left to the platform.
4662
+ */
4663
+ /** ISO calendar dates with no time part: `YYYY`, `YYYY-MM`, `YYYY-MM-DD`. */
4664
+ const DATE_ONLY = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/;
4665
+ /**
4666
+ * Parse a cell value into a `Date`, or `null` when there is nothing to show.
4667
+ *
4668
+ * `null` covers the empty cases (`null`, `undefined`, `''`) and unparseable
4669
+ * input alike, so callers fall back to the raw value rather than rendering
4670
+ * "Invalid Date" or the epoch.
4671
+ */
4672
+ function parseCellDate(value) {
4673
+ if (value === null || value === undefined || value === '')
4674
+ return null;
4675
+ if (value instanceof Date) {
4676
+ return Number.isNaN(value.getTime()) ? null : value;
4677
+ }
4678
+ if (typeof value === 'number') {
4679
+ const fromEpoch = new Date(value);
4680
+ return Number.isNaN(fromEpoch.getTime()) ? null : fromEpoch;
4681
+ }
4682
+ if (typeof value === 'string') {
4683
+ const trimmed = value.trim();
4684
+ if (trimmed === '')
4685
+ return null;
4686
+ const calendarDay = DATE_ONLY.exec(trimmed);
4687
+ if (calendarDay) {
4688
+ const year = Number(calendarDay[1]);
4689
+ const month = calendarDay[2] === undefined ? 1 : Number(calendarDay[2]);
4690
+ const day = calendarDay[3] === undefined ? 1 : Number(calendarDay[3]);
4691
+ // Local midnight: the same calendar day in every timezone.
4692
+ const local = new Date(year, month - 1, day);
4693
+ // Years 0-99 mean themselves, not 1900+n.
4694
+ if (year >= 0 && year <= 99)
4695
+ local.setFullYear(year, month - 1, day);
4696
+ if (Number.isNaN(local.getTime()))
4697
+ return null;
4698
+ // `new Date(2026, 12, 1)` rolls over to 2027 instead of rejecting, so a
4699
+ // typo like '2026-13-01' or '2026-02-30' would render as a confident but
4700
+ // WRONG day. The platform calls those strings invalid; so do we, and the
4701
+ // caller falls back to showing the raw value.
4702
+ if (local.getFullYear() !== year ||
4703
+ local.getMonth() !== month - 1 ||
4704
+ local.getDate() !== day) {
4705
+ return null;
4706
+ }
4707
+ return local;
4708
+ }
4709
+ const parsed = new Date(trimmed);
4710
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
4711
+ }
4712
+ const coerced = new Date(value);
4713
+ return Number.isNaN(coerced.getTime()) ? null : coerced;
4714
+ }
4715
+ /**
4716
+ * Midnight on the calendar day a value falls on, in local time. Used by
4717
+ * day-granularity comparisons (today/past/future classification) so an
4718
+ * instant is never bumped across a day boundary by the UTC offset.
4719
+ */
4720
+ function startOfLocalDay(date) {
4721
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
4722
+ }
4723
+
4366
4724
  (() => {
4367
4725
  let _classDecorators = [snice.element('snice-cell')];
4368
4726
  let _classDescriptor;
@@ -4449,7 +4807,14 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
4449
4807
  this.applyConditionalFormatting();
4450
4808
  }
4451
4809
  applyAlignment() {
4452
- this.style.textAlign = this.align;
4810
+ // snice-cell.css already encodes each type's default alignment
4811
+ // (:host([type="accounting"]) and friends are right-aligned, boolean/rating
4812
+ // centred). Writing an inline text-align unconditionally OVERRODE that, so a
4813
+ // standalone accounting figure sat left-aligned beside a right-aligned
4814
+ // currency cell — and the accounting format's alignment padding meant
4815
+ // nothing. Only an alignment the caller actually declared wins.
4816
+ const declared = this.getAttribute('align');
4817
+ this.style.textAlign = declared ?? '';
4453
4818
  }
4454
4819
  applyConditionalFormatting() {
4455
4820
  if (!this.column)
@@ -4580,24 +4945,60 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
4580
4945
  return String(this.value);
4581
4946
  return `${num.toFixed(format.decimals ?? 2)}%`;
4582
4947
  }
4948
+ /**
4949
+ * Accounting notation is currency notation: symbol, grouped digits, and
4950
+ * negatives in parentheses instead of a minus sign. It shares its defaults
4951
+ * with `formatCurrency()` on purpose — an `accounting` cell and a `currency`
4952
+ * cell showing the same amount must not disagree about what money looks
4953
+ * like. `numberFormat` still steers decimals/grouping (its historic role
4954
+ * here), `currencyFormat` steers the symbol and locale, and an explicit
4955
+ * `numberFormat.prefix` opts out of the symbol entirely for ledgers that
4956
+ * label their own columns.
4957
+ */
4583
4958
  formatAccounting() {
4584
4959
  const format = this.column.numberFormat || {};
4960
+ const currency = this.column.currencyFormat || {};
4585
4961
  const num = Number(this.value);
4586
4962
  if (isNaN(num))
4587
4963
  return String(this.value);
4588
- let formatted = Math.abs(num).toFixed(format.decimals ?? 2);
4589
- if (format.thousandsSeparator) {
4590
- const parts = formatted.split('.');
4591
- parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
4592
- formatted = parts.join('.');
4593
- }
4594
- if (num < 0) {
4595
- formatted = `(${formatted})`;
4964
+ const decimals = format.decimals ?? currency.decimals ?? 2;
4965
+ const useGrouping = format.thousandsSeparator ?? currency.thousandsSeparator ?? true;
4966
+ const magnitude = Math.abs(num);
4967
+ const options = {
4968
+ useGrouping,
4969
+ minimumFractionDigits: decimals,
4970
+ maximumFractionDigits: decimals,
4971
+ };
4972
+ // An author-supplied prefix/suffix is the caller naming the unit; adding a
4973
+ // currency symbol on top would print it twice.
4974
+ const symbolIsCallers = !!format.prefix || !!format.suffix;
4975
+ if (!symbolIsCallers) {
4976
+ options.style = 'currency';
4977
+ options.currency = currency.currency || 'USD';
4978
+ options.currencyDisplay = currency.currencyDisplay || currency.display || 'symbol';
4979
+ }
4980
+ let formatted;
4981
+ try {
4982
+ formatted = new Intl.NumberFormat(currency.locale || 'en-US', options).format(magnitude);
4596
4983
  }
4597
- else {
4598
- formatted = ` ${formatted} `;
4984
+ catch {
4985
+ formatted = magnitude.toFixed(decimals);
4986
+ if (useGrouping) {
4987
+ const parts = formatted.split('.');
4988
+ parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
4989
+ formatted = parts.join('.');
4990
+ }
4991
+ if (!symbolIsCallers)
4992
+ formatted = `$${formatted}`;
4599
4993
  }
4600
- return formatted;
4994
+ if (format.prefix)
4995
+ formatted = format.prefix + formatted;
4996
+ if (format.suffix)
4997
+ formatted = formatted + format.suffix;
4998
+ // The trailing space is the format's whole reason for existing: it keeps a
4999
+ // positive row's last digit under the ")" of the row above in a
5000
+ // right-aligned column. A normal space collapses in HTML, so pad with NBSP.
5001
+ return num < 0 ? `(${formatted})` : `${formatted}\u00a0`;
4601
5002
  }
4602
5003
  formatScientific() {
4603
5004
  const format = this.column.numberFormat || {};
@@ -4639,8 +5040,10 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
4639
5040
  }
4640
5041
  formatDate() {
4641
5042
  const format = this.column.dateFormat || {};
4642
- const date = new Date(this.value);
4643
- if (isNaN(date.getTime()))
5043
+ // Calendar days are read at local midnight (see table-date.ts) so this
5044
+ // generic cell shows the same day as `snice-cell-date` and as the editor.
5045
+ const date = parseCellDate(this.value);
5046
+ if (!date)
4644
5047
  return String(this.value);
4645
5048
  if (format.customFormat) {
4646
5049
  // Simple custom format implementation
@@ -5279,7 +5682,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
5279
5682
  _styles_decorators = [snice.styles()];
5280
5683
  _init_decorators = [snice.ready()];
5281
5684
  _updateAlignment_decorators = [snice.watch('align')];
5282
- _updateContent_decorators = [snice.watch('value', 'column')];
5685
+ _updateContent_decorators = [snice.watch('value', 'column', 'dateFormat', 'customFormat', 'locale', 'relativeTime', 'showTime')];
5283
5686
  __esDecorate(this, null, _render_decorators, { kind: "method", name: "render", static: false, private: false, access: { has: obj => "render" in obj, get: obj => obj.render }, metadata: _metadata }, null, _instanceExtraInitializers);
5284
5687
  __esDecorate(this, null, _styles_decorators, { kind: "method", name: "styles", static: false, private: false, access: { has: obj => "styles" in obj, get: obj => obj.styles }, metadata: _metadata }, null, _instanceExtraInitializers);
5285
5688
  __esDecorate(this, null, _init_decorators, { kind: "method", name: "init", static: false, private: false, access: { has: obj => "init" in obj, get: obj => obj.init }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -5323,6 +5726,9 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
5323
5726
  updateAlignment() {
5324
5727
  this.applyAlignment();
5325
5728
  }
5729
+ // Every input the formatter reads, not just the value: a late
5730
+ // `custom-format` / `locale` / `show-time` change must repaint the day it
5731
+ // describes, or the cell keeps showing the format it happened to be born with.
5326
5732
  updateContent() {
5327
5733
  if (this.contentElement) {
5328
5734
  const formattedValue = this.formatDateValue();
@@ -5339,8 +5745,11 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
5339
5745
  if (this.column.formatter) {
5340
5746
  return this.column.formatter(this.value, this.rowData);
5341
5747
  }
5342
- const date = new Date(this.value);
5343
- if (isNaN(date.getTime())) {
5748
+ // A date-only value names a calendar day, so it is read at LOCAL midnight
5749
+ // (see table-date.ts). Parsing it with `new Date` would place it at UTC
5750
+ // midnight and render the previous day for every negative UTC offset.
5751
+ const date = parseCellDate(this.value);
5752
+ if (!date) {
5344
5753
  return String(this.value);
5345
5754
  }
5346
5755
  // Show relative time if enabled
@@ -5427,13 +5836,18 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
5427
5836
  }
5428
5837
  }
5429
5838
  getDateStyles() {
5430
- const date = new Date(this.value);
5431
- const now = new Date();
5432
- const isToday = date.toDateString() === now.toDateString();
5433
- const isPast = date < now;
5434
- const isFuture = date > now;
5839
+ const date = parseCellDate(this.value);
5435
5840
  // Apply CSS classes instead of direct styles
5436
5841
  this.classList.remove('date--today', 'date--past', 'date--future');
5842
+ if (!date)
5843
+ return '';
5844
+ // Day granularity: a calendar day is today, past, or future as a whole —
5845
+ // the wall-clock time of day never decides which.
5846
+ const day = startOfLocalDay(date).getTime();
5847
+ const today = startOfLocalDay(new Date()).getTime();
5848
+ const isToday = day === today;
5849
+ const isPast = day < today;
5850
+ const isFuture = day > today;
5437
5851
  if (isToday) {
5438
5852
  this.classList.add('date--today');
5439
5853
  }
@@ -7128,7 +7542,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
7128
7542
  return _classThis;
7129
7543
  })();
7130
7544
 
7131
- var cssContent$a = ":host([hidden]){display:none}:host{color:var(--snice-color-text,rgb(23 23 23));display:flex;align-items:center;min-height:2.5rem;height:2.5rem;min-width:6.25rem;flex:1;font-size:.875rem;line-height:1.25rem}.cell-content{width:100%;min-width:6.25rem;min-height:1.25rem;display:flex;align-items:center;gap:.5rem}.status-dot{width:.5rem;height:.5rem;border-radius:50%;flex-shrink:0;position:relative}.status-dot--online{background-color:var(--snice-color-success,rgb(22 163 74));box-shadow:0 0 0 2px rgb(22 163 74 / .2)}.status-dot--offline{background-color:rgb(156 163 175);box-shadow:0 0 0 2px rgb(107 114 128 / .2)}.status-dot--busy{background-color:var(--snice-color-danger,rgb(220 38 38));box-shadow:0 0 0 2px rgb(220 38 38 / .2)}.status-dot--away{background-color:var(--snice-color-warning,rgb(234 179 8));box-shadow:0 0 0 2px rgb(234 179 8 / .2)}.status-dot--custom{background-color:var(--snice-color-primary,rgb(37 99 235));box-shadow:0 0 0 2px rgb(37 99 235 / .2)}.status-dot--online::after{content:'';position:absolute;top:-2px;left:-2px;right:-2px;bottom:-2px;border-radius:50%;border:2px solid var(--snice-color-success,rgb(22 163 74));animation:2s ease-in-out infinite status-pulse;opacity:0}@keyframes status-pulse{0%{transform:scale(1);opacity:1}100%{transform:scale(1.5);opacity:0}}.status-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.status-label--online{color:var(--snice-color-success,rgb(22 163 74))}.status-label--offline{color:var(--snice-color-text-secondary,rgb(82 82 82))}.status-label--busy{color:var(--snice-color-danger,rgb(220 38 38))}.status-label--away{color:var(--snice-color-warning,rgb(234 179 8))}.status-label--custom{color:var(--snice-color-text,rgb(23 23 23))}:host([align=left]) .cell-content{justify-content:flex-start}:host([align=center]) .cell-content{justify-content:center}:host([align=right]) .cell-content{justify-content:flex-end}@media (max-width:768px){:host{color:var(--snice-color-text,rgb(23 23 23));font-size:.75rem;line-height:1rem}.status-dot{width:.375rem;height:.375rem}}@media (prefers-reduced-motion:reduce){.status-dot--online::after{animation:none}}@media print{.status-dot{border:1px solid var(--snice-color-text,rgb(23 23 23))}.status-dot--online{background-color:var(--snice-color-surface,rgb(255 255 255));border-color:var(--snice-color-success,rgb(22 163 74))}.status-dot--offline{background-color:var(--snice-color-surface,rgb(255 255 255));border-color:var(--snice-color-neutral,rgb(107 114 128))}.status-dot--busy{background-color:var(--snice-color-surface,rgb(255 255 255));border-color:var(--snice-color-danger,rgb(220 38 38))}.status-dot--away{background-color:var(--snice-color-surface,rgb(255 255 255));border-color:var(--snice-color-warning,rgb(234 179 8))}}";
7545
+ var cssContent$a = ":host([hidden]){display:none}:host{color:var(--snice-color-text,rgb(23 23 23));display:flex;align-items:center;min-height:2.5rem;height:2.5rem;min-width:var(--snice-table-cell-min-width,6.25rem);flex:1;font-size:.875rem;line-height:1.25rem}.cell-content{width:100%;min-width:var(--snice-table-cell-min-width,6.25rem);min-height:1.25rem;display:flex;align-items:center;gap:.5rem}.status-dot{width:.5rem;height:.5rem;border-radius:50%;flex-shrink:0;position:relative}.status-dot--online{background-color:var(--snice-color-success,rgb(22 163 74));box-shadow:0 0 0 2px rgb(22 163 74 / .2)}.status-dot--offline{background-color:rgb(156 163 175);box-shadow:0 0 0 2px rgb(107 114 128 / .2)}.status-dot--busy{background-color:var(--snice-color-danger,rgb(220 38 38));box-shadow:0 0 0 2px rgb(220 38 38 / .2)}.status-dot--away{background-color:var(--snice-color-warning,rgb(234 179 8));box-shadow:0 0 0 2px rgb(234 179 8 / .2)}.status-dot--custom{background-color:var(--snice-color-primary,rgb(37 99 235));box-shadow:0 0 0 2px rgb(37 99 235 / .2)}.status-dot--online::after{content:'';position:absolute;top:-2px;left:-2px;right:-2px;bottom:-2px;border-radius:50%;border:2px solid var(--snice-color-success,rgb(22 163 74));animation:2s ease-in-out infinite status-pulse;opacity:0}@keyframes status-pulse{0%{transform:scale(1);opacity:1}100%{transform:scale(1.5);opacity:0}}.status-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.status-label--online{color:var(--snice-color-success,rgb(22 163 74))}.status-label--offline{color:var(--snice-color-text-secondary,rgb(82 82 82))}.status-label--busy{color:var(--snice-color-danger,rgb(220 38 38))}.status-label--away{color:var(--snice-color-warning,rgb(234 179 8))}.status-label--custom{color:var(--snice-color-text,rgb(23 23 23))}:host([align=left]) .cell-content{justify-content:flex-start}:host([align=center]) .cell-content{justify-content:center}:host([align=right]) .cell-content{justify-content:flex-end}@media (max-width:768px){:host{color:var(--snice-color-text,rgb(23 23 23));font-size:.75rem;line-height:1rem}.status-dot{width:.375rem;height:.375rem}}@media (prefers-reduced-motion:reduce){.status-dot--online::after{animation:none}}@media print{.status-dot{border:1px solid var(--snice-color-text,rgb(23 23 23))}.status-dot--online{background-color:var(--snice-color-surface,rgb(255 255 255));border-color:var(--snice-color-success,rgb(22 163 74))}.status-dot--offline{background-color:var(--snice-color-surface,rgb(255 255 255));border-color:var(--snice-color-neutral,rgb(107 114 128))}.status-dot--busy{background-color:var(--snice-color-surface,rgb(255 255 255));border-color:var(--snice-color-danger,rgb(220 38 38))}.status-dot--away{background-color:var(--snice-color-surface,rgb(255 255 255));border-color:var(--snice-color-warning,rgb(234 179 8))}}";
7132
7546
 
7133
7547
  (() => {
7134
7548
  let _classDecorators = [snice.element('snice-cell-status')];
@@ -7268,7 +7682,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
7268
7682
  return _classThis;
7269
7683
  })();
7270
7684
 
7271
- var cssContent$9 = ":host([hidden]){display:none}:host{color:var(--snice-color-text,rgb(23 23 23));display:flex;align-items:center;min-height:2.5rem;min-width:6.25rem;flex:1;font-size:.875rem;line-height:1.25rem}.cell-content{width:100%;min-width:6.25rem;min-height:1.25rem}.actions-container{display:inline-flex;gap:.5rem;align-items:center}.action-button{display:inline-flex;align-items:center;gap:.25rem;padding:.25rem .5rem;border:1px solid transparent;border-radius:.25rem;background:0 0;cursor:pointer;transition:.2s;font-size:.875rem;line-height:1}.action-button:hover{background:var(--snice-color-surface-hover,rgb(243 244 246))}.action-button:focus-visible{outline:var(--snice-focus-ring-width,2px) solid var(--snice-focus-ring-color,rgb(37 99 235 / .5));outline-offset:2px}.action-button:active{transform:scale(.95)}.action-button:disabled{opacity:.5;cursor:not-allowed}.action-button:disabled:hover{background:0 0}.action-button--primary{color:var(--snice-color-primary,rgb(37 99 235))}.action-button--primary:hover:not(:disabled){background:rgb(37 99 235 / .1)}.action-button--secondary{color:var(--snice-color-text,rgb(23 23 23))}.action-button--danger{color:var(--snice-color-danger,rgb(220 38 38))}.action-button--danger:hover:not(:disabled){background:rgb(220 38 38 / .1)}.action-button--success{color:var(--snice-color-success,rgb(22 163 74))}.action-button--success:hover:not(:disabled){background:rgb(22 163 74 / .1)}.action-icon{display:inline-flex;align-items:center;font-size:1rem}img.action-icon{width:1rem;height:1rem;object-fit:contain}.action-icon svg{width:1rem;height:1rem}.action-label{font-weight:var(--snice-font-weight-medium,500)}:host([align=left]){justify-content:flex-start}:host([align=center]){justify-content:center}:host([align=right]){justify-content:flex-end}@media (max-width:768px){.action-button{padding:.25rem}.action-label{display:none}}@media print{.actions-container{display:none}}";
7685
+ var cssContent$9 = ":host([hidden]){display:none}:host{color:var(--snice-color-text,rgb(23 23 23));display:flex;align-items:center;min-height:2.5rem;min-width:var(--snice-table-cell-min-width,6.25rem);flex:1;font-size:.875rem;line-height:1.25rem}.cell-content{width:100%;min-width:var(--snice-table-cell-min-width,6.25rem);min-height:1.25rem}.actions-container{display:inline-flex;gap:.5rem;align-items:center}.action-button{display:inline-flex;align-items:center;gap:.25rem;padding:.25rem .5rem;border:1px solid transparent;border-radius:.25rem;background:0 0;cursor:pointer;transition:.2s;font-size:.875rem;line-height:1}.action-button:hover{background:var(--snice-color-surface-hover,rgb(243 244 246))}.action-button:focus-visible{outline:var(--snice-focus-ring-width,2px) solid var(--snice-focus-ring-color,rgb(37 99 235 / .5));outline-offset:2px}.action-button:active{transform:scale(.95)}.action-button:disabled{opacity:.5;cursor:not-allowed}.action-button:disabled:hover{background:0 0}.action-button--primary{color:var(--snice-color-primary,rgb(37 99 235))}.action-button--primary:hover:not(:disabled){background:rgb(37 99 235 / .1)}.action-button--secondary{color:var(--snice-color-text,rgb(23 23 23))}.action-button--danger{color:var(--snice-color-danger,rgb(220 38 38))}.action-button--danger:hover:not(:disabled){background:rgb(220 38 38 / .1)}.action-button--success{color:var(--snice-color-success,rgb(22 163 74))}.action-button--success:hover:not(:disabled){background:rgb(22 163 74 / .1)}.action-icon{display:inline-flex;align-items:center;font-size:1rem}img.action-icon{width:1rem;height:1rem;object-fit:contain}.action-icon svg{width:1rem;height:1rem}.action-label{font-weight:var(--snice-font-weight-medium,500)}:host([align=left]){justify-content:flex-start}:host([align=center]){justify-content:center}:host([align=right]){justify-content:flex-end}@media (max-width:768px){.action-button{padding:.25rem}.action-label{display:none}}@media print{.actions-container{display:none}}";
7272
7686
 
7273
7687
  (() => {
7274
7688
  let _classDecorators = [snice.element('snice-cell-actions')];
@@ -7406,7 +7820,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
7406
7820
  return _classThis;
7407
7821
  })();
7408
7822
 
7409
- var cssContent$8 = ":host([hidden]){display:none}:host{color:var(--snice-color-text,rgb(23 23 23));display:flex;align-items:center;min-height:2.5rem;min-width:6.25rem;flex:1;font-size:.875rem;line-height:1.25rem}.cell-content{width:100%;min-width:6.25rem;min-height:1.25rem}.cell-link{display:inline-flex;align-items:center;gap:.25rem;color:var(--snice-color-primary,rgb(37 99 235));text-decoration:none;transition:color .2s}.cell-link:hover{color:var(--snice-color-primary-hover,rgb(29 78 216));text-decoration:underline}.cell-link:focus-visible{outline:var(--snice-focus-ring-width,2px) solid var(--snice-focus-ring-color,rgb(37 99 235 / .5));outline-offset:2px;border-radius:var(--snice-border-radius-sm,.125rem)}.cell-link:visited{color:var(--snice-color-text-secondary,rgb(107 114 128))}.link-icon{display:inline-flex;align-items:center;font-size:1rem}img.link-icon{width:1rem;height:1rem;object-fit:contain}.link-icon svg{width:1rem;height:1rem}.link-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.external-icon{display:inline-flex;align-items:center;font-size:.75rem;opacity:.7}:host([align=left]){text-align:left;justify-content:flex-start}:host([align=center]){text-align:center;justify-content:center}:host([align=right]){text-align:right;justify-content:flex-end}@media (max-width:768px){:host{color:var(--snice-color-text,rgb(23 23 23));font-size:.75rem;line-height:1rem}}@media print{.cell-link{text-decoration:underline}.cell-link::after{content:\" (\" attr(href) \")\";font-size:.75rem;color:var(--snice-color-text-secondary,rgb(82 82 82))}}";
7823
+ var cssContent$8 = ":host([hidden]){display:none}:host{color:var(--snice-color-text,rgb(23 23 23));display:flex;align-items:center;min-height:2.5rem;min-width:var(--snice-table-cell-min-width,6.25rem);flex:1;font-size:.875rem;line-height:1.25rem}.cell-content{width:100%;min-width:var(--snice-table-cell-min-width,6.25rem);min-height:1.25rem}.cell-link{display:inline-flex;align-items:center;gap:.25rem;color:var(--snice-color-primary,rgb(37 99 235));text-decoration:none;transition:color .2s}.cell-link:hover{color:var(--snice-color-primary-hover,rgb(29 78 216));text-decoration:underline}.cell-link:focus-visible{outline:var(--snice-focus-ring-width,2px) solid var(--snice-focus-ring-color,rgb(37 99 235 / .5));outline-offset:2px;border-radius:var(--snice-border-radius-sm,.125rem)}.cell-link:visited{color:var(--snice-color-text-secondary,rgb(107 114 128))}.link-icon{display:inline-flex;align-items:center;font-size:1rem}img.link-icon{width:1rem;height:1rem;object-fit:contain}.link-icon svg{width:1rem;height:1rem}.link-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.external-icon{display:inline-flex;align-items:center;font-size:.75rem;opacity:.7}:host([align=left]){text-align:left;justify-content:flex-start}:host([align=center]){text-align:center;justify-content:center}:host([align=right]){text-align:right;justify-content:flex-end}@media (max-width:768px){:host{color:var(--snice-color-text,rgb(23 23 23));font-size:.75rem;line-height:1rem}}@media print{.cell-link{text-decoration:underline}.cell-link::after{content:\" (\" attr(href) \")\";font-size:.75rem;color:var(--snice-color-text-secondary,rgb(82 82 82))}}";
7410
7824
 
7411
7825
  (() => {
7412
7826
  let _classDecorators = [snice.element('snice-cell-link')];
@@ -7546,7 +7960,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
7546
7960
  return _classThis;
7547
7961
  })();
7548
7962
 
7549
- var cssContent$7 = ":host([hidden]){display:none}:host{color:var(--snice-color-text,rgb(23 23 23));display:flex;align-items:center;min-height:2.5rem;min-width:6.25rem;flex:1;font-size:.875rem;line-height:1.25rem}.cell-content{width:100%;min-width:6.25rem;min-height:1.25rem;display:flex;align-items:center;gap:.375rem}.email-icon{display:inline-flex;align-items:center;font-size:.875rem;opacity:.7;flex-shrink:0}.email-link{color:var(--snice-color-primary,rgb(37 99 235));text-decoration:none;transition:color .2s;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.email-link:hover{color:var(--snice-color-primary-hover,rgb(29 78 216));text-decoration:underline}.email-link:focus-visible{outline:var(--snice-focus-ring-width,2px) solid var(--snice-focus-ring-color,rgb(37 99 235 / .5));outline-offset:2px;border-radius:var(--snice-border-radius-sm,.125rem)}.email-link:visited{color:var(--snice-color-text-secondary,rgb(107 114 128))}.email-empty{color:var(--snice-color-text-secondary,rgb(82 82 82));font-style:italic}:host([align=left]) .cell-content{justify-content:flex-start}:host([align=center]) .cell-content{justify-content:center}:host([align=right]) .cell-content{justify-content:flex-end}@media (max-width:768px){:host{color:var(--snice-color-text,rgb(23 23 23));font-size:.75rem;line-height:1rem}.email-icon{font-size:.75rem}}@media print{.email-link{text-decoration:underline}.email-link::after{content:\" (\" attr(href) \")\";font-size:.75rem;color:var(--snice-color-text-secondary,rgb(82 82 82))}.email-icon{display:none}}";
7963
+ var cssContent$7 = ":host([hidden]){display:none}:host{color:var(--snice-color-text,rgb(23 23 23));display:flex;align-items:center;min-height:2.5rem;min-width:var(--snice-table-cell-min-width,6.25rem);flex:1;font-size:.875rem;line-height:1.25rem}.cell-content{width:100%;min-width:var(--snice-table-cell-min-width,6.25rem);min-height:1.25rem;display:flex;align-items:center;gap:.375rem}.email-icon{display:inline-flex;align-items:center;font-size:.875rem;opacity:.7;flex-shrink:0}.email-link{color:var(--snice-color-primary,rgb(37 99 235));text-decoration:none;transition:color .2s;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.email-link:hover{color:var(--snice-color-primary-hover,rgb(29 78 216));text-decoration:underline}.email-link:focus-visible{outline:var(--snice-focus-ring-width,2px) solid var(--snice-focus-ring-color,rgb(37 99 235 / .5));outline-offset:2px;border-radius:var(--snice-border-radius-sm,.125rem)}.email-link:visited{color:var(--snice-color-text-secondary,rgb(107 114 128))}.email-empty{color:var(--snice-color-text-secondary,rgb(82 82 82));font-style:italic}:host([align=left]) .cell-content{justify-content:flex-start}:host([align=center]) .cell-content{justify-content:center}:host([align=right]) .cell-content{justify-content:flex-end}@media (max-width:768px){:host{color:var(--snice-color-text,rgb(23 23 23));font-size:.75rem;line-height:1rem}.email-icon{font-size:.75rem}}@media print{.email-link{text-decoration:underline}.email-link::after{content:\" (\" attr(href) \")\";font-size:.75rem;color:var(--snice-color-text-secondary,rgb(82 82 82))}.email-icon{display:none}}";
7550
7964
 
7551
7965
  (() => {
7552
7966
  let _classDecorators = [snice.element('snice-cell-email')];
@@ -7667,7 +8081,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
7667
8081
  return _classThis;
7668
8082
  })();
7669
8083
 
7670
- var cssContent$6 = ":host([hidden]){display:none}:host{color:var(--snice-color-text,rgb(23 23 23));display:flex;align-items:center;min-height:2.5rem;height:2.5rem;min-width:6.25rem;flex:1;font-size:.875rem;line-height:1.25rem}.cell-content{width:100%;min-width:6.25rem;min-height:1.25rem;display:flex;align-items:center;gap:.375rem}.phone-icon{display:inline-flex;align-items:center;font-size:.875rem;opacity:.7;flex-shrink:0}.phone-link{color:var(--snice-color-primary,rgb(37 99 235));text-decoration:none;transition:color .2s;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-variant-numeric:tabular-nums}.phone-link:hover{color:var(--snice-color-primary-hover,rgb(29 78 216));text-decoration:underline}.phone-link:focus-visible{outline:var(--snice-focus-ring-width,2px) solid var(--snice-focus-ring-color,rgb(37 99 235 / .5));outline-offset:2px;border-radius:var(--snice-border-radius-sm,.125rem)}.phone-link:visited{color:var(--snice-color-text-secondary,rgb(107 114 128))}.phone-empty{color:var(--snice-color-text-secondary,rgb(82 82 82));font-style:italic}:host([align=left]) .cell-content{justify-content:flex-start}:host([align=center]) .cell-content{justify-content:center}:host([align=right]) .cell-content{justify-content:flex-end}@media (max-width:768px){:host{color:var(--snice-color-text,rgb(23 23 23));font-size:.75rem;line-height:1rem}.phone-icon{font-size:.75rem}}@media print{.phone-link{text-decoration:underline}.phone-link::after{content:\" (\" attr(href) \")\";font-size:.75rem;color:var(--snice-color-text-secondary,rgb(82 82 82))}.phone-icon{display:none}}";
8084
+ var cssContent$6 = ":host([hidden]){display:none}:host{color:var(--snice-color-text,rgb(23 23 23));display:flex;align-items:center;min-height:2.5rem;height:2.5rem;min-width:var(--snice-table-cell-min-width,6.25rem);flex:1;font-size:.875rem;line-height:1.25rem}.cell-content{width:100%;min-width:var(--snice-table-cell-min-width,6.25rem);min-height:1.25rem;display:flex;align-items:center;gap:.375rem}.phone-icon{display:inline-flex;align-items:center;font-size:.875rem;opacity:.7;flex-shrink:0}.phone-link{color:var(--snice-color-primary,rgb(37 99 235));text-decoration:none;transition:color .2s;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-variant-numeric:tabular-nums}.phone-link:hover{color:var(--snice-color-primary-hover,rgb(29 78 216));text-decoration:underline}.phone-link:focus-visible{outline:var(--snice-focus-ring-width,2px) solid var(--snice-focus-ring-color,rgb(37 99 235 / .5));outline-offset:2px;border-radius:var(--snice-border-radius-sm,.125rem)}.phone-link:visited{color:var(--snice-color-text-secondary,rgb(107 114 128))}.phone-empty{color:var(--snice-color-text-secondary,rgb(82 82 82));font-style:italic}:host([align=left]) .cell-content{justify-content:flex-start}:host([align=center]) .cell-content{justify-content:center}:host([align=right]) .cell-content{justify-content:flex-end}@media (max-width:768px){:host{color:var(--snice-color-text,rgb(23 23 23));font-size:.75rem;line-height:1rem}.phone-icon{font-size:.75rem}}@media print{.phone-link{text-decoration:underline}.phone-link::after{content:\" (\" attr(href) \")\";font-size:.75rem;color:var(--snice-color-text-secondary,rgb(82 82 82))}.phone-icon{display:none}}";
7671
8085
 
7672
8086
  (() => {
7673
8087
  let _classDecorators = [snice.element('snice-cell-phone')];
@@ -7816,7 +8230,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
7816
8230
  return _classThis;
7817
8231
  })();
7818
8232
 
7819
- var cssContent$5 = ":host([hidden]){display:none}:host{color:var(--snice-color-text,rgb(23 23 23));display:flex;align-items:center;min-height:2.5rem;min-width:6.25rem;flex:1;font-size:.875rem;line-height:1.25rem}.cell-content{width:100%;min-width:6.25rem;min-height:1.25rem;display:flex;align-items:center;gap:.5rem}.color-swatch{border:1px solid var(--snice-color-border,rgb(226 226 226));border-radius:.25rem;flex-shrink:0;box-shadow:var(--snice-shadow-xs,0 1px 2px 0 rgb(0 0 0 / .03))}.color-swatch--small{width:1rem;height:1rem}.color-swatch--medium{width:1.5rem;height:1.5rem}.color-swatch--large{width:2rem;height:2rem}.color-text{font-family:monospace;font-size:.875rem;color:var(--snice-color-text,rgb(23 23 23));overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host([align=left]) .cell-content{justify-content:flex-start}:host([align=center]) .cell-content{justify-content:center}:host([align=right]) .cell-content{justify-content:flex-end}@media (max-width:768px){:host{color:var(--snice-color-text,rgb(23 23 23));font-size:.75rem;line-height:1rem}.color-swatch--small{width:.875rem;height:.875rem}.color-swatch--medium{width:1.25rem;height:1.25rem}.color-swatch--large{width:1.75rem;height:1.75rem}.color-text{font-size:.75rem}}@media print{.color-swatch{border:2px solid var(--snice-color-text,rgb(23 23 23))}.color-text{font-size:.75rem}}";
8233
+ var cssContent$5 = ":host([hidden]){display:none}:host{color:var(--snice-color-text,rgb(23 23 23));display:flex;align-items:center;min-height:2.5rem;min-width:var(--snice-table-cell-min-width,6.25rem);flex:1;font-size:.875rem;line-height:1.25rem}.cell-content{width:100%;min-width:var(--snice-table-cell-min-width,6.25rem);min-height:1.25rem;display:flex;align-items:center;gap:.5rem}.color-swatch{border:1px solid var(--snice-color-border,rgb(226 226 226));border-radius:.25rem;flex-shrink:0;box-shadow:var(--snice-shadow-xs,0 1px 2px 0 rgb(0 0 0 / .03))}.color-swatch--small{width:1rem;height:1rem}.color-swatch--medium{width:1.5rem;height:1.5rem}.color-swatch--large{width:2rem;height:2rem}.color-text{font-family:monospace;font-size:.875rem;color:var(--snice-color-text,rgb(23 23 23));overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host([align=left]) .cell-content{justify-content:flex-start}:host([align=center]) .cell-content{justify-content:center}:host([align=right]) .cell-content{justify-content:flex-end}@media (max-width:768px){:host{color:var(--snice-color-text,rgb(23 23 23));font-size:.75rem;line-height:1rem}.color-swatch--small{width:.875rem;height:.875rem}.color-swatch--medium{width:1.25rem;height:1.25rem}.color-swatch--large{width:1.75rem;height:1.75rem}.color-text{font-size:.75rem}}@media print{.color-swatch{border:2px solid var(--snice-color-text,rgb(23 23 23))}.color-text{font-size:.75rem}}";
7820
8234
 
7821
8235
  (() => {
7822
8236
  let _classDecorators = [snice.element('snice-cell-color')];
@@ -8000,7 +8414,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
8000
8414
  return _classThis;
8001
8415
  })();
8002
8416
 
8003
- var cssContent$4 = ":host([hidden]){display:none}:host{color:var(--snice-color-text,rgb(23 23 23));display:flex;align-items:center;min-height:2.5rem;height:2.5rem;min-width:6.25rem;flex:1;font-size:.875rem;line-height:1.25rem}.cell-content{width:100%;min-width:6.25rem;min-height:1.25rem;display:flex;align-items:center;gap:.375rem}.location-icon{display:inline-flex;align-items:center;font-size:.875rem;opacity:.7;flex-shrink:0}.location-link{color:var(--snice-color-primary,rgb(37 99 235));text-decoration:none;transition:color .2s;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.location-link:hover{color:var(--snice-color-primary-hover,rgb(29 78 216));text-decoration:underline}.location-link:focus-visible{outline:var(--snice-focus-ring-width,2px) solid var(--snice-focus-ring-color,rgb(37 99 235 / .5));outline-offset:2px;border-radius:var(--snice-border-radius-sm,.125rem)}.location-link:visited{color:var(--snice-color-text-secondary,rgb(107 114 128))}.location-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--snice-color-text,rgb(23 23 23))}.location-empty{color:var(--snice-color-text-secondary,rgb(82 82 82));font-style:italic}:host([align=left]) .cell-content{justify-content:flex-start}:host([align=center]) .cell-content{justify-content:center}:host([align=right]) .cell-content{justify-content:flex-end}@media (max-width:768px){:host{color:var(--snice-color-text,rgb(23 23 23));font-size:.75rem;line-height:1rem}.location-icon{font-size:.75rem}}@media print{.location-link{text-decoration:underline}.location-link::after{content:\" (\" attr(href) \")\";font-size:.75rem;color:var(--snice-color-text-secondary,rgb(82 82 82))}.location-icon{display:none}}";
8417
+ var cssContent$4 = ":host([hidden]){display:none}:host{color:var(--snice-color-text,rgb(23 23 23));display:flex;align-items:center;min-height:2.5rem;height:2.5rem;min-width:var(--snice-table-cell-min-width,6.25rem);flex:1;font-size:.875rem;line-height:1.25rem}.cell-content{width:100%;min-width:var(--snice-table-cell-min-width,6.25rem);min-height:1.25rem;display:flex;align-items:center;gap:.375rem}.location-icon{display:inline-flex;align-items:center;font-size:.875rem;opacity:.7;flex-shrink:0}.location-link{color:var(--snice-color-primary,rgb(37 99 235));text-decoration:none;transition:color .2s;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.location-link:hover{color:var(--snice-color-primary-hover,rgb(29 78 216));text-decoration:underline}.location-link:focus-visible{outline:var(--snice-focus-ring-width,2px) solid var(--snice-focus-ring-color,rgb(37 99 235 / .5));outline-offset:2px;border-radius:var(--snice-border-radius-sm,.125rem)}.location-link:visited{color:var(--snice-color-text-secondary,rgb(107 114 128))}.location-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--snice-color-text,rgb(23 23 23))}.location-empty{color:var(--snice-color-text-secondary,rgb(82 82 82));font-style:italic}:host([align=left]) .cell-content{justify-content:flex-start}:host([align=center]) .cell-content{justify-content:center}:host([align=right]) .cell-content{justify-content:flex-end}@media (max-width:768px){:host{color:var(--snice-color-text,rgb(23 23 23));font-size:.75rem;line-height:1rem}.location-icon{font-size:.75rem}}@media print{.location-link{text-decoration:underline}.location-link::after{content:\" (\" attr(href) \")\";font-size:.75rem;color:var(--snice-color-text-secondary,rgb(82 82 82))}.location-icon{display:none}}";
8004
8418
 
8005
8419
  (() => {
8006
8420
  let _classDecorators = [snice.element('snice-cell-location')];
@@ -8183,7 +8597,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
8183
8597
  return _classThis;
8184
8598
  })();
8185
8599
 
8186
- var cssContent$3 = ":host([hidden]){display:none}:host{color:var(--snice-color-text,rgb(23 23 23));display:flex;align-items:center;min-height:2.5rem;height:auto;min-width:6.25rem;flex:1;font-size:.875rem;line-height:1.25rem}.cell-content{width:100%;min-width:6.25rem;min-height:1.25rem;display:flex;align-items:flex-start;gap:.5rem;padding:.25rem 0}.json-toggle{background:0 0;border:none;color:var(--snice-color-text-secondary,rgb(82 82 82));cursor:pointer;padding:.125rem .25rem;font-size:.75rem;line-height:1;border-radius:.25rem;transition:background-color .2s;flex-shrink:0;display:inline-flex;align-items:center;justify-content:center}.json-toggle svg{width:.75rem;height:.75rem}.json-toggle:hover{background-color:var(--snice-color-surface-hover,rgb(245 245 245))}.json-toggle:focus-visible{outline:var(--snice-focus-ring-width,2px) solid var(--snice-focus-ring-color,rgb(37 99 235 / .5));outline-offset:2px}.json-viewer{font-family:'Courier New',Consolas,monospace;font-size:.8125rem;overflow:hidden;flex:1}.json-viewer--collapsed{white-space:nowrap;text-overflow:ellipsis}.json-viewer--expanded{white-space:pre-wrap;word-break:break-word}.json-preview{color:var(--snice-color-text-secondary,rgb(82 82 82));font-style:italic}.json-object{padding-left:1rem}.json-line{margin:.125rem 0}.json-key{color:var(--snice-color-primary,rgb(37 99 235));font-weight:var(--snice-font-weight-medium,500);margin-right:.25rem}.json-string{color:var(--snice-color-success,rgb(22 163 74))}.json-number{color:var(--snice-color-primary,rgb(59 130 246))}.json-boolean{color:var(--snice-color-warning,rgb(234 179 8))}.json-empty,.json-null,.json-undefined{color:var(--snice-color-text-secondary,rgb(82 82 82));font-style:italic}:host([align=left]) .cell-content{justify-content:flex-start}:host([align=center]) .cell-content{justify-content:center}:host([align=right]) .cell-content{justify-content:flex-end}@media (max-width:768px){:host{color:var(--snice-color-text,rgb(23 23 23));font-size:.75rem;line-height:1rem}.json-viewer{font-size:.75rem}.json-toggle{font-size:.625rem}}@media print{.json-toggle{display:none}.json-viewer--collapsed{white-space:normal}.json-viewer{font-size:.625rem}}";
8600
+ var cssContent$3 = ":host([hidden]){display:none}:host{color:var(--snice-color-text,rgb(23 23 23));display:flex;align-items:center;min-height:2.5rem;height:auto;min-width:var(--snice-table-cell-min-width,6.25rem);flex:1;font-size:.875rem;line-height:1.25rem}.cell-content{width:100%;min-width:var(--snice-table-cell-min-width,6.25rem);min-height:1.25rem;display:flex;align-items:flex-start;gap:.5rem;padding:.25rem 0}.json-toggle{background:0 0;border:none;color:var(--snice-color-text-secondary,rgb(82 82 82));cursor:pointer;padding:.125rem .25rem;font-size:.75rem;line-height:1;border-radius:.25rem;transition:background-color .2s;flex-shrink:0;display:inline-flex;align-items:center;justify-content:center}.json-toggle svg{width:.75rem;height:.75rem}.json-toggle:hover{background-color:var(--snice-color-surface-hover,rgb(245 245 245))}.json-toggle:focus-visible{outline:var(--snice-focus-ring-width,2px) solid var(--snice-focus-ring-color,rgb(37 99 235 / .5));outline-offset:2px}.json-viewer{font-family:'Courier New',Consolas,monospace;font-size:.8125rem;overflow:hidden;flex:1}.json-viewer--collapsed{white-space:nowrap;text-overflow:ellipsis}.json-viewer--expanded{white-space:pre-wrap;word-break:break-word}.json-preview{color:var(--snice-color-text-secondary,rgb(82 82 82));font-style:italic}.json-object{padding-left:1rem}.json-line{margin:.125rem 0}.json-key{color:var(--snice-color-primary,rgb(37 99 235));font-weight:var(--snice-font-weight-medium,500);margin-right:.25rem}.json-string{color:var(--snice-color-success,rgb(22 163 74))}.json-number{color:var(--snice-color-primary,rgb(59 130 246))}.json-boolean{color:var(--snice-color-warning,rgb(234 179 8))}.json-empty,.json-null,.json-undefined{color:var(--snice-color-text-secondary,rgb(82 82 82));font-style:italic}:host([align=left]) .cell-content{justify-content:flex-start}:host([align=center]) .cell-content{justify-content:center}:host([align=right]) .cell-content{justify-content:flex-end}@media (max-width:768px){:host{color:var(--snice-color-text,rgb(23 23 23));font-size:.75rem;line-height:1rem}.json-viewer{font-size:.75rem}.json-toggle{font-size:.625rem}}@media print{.json-toggle{display:none}.json-viewer--collapsed{white-space:normal}.json-viewer{font-size:.625rem}}";
8187
8601
 
8188
8602
  (() => {
8189
8603
  let _classDecorators = [snice.element('snice-cell-json')];
@@ -8604,7 +9018,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
8604
9018
  return _classThis;
8605
9019
  })();
8606
9020
 
8607
- var cssContent$2 = ":host([hidden]){display:none}:host{color:var(--snice-color-text,rgb(23 23 23));display:flex;align-items:center;min-height:2.5rem;min-width:6.25rem;flex:1;font-size:.875rem;line-height:1.25rem}.cell-content{width:100%;min-width:6.25rem;min-height:1.25rem;display:flex;align-items:center}.cell-image{display:block;object-fit:cover;background-color:rgb(243 244 246)}.image--small{width:1.5rem;height:1.5rem}.image--medium{width:2rem;height:2rem}.image--large{width:3rem;height:3rem}.image--rounded{border-radius:.25rem}.image--square{border-radius:0}.image--circle{border-radius:50%}.cell-image--placeholder{background:linear-gradient(135deg,rgb(229 231 235) 0,rgb(209 213 219) 100%);border:1px solid rgb(209 213 219);position:relative}.cell-image__placeholder-icon{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);display:inline-flex;align-items:center;justify-content:center;color:var(--snice-color-text-tertiary,rgb(115 115 115));opacity:.55}.cell-image__placeholder-icon svg{width:50%;max-width:1.25rem;height:auto}:host([align=left]) .cell-content{justify-content:flex-start}:host([align=center]) .cell-content{justify-content:center}:host([align=right]) .cell-content{justify-content:flex-end}:host(.loading) .cell-image{opacity:.6;animation:1.5s ease-in-out infinite image-pulse}@keyframes image-pulse{0%,100%{opacity:.6}50%{opacity:.3}}@media (max-width:768px){.image--small{width:1.25rem;height:1.25rem}.image--medium{width:1.75rem;height:1.75rem}.image--large{width:2.5rem;height:2.5rem}}@media (prefers-reduced-motion:reduce){:host(.loading) .cell-image{animation:none}}@media print{.cell-image{max-width:2rem;max-height:2rem}}";
9021
+ var cssContent$2 = ":host([hidden]){display:none}:host{color:var(--snice-color-text,rgb(23 23 23));display:flex;align-items:center;min-height:2.5rem;min-width:var(--snice-table-cell-min-width,6.25rem);flex:1;font-size:.875rem;line-height:1.25rem}.cell-content{width:100%;min-width:var(--snice-table-cell-min-width,6.25rem);min-height:1.25rem;display:flex;align-items:center}.cell-image{display:block;object-fit:cover;background-color:rgb(243 244 246)}.image--small{width:1.5rem;height:1.5rem}.image--medium{width:2rem;height:2rem}.image--large{width:3rem;height:3rem}.image--rounded{border-radius:.25rem}.image--square{border-radius:0}.image--circle{border-radius:50%}.cell-image--placeholder{background:linear-gradient(135deg,rgb(229 231 235) 0,rgb(209 213 219) 100%);border:1px solid rgb(209 213 219);position:relative}.cell-image__placeholder-icon{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);display:inline-flex;align-items:center;justify-content:center;color:var(--snice-color-text-tertiary,rgb(115 115 115));opacity:.55}.cell-image__placeholder-icon svg{width:50%;max-width:1.25rem;height:auto}:host([align=left]) .cell-content{justify-content:flex-start}:host([align=center]) .cell-content{justify-content:center}:host([align=right]) .cell-content{justify-content:flex-end}:host(.loading) .cell-image{opacity:.6;animation:1.5s ease-in-out infinite image-pulse}@keyframes image-pulse{0%,100%{opacity:.6}50%{opacity:.3}}@media (max-width:768px){.image--small{width:1.25rem;height:1.25rem}.image--medium{width:1.75rem;height:1.75rem}.image--large{width:2.5rem;height:2.5rem}}@media (prefers-reduced-motion:reduce){:host(.loading) .cell-image{animation:none}}@media print{.cell-image{max-width:2rem;max-height:2rem}}";
8608
9022
 
8609
9023
  (() => {
8610
9024
  let _classDecorators = [snice.element('snice-cell-image')];
@@ -9308,7 +9722,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
9308
9722
  return _classThis;
9309
9723
  })();
9310
9724
 
9311
- var cssContent$1 = ":host([hidden]){display:none}:host{display:block;width:100%;min-height:2.5rem}.row-container{display:flex;align-items:stretch;width:100%;min-height:2.5rem;background:0 0;color:var(--snice-color-text,rgb(23 23 23));border-bottom:1px solid var(--snice-color-border,rgb(226 226 226));transition:background-color .15s}.cell{display:flex;align-items:center;border-right:1px solid var(--snice-color-border,rgb(226 226 226));min-height:2.5rem;min-width:6.25rem;flex:1;word-wrap:break-word;overflow:hidden;color:var(--snice-color-text,rgb(23 23 23))}.cell:last-child{border-right:none}.cell--checkbox{width:3rem;min-width:3rem;max-width:3rem;padding:.75rem;justify-content:center}.row-checkbox{width:1rem;height:1rem;cursor:pointer;margin:0;display:block;accent-color:var(--snice-color-primary,rgb(37 99 235))}.cells-container{display:flex;flex:1;align-items:stretch}.cell--text{text-align:left}.cell--accounting,.cell--currency,.cell--fraction,.cell--number,.cell--percent,.cell--scientific{text-align:right;font-variant-numeric:tabular-nums}.cell--date{min-width:8rem;font-variant-numeric:tabular-nums}.cell--boolean{text-align:center;font-size:1rem}.cell--rating{text-align:center;min-width:6rem}.cell--progress{min-width:8rem}.cell--sparkline{text-align:center;min-width:6rem}.cell--duration,.cell--filesize{text-align:right;font-variant-numeric:tabular-nums}:host(.row--hoverable) .row-container:hover{background-color:var(--snice-color-surface-hover,rgb(243 244 246))}:host(.row--clickable) .row-container{cursor:pointer}:host(.row--clickable) .row-container:hover{background-color:var(--snice-color-surface-hover,rgb(243 244 246))}:host(.row--selected) .row-container{background-color:var(--snice-color-primary-subtle,rgb(239 246 255));box-shadow:inset 2px 0 0 0 var(--snice-color-primary,rgb(37 99 235))}:host(.row--selected.row--hoverable) .row-container:hover{background-color:var(--snice-color-primary-subtle-hover,var(--snice-color-primary-subtle,rgb(219 234 254)))}:host(.row--highlighted) .row-container{background-color:var(--snice-color-warning-subtle,rgb(254 249 195))!important;animation:2s ease-out row-highlight-fade}@keyframes row-highlight-fade{0%{background-color:var(--snice-color-warning-subtle-hover,rgb(254 252 232))}100%{background-color:var(--snice-color-warning-subtle,rgb(254 249 195))}}:host(:focus) .row-container{outline:var(--snice-focus-ring-width,2px) solid var(--snice-focus-ring-color,rgb(37 99 235 / .5));outline-offset:-2px}:host(.row--selectable){tabindex:0}:host(.row--striped) .row-container{background-color:var(--snice-color-surface-container-low,rgb(245 245 245))}:host(.row--bordered) .row-container{border-bottom:1px solid var(--snice-color-border,rgb(226 226 226))}:host(.row--small) .cell{padding:.5rem .75rem;min-height:2rem;font-size:.75rem}:host(.row--small) .row-container{min-height:2rem}:host(.row--large) .cell{padding:1rem 1.25rem;min-height:3.5rem;font-size:1rem}:host(.row--large) .row-container{min-height:3.5rem}@media (max-width:768px){.cell{padding:.5rem .75rem;font-size:.875rem}.cell--checkbox{width:2.5rem;min-width:2.5rem;max-width:2.5rem;padding:.5rem}.row-checkbox{width:.875rem;height:.875rem}}:host(.row--loading) .row-container{opacity:.6;pointer-events:none}:host(.row--loading) .row-container::after{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(90deg,transparent,var(--snice-color-surface,rgb(255 255 255 / .6)),transparent);animation:1.5s infinite loading-shimmer}@keyframes loading-shimmer{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}@media (prefers-reduced-motion:reduce){.row-container{transition:none}:host(.row--highlighted) .row-container{animation:none;background-color:var(--snice-color-warning-subtle,rgb(254 249 195))!important}:host(.row--loading) .row-container::after{animation:none}}@media (prefers-contrast:high){.cell{border-right:1px solid}.row-container{border-bottom:1px solid}:host(.row--selected) .row-container{border:2px solid}}";
9725
+ var cssContent$1 = ":host([hidden]){display:none}:host{display:block;width:100%;min-height:2.5rem}.row-container{display:flex;align-items:stretch;width:100%;min-height:2.5rem;background:0 0;color:var(--snice-color-text,rgb(23 23 23));border-bottom:1px solid var(--snice-color-border,rgb(226 226 226));transition:background-color .15s}.cell{display:flex;align-items:center;border-right:1px solid var(--snice-color-border,rgb(226 226 226));min-height:2.5rem;min-width:6.25rem;flex:1;word-wrap:break-word;overflow:hidden;color:var(--snice-color-text,rgb(23 23 23))}.cell:last-child{border-right:none}.cell--checkbox{width:3rem;min-width:3rem;max-width:3rem;padding:.75rem;justify-content:center}.row-checkbox{width:1rem;height:1rem;cursor:pointer;margin:0;display:block;accent-color:var(--snice-color-primary,rgb(37 99 235))}.cells-container{display:flex;flex:1;align-items:stretch}.cell--text{text-align:left}.cell--accounting,.cell--currency,.cell--fraction,.cell--number,.cell--percent,.cell--scientific{text-align:right;font-variant-numeric:tabular-nums}.cell--date{min-width:8rem;font-variant-numeric:tabular-nums}.cell--boolean{text-align:center;font-size:1rem}.cell--rating{text-align:center;min-width:6rem}.cell--progress{min-width:8rem}.cell--sparkline{text-align:center;min-width:6rem}.cell--duration,.cell--filesize{text-align:right;font-variant-numeric:tabular-nums}:host(.row--hoverable) .row-container:hover{background-color:var(--snice-color-surface-hover,rgb(243 244 246))}:host(.row--clickable) .row-container{cursor:pointer}:host(.row--clickable) .row-container:hover{background-color:var(--snice-color-surface-hover,rgb(243 244 246))}:host(.row--selected) .row-container{background-color:var(--snice-color-primary-subtle,rgb(239 246 255));box-shadow:inset 2px 0 0 0 var(--snice-color-primary,rgb(37 99 235))}:host(.row--selected.row--hoverable) .row-container:hover{background-color:var(--snice-color-primary-subtle-hover,var(--snice-color-primary-subtle,rgb(219 234 254)))}:host(.row--highlighted) .row-container{background-color:var(--snice-color-warning-subtle,rgb(254 249 195))!important;animation:2s ease-out row-highlight-fade}@keyframes row-highlight-fade{0%{background-color:var(--snice-color-warning-subtle-hover,rgb(254 252 232))}100%{background-color:var(--snice-color-warning-subtle,rgb(254 249 195))}}:host(:focus) .row-container{outline:var(--snice-focus-ring-width,2px) solid var(--snice-focus-ring-color,rgb(37 99 235 / .5));outline-offset:-2px}:host(.row--selectable){tabindex:0}:host(.row--striped) .row-container{background-color:var(--snice-table-stripe-bg,var(--snice-color-overlay-stripe,hsl(0 0% 0% / .02)))}:host(.row--bordered) .row-container{border-bottom:1px solid var(--snice-color-border,rgb(226 226 226))}:host(.row--small) .cell{padding:.5rem .75rem;min-height:2rem;font-size:.75rem}:host(.row--small) .row-container{min-height:2rem}:host(.row--large) .cell{padding:1rem 1.25rem;min-height:3.5rem;font-size:1rem}:host(.row--large) .row-container{min-height:3.5rem}@media (max-width:768px){.cell{padding:.5rem .75rem;font-size:.875rem}.cell--checkbox{width:2.5rem;min-width:2.5rem;max-width:2.5rem;padding:.5rem}.row-checkbox{width:.875rem;height:.875rem}}:host(.row--loading) .row-container{opacity:.6;pointer-events:none}:host(.row--loading) .row-container::after{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(90deg,transparent,var(--snice-color-surface,rgb(255 255 255 / .6)),transparent);animation:1.5s infinite loading-shimmer}@keyframes loading-shimmer{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}@media (prefers-reduced-motion:reduce){.row-container{transition:none}:host(.row--highlighted) .row-container{animation:none;background-color:var(--snice-color-warning-subtle,rgb(254 249 195))!important}:host(.row--loading) .row-container::after{animation:none}}@media (prefers-contrast:high){.cell{border-right:1px solid}.row-container{border-bottom:1px solid}:host(.row--selected) .row-container{border:2px solid}}";
9312
9726
 
9313
9727
  (() => {
9314
9728
  let _classDecorators = [snice.element('snice-row')];
@@ -9561,19 +9975,12 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
9561
9975
  <div class="cell cell--${column.type}" part="cell" style="${this.getCellStyles(column)}">
9562
9976
  ${snice.unsafeHTML(`<${cellComponent}
9563
9977
  type="${column.type}"
9564
- align="${column.align || this.getDefaultAlign(column.type)}"
9978
+ align="${column.align || defaultCellAlign(column.type)}"
9565
9979
  data-column-index="${columnIndex}"
9566
9980
  ></${cellComponent}>`)}
9567
9981
  </div>
9568
9982
  `;
9569
9983
  }
9570
- getDefaultAlign(type) {
9571
- if (['number', 'currency', 'percent', 'percentage', 'duration', 'filesize', 'accounting', 'scientific', 'fraction'].includes(type || ''))
9572
- return 'right';
9573
- if (['boolean', 'rating', 'image'].includes(type || ''))
9574
- return 'center';
9575
- return 'left';
9576
- }
9577
9984
  applyCellPresentation(wrapper, column, value) {
9578
9985
  if (!wrapper)
9579
9986
  return;
@@ -9806,11 +10213,24 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
9806
10213
  window.removeEventListener('resize', this.scrollHandler);
9807
10214
  cancelAnimationFrame(this.rafId);
9808
10215
  }
9809
- setTotalRows(total) {
10216
+ /**
10217
+ * Publish a new row count. `repaint` false updates the model only — the
10218
+ * pending re-render is still armed (lastStartIndex is reset), so the caller's
10219
+ * own paint (scrollToIndex / refresh) performs it. Callers that resolve a
10220
+ * display index and then scroll MUST use that form: painting here runs
10221
+ * `afterRender`, and a host whose afterRender restores keyboard focus would
10222
+ * re-enter this method from inside its own update.
10223
+ */
10224
+ setTotalRows(total, repaint = true) {
9810
10225
  this.options.totalRows = total;
9811
10226
  this.invalidateOffsets();
9812
10227
  this.lastStartIndex = -1; // Force re-render
9813
- this.update();
10228
+ if (repaint)
10229
+ this.update();
10230
+ }
10231
+ /** Row count the virtualizer is currently windowing over. */
10232
+ getTotalRows() {
10233
+ return this.options.totalRows ?? 0;
9814
10234
  }
9815
10235
  /**
9816
10236
  * Force a full re-render of the visible range.
@@ -9821,11 +10241,30 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
9821
10241
  this.lastEndIndex = -1;
9822
10242
  this.update();
9823
10243
  }
10244
+ /**
10245
+ * The element that scrolls RIGHT NOW. A structural re-render of the host
10246
+ * swaps the node captured at `attach()`; keep the listener and every lookup
10247
+ * (notably the `<tbody>` we paint into) on the live one.
10248
+ */
10249
+ resolveContainer() {
10250
+ const live = this.options.resolveScrollContainer?.() ?? null;
10251
+ const current = this.options.scrollContainer;
10252
+ if (live && live !== current) {
10253
+ current?.removeEventListener('scroll', this.scrollHandler);
10254
+ live.addEventListener('scroll', this.scrollHandler, { passive: true });
10255
+ this.options.scrollContainer = live;
10256
+ // The new container's tbody is empty — the range we believed painted is
10257
+ // not on screen anymore.
10258
+ this.lastStartIndex = -1;
10259
+ this.lastEndIndex = -1;
10260
+ }
10261
+ return this.options.scrollContainer ?? null;
10262
+ }
9824
10263
  /**
9825
10264
  * Calculate visible range and render if changed.
9826
10265
  */
9827
10266
  update() {
9828
- if (!this.enabled || !this.options.scrollContainer)
10267
+ if (!this.enabled || !this.resolveContainer())
9829
10268
  return;
9830
10269
  const { bufferPx, totalRows, scrollContainer, renderRange } = this.options;
9831
10270
  const scrollTop = scrollContainer.scrollTop;
@@ -9839,11 +10278,12 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
9839
10278
  // Filtering, pagination, or collapsing a group can shrink the flattened
9840
10279
  // model while the container is scrolled near the old end. Clamp before
9841
10280
  // deriving indices so startIndex never sits beyond totalRows and blanks the
9842
- // body. Reflect the clamp to the element as well so its next scroll event
9843
- // observes the same state.
10281
+ // body. Only an offset that overruns the whole content is written back to
10282
+ // the element — a browser already clamps its own scrollTop, and rewriting a
10283
+ // deliberate offset (scrollToRow) would undo the caller's request.
9844
10284
  const maxScrollTop = Math.max(0, totalHeight - viewportHeight);
9845
10285
  const effectiveScrollTop = Math.min(Math.max(0, scrollTop), maxScrollTop);
9846
- if (scrollTop !== effectiveScrollTop)
10286
+ if (scrollTop > totalHeight || scrollTop < 0)
9847
10287
  scrollContainer.scrollTop = effectiveScrollTop;
9848
10288
  // Calculate visible range with buffer
9849
10289
  const startPx = Math.max(0, effectiveScrollTop - bufferPx);
@@ -9853,75 +10293,89 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
9853
10293
  // Skip if range hasn't changed
9854
10294
  if (startIndex === this.lastStartIndex && endIndex === this.lastEndIndex)
9855
10295
  return;
9856
- this.lastStartIndex = startIndex;
9857
- this.lastEndIndex = endIndex;
10296
+ // Nothing to paint into yet (the host template has not committed its table,
10297
+ // or a structural branch flip is mid-flight). Return WITHOUT recording the
10298
+ // range: a dropped paint that looks like a completed one makes the guard
10299
+ // above suppress every retry, and then only a viewport-height change (a
10300
+ // window resize) ever repaints the rows.
10301
+ const tbody = scrollContainer.querySelector('tbody');
10302
+ if (!tbody)
10303
+ return;
9858
10304
  // Update spacers
9859
10305
  const topHeight = offsets[startIndex] || 0;
9860
10306
  const bottomHeight = Math.max(0, totalHeight - (offsets[endIndex] || totalHeight));
9861
10307
  this.topSpacer.style.height = `${topHeight}px`;
9862
10308
  this.bottomSpacer.style.height = `${bottomHeight}px`;
10309
+ // Span the columns that exist, never more (see resolveColumnSpan).
10310
+ const columnSpan = Math.max(1, Math.round(this.options.resolveColumnSpan?.() ?? 1));
9863
10311
  // Render the visible range
9864
10312
  const fragment = renderRange(startIndex, endIndex);
9865
10313
  // Build the tbody content
9866
- const tbody = this.options.scrollContainer.querySelector('tbody');
9867
- if (tbody) {
9868
- tbody.innerHTML = '';
9869
- // Pinned-top rows render outside (above) the windowed range so they are
9870
- // always present. Rebuilt every window recompute since tbody is wiped.
9871
- const pinnedTop = this.options.renderPinnedTop?.();
9872
- if (pinnedTop)
9873
- tbody.appendChild(pinnedTop);
9874
- if (topHeight > 0) {
9875
- const topTd = document.createElement('td');
9876
- topTd.colSpan = 999;
9877
- topTd.style.height = `${topHeight}px`;
9878
- topTd.style.padding = '0';
9879
- topTd.style.border = 'none';
9880
- this.topSpacer.innerHTML = '';
9881
- this.topSpacer.appendChild(topTd);
9882
- tbody.appendChild(this.topSpacer);
9883
- }
9884
- tbody.appendChild(fragment);
9885
- if (bottomHeight > 0) {
9886
- const bottomTd = document.createElement('td');
9887
- bottomTd.colSpan = 999;
9888
- bottomTd.style.height = `${bottomHeight}px`;
9889
- bottomTd.style.padding = '0';
9890
- bottomTd.style.border = 'none';
9891
- this.bottomSpacer.innerHTML = '';
9892
- this.bottomSpacer.appendChild(bottomTd);
9893
- tbody.appendChild(this.bottomSpacer);
9894
- }
9895
- // Pinned-bottom rows render outside (below) the windowed range.
9896
- const pinnedBottom = this.options.renderPinnedBottom?.();
9897
- if (pinnedBottom)
9898
- tbody.appendChild(pinnedBottom);
9899
- this.options.afterRender?.();
9900
- }
10314
+ tbody.innerHTML = '';
10315
+ // Pinned-top rows render outside (above) the windowed range so they are
10316
+ // always present. Rebuilt every window recompute since tbody is wiped.
10317
+ const pinnedTop = this.options.renderPinnedTop?.();
10318
+ if (pinnedTop)
10319
+ tbody.appendChild(pinnedTop);
10320
+ if (topHeight > 0) {
10321
+ const topTd = document.createElement('td');
10322
+ topTd.colSpan = columnSpan;
10323
+ topTd.style.height = `${topHeight}px`;
10324
+ topTd.style.padding = '0';
10325
+ topTd.style.border = 'none';
10326
+ this.topSpacer.innerHTML = '';
10327
+ this.topSpacer.appendChild(topTd);
10328
+ tbody.appendChild(this.topSpacer);
10329
+ }
10330
+ tbody.appendChild(fragment);
10331
+ // Nothing to window over: hand the body back its empty/loading/error row
10332
+ // instead of leaving the user staring at a blank frame.
10333
+ if (totalRows === 0) {
10334
+ const emptyBody = this.options.renderEmptyBody?.();
10335
+ if (emptyBody)
10336
+ tbody.appendChild(emptyBody);
10337
+ }
10338
+ if (bottomHeight > 0) {
10339
+ const bottomTd = document.createElement('td');
10340
+ bottomTd.colSpan = columnSpan;
10341
+ bottomTd.style.height = `${bottomHeight}px`;
10342
+ bottomTd.style.padding = '0';
10343
+ bottomTd.style.border = 'none';
10344
+ this.bottomSpacer.innerHTML = '';
10345
+ this.bottomSpacer.appendChild(bottomTd);
10346
+ tbody.appendChild(this.bottomSpacer);
10347
+ }
10348
+ // Pinned-bottom rows render outside (below) the windowed range.
10349
+ const pinnedBottom = this.options.renderPinnedBottom?.();
10350
+ if (pinnedBottom)
10351
+ tbody.appendChild(pinnedBottom);
10352
+ // The window is on screen — only NOW does this range count as painted.
10353
+ // Committed before afterRender so a re-entrant update() from that hook
10354
+ // (keyboard focus restoration scrolls) sees the range it is looking at.
10355
+ this.lastStartIndex = startIndex;
10356
+ this.lastEndIndex = endIndex;
10357
+ this.options.afterRender?.();
9901
10358
  }
9902
10359
  /** Get the currently visible row index range */
9903
10360
  getVisibleRange() {
9904
10361
  return { start: this.lastStartIndex, end: this.lastEndIndex };
9905
10362
  }
9906
- /** Scroll to bring a specific row into view */
9907
- scrollToRow(index) {
9908
- if (!this.options.scrollContainer)
9909
- return;
9910
- const top = this.offsetForIndex(index);
9911
- this.options.scrollContainer.scrollTop = top;
9912
- }
9913
10363
  /**
9914
- * Scroll a logical row index into view AND synchronously render its range.
9915
- * Unlike `scrollToRow`, this does not rely on a scroll event (a programmatic
9916
- * scrollTop assignment does not fire one) it forces the window recompute
9917
- * immediately so the row is in the DOM by the time the caller focuses it.
10364
+ * Scroll a row into view AND synchronously render the window around it.
10365
+ * A programmatic `scrollTop` assignment fires no scroll event, so the window
10366
+ * has to be recomputed hereotherwise the viewport lands on bare spacer and
10367
+ * the requested row is never in the DOM.
9918
10368
  */
9919
- scrollToIndex(index) {
10369
+ scrollToRow(index) {
9920
10370
  if (!this.options.scrollContainer)
9921
10371
  return;
9922
10372
  this.options.scrollContainer.scrollTop = this.offsetForIndex(index);
9923
10373
  this.refresh();
9924
10374
  }
10375
+ /** Scroll to a logical (flattened display) row index. */
10376
+ scrollToIndex(index) {
10377
+ this.scrollToRow(index);
10378
+ }
9925
10379
  /** Get current scroll position */
9926
10380
  getScrollPosition() {
9927
10381
  const el = this.options.scrollContainer;
@@ -9991,6 +10445,13 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
9991
10445
  * Column management for snice-table.
9992
10446
  * Handles: flex width, resize, auto-size, visibility, ordering, pinning, groups.
9993
10447
  */
10448
+ /** Padding + border a cell spends before any content fits inside it. */
10449
+ function horizontalCellChrome(cell) {
10450
+ const style = getComputedStyle(cell);
10451
+ const px = (value) => Number.parseFloat(value) || 0;
10452
+ return px(style.paddingLeft) + px(style.paddingRight)
10453
+ + px(style.borderLeftWidth) + px(style.borderRightWidth);
10454
+ }
9994
10455
  class TableColumnManager {
9995
10456
  constructor() {
9996
10457
  this.states = new Map();
@@ -10000,27 +10461,110 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
10000
10461
  this.resizingKey = null;
10001
10462
  this.resizeOverlay = null;
10002
10463
  this.tableElement = null;
10464
+ this.fitMode = 'scroll';
10465
+ /**
10466
+ * The content budget the last `squish` fit shared out. A drag-resize needs it
10467
+ * to know how much width the columns beside the dragged one have to give up
10468
+ * (or take back) for the total to stay on the frame.
10469
+ */
10470
+ this.squishBudget = null;
10471
+ }
10472
+ /** Fallback content width for a column nobody sized. */
10473
+ static { this.DEFAULT_WIDTH = 150; }
10474
+ /**
10475
+ * The floor `squish` relaxes `minWidth` down to. Not zero: a column narrower
10476
+ * than this shows no glyph at all, only its ellipsis, and stops being a
10477
+ * column the reader can identify. A frame too narrow even for this is past
10478
+ * what the mode can honour — see `fitVisibleColumns`.
10479
+ */
10480
+ static { this.SQUISH_MIN_WIDTH = 24; }
10481
+ /**
10482
+ * Which fitting policy `fitVisibleColumns` (and a drag-resize) follows.
10483
+ * Owned by the table's `columnFit` property; see the `ColumnFit` docs.
10484
+ */
10485
+ setFitMode(mode) {
10486
+ if (mode === this.fitMode)
10487
+ return;
10488
+ this.fitMode = mode;
10489
+ // The budget belongs to the squish fit that recorded it.
10490
+ if (mode !== 'squish')
10491
+ this.squishBudget = null;
10492
+ }
10493
+ getFitMode() {
10494
+ return this.fitMode;
10003
10495
  }
10004
- /** Initialize column states from column definitions */
10496
+ /**
10497
+ * Routine resync, run on every render: make sure a state exists for each
10498
+ * declared column and refresh the flags that are pure declaration (min/max,
10499
+ * resizable, reorderable, hideable, pinnable, flex).
10500
+ *
10501
+ * Deliberately PRESERVES the interaction-owned members of an existing state —
10502
+ * width, visible, pinned, order — because renderBody() calls this on every
10503
+ * paint, and re-applying the declaration here would undo a drag-resize, a
10504
+ * hidden column or a user reorder on the very next render. Re-applying the
10505
+ * declaration is applyConfiguration()'s job, and only a `columns` assignment
10506
+ * calls that.
10507
+ */
10005
10508
  initialize(columns, tableEl) {
10006
10509
  this.tableElement = tableEl;
10007
10510
  columns.forEach((col, index) => {
10008
10511
  const existing = this.states.get(col.key);
10009
- this.states.set(col.key, {
10010
- key: col.key,
10011
- width: existing?.width ?? this.parseWidth(col.width) ?? 150,
10012
- flex: col.flex,
10013
- minWidth: col.minWidth ?? 50,
10014
- maxWidth: col.maxWidth ?? Infinity,
10015
- visible: existing?.visible ?? true,
10016
- pinned: existing?.pinned ?? (col.pinned || false),
10017
- order: existing?.order ?? index,
10018
- resizable: col.resizable !== false,
10019
- reorderable: col.reorderable !== false,
10020
- hideable: col.hideable !== false,
10021
- pinnable: col.pinnable !== false,
10022
- });
10512
+ this.states.set(col.key, this.buildState(col, index, existing));
10513
+ });
10514
+ }
10515
+ /**
10516
+ * A `columns` assignment (or setColumns()) delivered a NEW CONFIGURATION.
10517
+ *
10518
+ * The declaration is the source of truth here: painted order, `pinned`,
10519
+ * `width` and visibility all come from the array just assigned, and states for
10520
+ * columns the new array no longer declares are DROPPED. Without this the first
10521
+ * configuration the table ever saw won permanently — a re-ordered array kept
10522
+ * the original order, a newly declared pin or width was ignored, and a column
10523
+ * hidden through setColumnVisible() could never be brought back
10524
+ * (MATRIX-columns-2/3/4/6).
10525
+ *
10526
+ * The cost is that the assignment also discards user interaction state (a
10527
+ * drag-resize, a manual reorder, a hidden column) for the columns it
10528
+ * re-declares. That is the intended reading of the contract, and it applies to
10529
+ * EVERY assignment: `columns` declares `hasChanged: () => true`, so handing
10530
+ * back the array the table already holds — after mutating it, or unchanged —
10531
+ * re-applies the declaration exactly like a fresh array would. Mutating the
10532
+ * published definitions is therefore NOT an escape hatch; the only way to keep
10533
+ * user column state is to not assign `columns` at all and drive the change
10534
+ * through the column APIs instead (setColumnVisible / moveColumn / pinColumn /
10535
+ * unpinColumn), which mutate the state map in place. Renders triggered by this
10536
+ * go through the table's coalescing queue, so re-applying costs no extra paint.
10537
+ */
10538
+ applyConfiguration(columns, tableEl) {
10539
+ this.tableElement = tableEl;
10540
+ const next = new Map();
10541
+ columns.forEach((col, index) => {
10542
+ next.set(col.key, this.buildState(col, index));
10023
10543
  });
10544
+ this.states = next;
10545
+ }
10546
+ /**
10547
+ * One place that turns a column definition into a state. `existing` supplies
10548
+ * the interaction-owned members on the resync path; omitting it yields the
10549
+ * pure declared state applyConfiguration() wants.
10550
+ */
10551
+ buildState(col, index, existing) {
10552
+ const declared = this.parseWidth(col.width);
10553
+ return {
10554
+ key: col.key,
10555
+ width: existing?.width ?? declared ?? TableColumnManager.DEFAULT_WIDTH,
10556
+ flex: col.flex,
10557
+ minWidth: col.minWidth ?? 50,
10558
+ maxWidth: col.maxWidth ?? Infinity,
10559
+ visible: existing?.visible ?? true,
10560
+ pinned: existing?.pinned ?? (col.pinned || false),
10561
+ order: existing?.order ?? index,
10562
+ resizable: col.resizable !== false,
10563
+ reorderable: col.reorderable !== false,
10564
+ hideable: col.hideable !== false,
10565
+ pinnable: col.pinnable !== false,
10566
+ authored: existing?.authored ?? declared !== undefined,
10567
+ };
10024
10568
  }
10025
10569
  parseWidth(width) {
10026
10570
  if (!width)
@@ -10076,48 +10620,222 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
10076
10620
  }
10077
10621
  return widths;
10078
10622
  }
10079
- /** Apply computed widths to <col> or <th> elements */
10080
- applyWidths(headerRow, widths) {
10081
- const ths = headerRow.querySelectorAll('th[data-key]');
10082
- ths.forEach(th => {
10083
- const key = th.getAttribute('data-key');
10084
- if (key && widths.has(key)) {
10085
- th.style.width = `${widths.get(key)}px`;
10623
+ /**
10624
+ * Fit the columns nobody sized into `availableWidth` (the frame's content
10625
+ * box), reserving `chromePerColumn` for each column's own padding + border.
10626
+ *
10627
+ * A column width is a CONTENT width — the rendered column is that plus its
10628
+ * chrome so the budget has to come off the top or the fitted table lands a
10629
+ * few pixels wider than the frame it was fitted to.
10630
+ *
10631
+ * Declared and user-chosen widths are spent first and never rewritten: a
10632
+ * table that asked for `width: '80'` keeps 80 whatever the frame does. What
10633
+ * is left is split evenly and clamped to each column's [minWidth, maxWidth],
10634
+ * so a frame too narrow for the minimums overflows into the frame's own
10635
+ * scroller instead of collapsing the columns to nothing.
10636
+ *
10637
+ * Returns whether any width actually moved, so callers can skip a repaint.
10638
+ *
10639
+ * Under `column-fit="squish"` the frame stops being a suggestion: see
10640
+ * `squishVisibleColumns`.
10641
+ */
10642
+ fitVisibleColumns(availableWidth, chromePerColumn = 0) {
10643
+ const visible = this.getVisibleColumns();
10644
+ // A layoutless (or not-yet-laid-out) frame reports 0. Fitting to that would
10645
+ // slam every column to its minimum for a measurement that means nothing.
10646
+ if (visible.length === 0 || !(availableWidth > 0))
10647
+ return false;
10648
+ const budget = availableWidth - visible.length * chromePerColumn;
10649
+ if (this.fitMode === 'squish') {
10650
+ this.squishBudget = budget;
10651
+ return this.squishVisibleColumns(visible, budget);
10652
+ }
10653
+ const auto = visible.filter((state) => !state.authored);
10654
+ if (auto.length === 0)
10655
+ return false;
10656
+ let remaining = budget;
10657
+ for (const state of visible) {
10658
+ if (state.authored)
10659
+ remaining -= state.width;
10660
+ }
10661
+ const share = Math.floor(remaining / auto.length);
10662
+ let changed = false;
10663
+ for (const state of auto) {
10664
+ const next = Math.max(state.minWidth, Math.min(state.maxWidth, share));
10665
+ if (next !== state.width) {
10666
+ state.width = next;
10667
+ changed = true;
10086
10668
  }
10087
- });
10669
+ }
10670
+ return changed;
10088
10671
  }
10089
- // ── Resize ──
10090
- /** Start column resize. Call from mousedown on resize handle. */
10091
- startResize(columnKey, startX) {
10092
- const state = this.states.get(columnKey);
10093
- if (!state || !state.resizable)
10094
- return;
10095
- this.resizingKey = columnKey;
10096
- this.resizeStartX = startX;
10097
- this.resizeStartWidth = state.width;
10098
- // Create overlay to capture mouse events
10099
- this.resizeOverlay = document.createElement('div');
10100
- this.resizeOverlay.style.cssText = 'position:fixed;inset:0;z-index:99999;cursor:col-resize;';
10101
- document.body.appendChild(this.resizeOverlay);
10102
- const onMove = (e) => {
10103
- e.preventDefault();
10104
- const delta = e.clientX - this.resizeStartX;
10105
- const newWidth = Math.max(state.minWidth, Math.min(state.maxWidth, this.resizeStartWidth + delta));
10106
- state.width = newWidth;
10672
+ /**
10673
+ * The `squish` policy: the columns add up to the frame, whatever that costs.
10674
+ *
10675
+ * `minWidth` is a readability preference, and this mode has already decided
10676
+ * the frame outranks it, so the only floor left is SQUISH_MIN_WIDTH. Two
10677
+ * cases:
10678
+ *
10679
+ * - There is an unsized column left to absorb the difference, and the
10680
+ * authored widths still leave room for it. Those are honoured exactly (a
10681
+ * column that asked for 80, or that the user dragged to 80, keeps 80) and
10682
+ * the rest is split evenly between the unsized ones.
10683
+ *
10684
+ * - Every column is authored, or the authored widths alone already overflow
10685
+ * the frame. There is nothing left to give, so `scroll`'s escape hatch
10686
+ * (overflow into the frame's scroller) is exactly what this mode exists to
10687
+ * prevent: every column is scaled by the same factor instead. Scaling is a
10688
+ * fixed point the second pass computes a factor of 1 — so this settles
10689
+ * on the first render rather than creeping on every repaint.
10690
+ *
10691
+ * Either way the remainder from integer division is handed back to the widest
10692
+ * columns that are free to take it, so the widths sum to the budget EXACTLY
10693
+ * and the rightmost column edge lands on the frame's inner edge instead of a
10694
+ * few pixels short of it.
10695
+ */
10696
+ squishVisibleColumns(visible, budget) {
10697
+ const floor = TableColumnManager.SQUISH_MIN_WIDTH;
10698
+ const auto = visible.filter((state) => !state.authored);
10699
+ // What the frame has left once the authored widths are paid for.
10700
+ const spare = budget - visible.reduce((spent, state) => spent + (state.authored ? state.width : 0), 0);
10701
+ const next = new Map();
10702
+ if (auto.length > 0 && spare >= auto.length * floor) {
10703
+ const share = Math.floor(spare / auto.length);
10704
+ for (const state of visible) {
10705
+ next.set(state.key, state.authored
10706
+ ? state.width
10707
+ : Math.max(floor, Math.min(state.maxWidth, share)));
10708
+ }
10709
+ }
10710
+ else {
10711
+ const current = visible.reduce((sum, state) => sum + state.width, 0) || 1;
10712
+ const scale = budget / current;
10713
+ for (const state of visible) {
10714
+ next.set(state.key, Math.max(floor, Math.floor(state.width * scale)));
10715
+ }
10716
+ }
10717
+ // Hand the leftover pixels to the widest column that is free to grow: it
10718
+ // absorbs a few pixels without reading as a different column, and a
10719
+ // narrower one might be sitting on its floor or its maxWidth.
10720
+ let slack = budget - Array.from(next.values()).reduce((sum, w) => sum + w, 0);
10721
+ if (slack > 0) {
10722
+ const takers = visible
10723
+ .filter((state) => (next.get(state.key) ?? 0) < state.maxWidth)
10724
+ .sort((a, b) => (next.get(b.key) ?? 0) - (next.get(a.key) ?? 0));
10725
+ for (const state of takers) {
10726
+ if (slack <= 0)
10727
+ break;
10728
+ const room = Math.min(slack, state.maxWidth - (next.get(state.key) ?? 0));
10729
+ next.set(state.key, (next.get(state.key) ?? 0) + room);
10730
+ slack -= room;
10731
+ }
10732
+ }
10733
+ let changed = false;
10734
+ for (const state of visible) {
10735
+ const width = next.get(state.key);
10736
+ if (width !== state.width) {
10737
+ state.width = width;
10738
+ changed = true;
10739
+ }
10740
+ }
10741
+ return changed;
10742
+ }
10743
+ /**
10744
+ * A drag-resize under `squish` moved one column; the others pay for it.
10745
+ *
10746
+ * The dragged column is the anchor — it keeps the width the pointer asked
10747
+ * for, capped so the remaining columns can still sit on their floor — and the
10748
+ * rest are scaled to fill exactly what is left of the frame. Without this a
10749
+ * drag would simply push the trailing column out of a frame that is not
10750
+ * allowed to scroll.
10751
+ */
10752
+ rebalanceSquish(anchorKey) {
10753
+ const budget = this.squishBudget;
10754
+ const anchor = this.states.get(anchorKey);
10755
+ if (budget === null || !anchor)
10756
+ return;
10757
+ const others = this.getVisibleColumns().filter((state) => state.key !== anchorKey);
10758
+ if (others.length === 0)
10759
+ return;
10760
+ const floor = TableColumnManager.SQUISH_MIN_WIDTH;
10761
+ anchor.width = Math.max(floor, Math.min(anchor.width, budget - others.length * floor));
10762
+ const remaining = budget - anchor.width;
10763
+ const current = others.reduce((sum, state) => sum + state.width, 0) || 1;
10764
+ const scale = remaining / current;
10765
+ for (const state of others) {
10766
+ state.width = Math.max(floor, Math.floor(state.width * scale));
10767
+ }
10768
+ let slack = remaining - others.reduce((sum, state) => sum + state.width, 0);
10769
+ for (const state of [...others].sort((a, b) => b.width - a.width)) {
10770
+ if (slack <= 0)
10771
+ break;
10772
+ const room = Math.min(slack, state.maxWidth - state.width);
10773
+ state.width += room;
10774
+ slack -= room;
10775
+ }
10776
+ }
10777
+ /** Push the current widths onto the painted header and body cells. */
10778
+ writeWidths(keys) {
10779
+ const root = this.tableElement?.shadowRoot;
10780
+ if (!root)
10781
+ return;
10782
+ for (const key of keys) {
10783
+ const width = this.states.get(key)?.width;
10784
+ if (width === undefined)
10785
+ continue;
10786
+ const th = root.querySelector(`th[data-key="${key}"]`);
10787
+ if (th)
10788
+ th.style.width = `${width}px`;
10789
+ const tds = root.querySelectorAll(`td[data-key="${key}"]`);
10790
+ tds.forEach((td) => { td.style.width = `${width}px`; });
10791
+ }
10792
+ }
10793
+ /** Apply computed widths to <col> or <th> elements */
10794
+ applyWidths(headerRow, widths) {
10795
+ const ths = headerRow.querySelectorAll('th[data-key]');
10796
+ ths.forEach(th => {
10797
+ const key = th.getAttribute('data-key');
10798
+ if (key && widths.has(key)) {
10799
+ th.style.width = `${widths.get(key)}px`;
10800
+ }
10801
+ });
10802
+ }
10803
+ // ── Resize ──
10804
+ /** Start column resize. Call from mousedown on resize handle. */
10805
+ startResize(columnKey, startX) {
10806
+ const state = this.states.get(columnKey);
10807
+ if (!state || !state.resizable)
10808
+ return;
10809
+ this.resizingKey = columnKey;
10810
+ this.resizeStartX = startX;
10811
+ this.resizeStartWidth = state.width;
10812
+ // Create overlay to capture mouse events
10813
+ this.resizeOverlay = document.createElement('div');
10814
+ this.resizeOverlay.style.cssText = 'position:fixed;inset:0;z-index:99999;cursor:col-resize;';
10815
+ document.body.appendChild(this.resizeOverlay);
10816
+ const onMove = (e) => {
10817
+ e.preventDefault();
10818
+ const delta = e.clientX - this.resizeStartX;
10819
+ // Squish relaxes minWidth for the fit, so it has to relax it for the drag
10820
+ // too — otherwise a column the fit already squished below its minimum
10821
+ // would jump back up to it the moment its edge was touched.
10822
+ const squish = this.fitMode === 'squish';
10823
+ const min = squish ? TableColumnManager.SQUISH_MIN_WIDTH : state.minWidth;
10824
+ state.width = Math.max(min, Math.min(state.maxWidth, this.resizeStartWidth + delta));
10107
10825
  // Remove flex if manually resized
10108
10826
  state.flex = undefined;
10109
- // Apply width to DOM immediately
10110
- const root = this.tableElement?.shadowRoot;
10111
- if (root) {
10112
- const th = root.querySelector(`th[data-key="${columnKey}"]`);
10113
- if (th)
10114
- th.style.width = `${newWidth}px`;
10115
- const tds = root.querySelectorAll(`td[data-key="${columnKey}"]`);
10116
- tds.forEach(td => td.style.width = `${newWidth}px`);
10117
- }
10827
+ // The user chose this width the container fit must leave it alone.
10828
+ state.authored = true;
10829
+ // Squish cannot let the drag push the table past the frame: the columns
10830
+ // beside this one give up (or take back) the difference.
10831
+ if (squish)
10832
+ this.rebalanceSquish(columnKey);
10833
+ this.writeWidths(squish
10834
+ ? this.getVisibleColumns().map((column) => column.key)
10835
+ : [columnKey]);
10118
10836
  // Dispatch resize event
10119
10837
  this.tableElement?.dispatchEvent(new CustomEvent('column-resize', {
10120
- detail: { key: columnKey, width: newWidth },
10838
+ detail: { key: columnKey, width: state.width },
10121
10839
  bubbles: true, composed: true
10122
10840
  }));
10123
10841
  };
@@ -10137,24 +10855,81 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
10137
10855
  }
10138
10856
  /** Auto-size a column to fit its content */
10139
10857
  autoSizeColumn(columnKey, tbody) {
10140
- const state = this.states.get(columnKey);
10141
- if (!state)
10142
- return;
10143
- // Measure header
10144
- const th = this.tableElement?.shadowRoot?.querySelector(`th[data-key="${columnKey}"]`);
10145
- let maxWidth = th ? th.scrollWidth : 0;
10146
- // Measure body cells
10147
- const cells = tbody.querySelectorAll(`td[data-key="${columnKey}"]`);
10148
- cells.forEach(cell => {
10149
- maxWidth = Math.max(maxWidth, cell.scrollWidth);
10150
- });
10151
- state.width = Math.max(state.minWidth, Math.min(state.maxWidth, maxWidth + 16)); // 16px padding
10152
- state.flex = undefined;
10858
+ this.autoSizeColumns([columnKey], tbody);
10153
10859
  }
10154
10860
  /** Auto-size all columns */
10155
10861
  autoSizeAll(tbody) {
10156
- for (const key of this.states.keys()) {
10157
- this.autoSizeColumn(key, tbody);
10862
+ this.autoSizeColumns(Array.from(this.states.keys()), tbody);
10863
+ }
10864
+ /** Slack added to a measured content width so the last glyph never sits on
10865
+ * the clip edge (and a sortable header keeps room for its chevron). */
10866
+ static { this.AUTO_SIZE_SLACK = 4; }
10867
+ /**
10868
+ * Measure the columns' content and give each the width it actually needs.
10869
+ *
10870
+ * A table cell's `scrollWidth` is NOT its content width: for content that
10871
+ * fits, it reports the padding box — the width the column already has. The
10872
+ * previous implementation took that maximum and added 16px, so every
10873
+ * "auto-size" pass could only GROW the column, by a chrome's worth each
10874
+ * time, until the table outgrew its own frame. Once it did, the right-pinned
10875
+ * column (`position: sticky`) parked over the columns beside it, and the
10876
+ * columns behind it read as deleted.
10877
+ *
10878
+ * The width a column NEEDS is a layout question, so ask layout: drop the
10879
+ * table into content-driven sizing (`table-layout: auto` at `max-content`)
10880
+ * with the measured columns' imposed widths removed, and read back the width
10881
+ * each header settles at. `max-content` also means no wrapping, so a long
10882
+ * cell reports its full single-line extent even though the painted column
10883
+ * ellipsises it. Everything is restored in the same task, before paint.
10884
+ */
10885
+ autoSizeColumns(columnKeys, tbody) {
10886
+ const root = this.tableElement?.shadowRoot ?? null;
10887
+ const tableEl = (root?.querySelector('table')
10888
+ ?? tbody.closest('table'));
10889
+ const targets = columnKeys
10890
+ .map((key) => this.states.get(key))
10891
+ .filter((state) => !!state);
10892
+ if (!tableEl || targets.length === 0)
10893
+ return;
10894
+ const restore = [];
10895
+ const push = (el, property) => {
10896
+ const previous = el.style[property];
10897
+ restore.push(() => { el.style[property] = previous; });
10898
+ };
10899
+ push(tableEl, 'tableLayout');
10900
+ push(tableEl, 'width');
10901
+ tableEl.style.tableLayout = 'auto';
10902
+ tableEl.style.width = 'max-content';
10903
+ const headers = new Map();
10904
+ for (const state of targets) {
10905
+ const header = root?.querySelector(`th[data-key="${state.key}"]`);
10906
+ if (!header)
10907
+ continue;
10908
+ headers.set(state.key, header);
10909
+ // An imposed width is still a preferred width under auto layout, so it
10910
+ // has to come off before the column can report what it wants.
10911
+ push(header, 'width');
10912
+ header.style.width = 'auto';
10913
+ for (const cell of Array.from(tbody.querySelectorAll(`td[data-key="${state.key}"]`))) {
10914
+ push(cell, 'width');
10915
+ cell.style.width = 'auto';
10916
+ }
10917
+ }
10918
+ const widths = new Map();
10919
+ for (const [key, header] of headers) {
10920
+ widths.set(key, header.getBoundingClientRect().width - horizontalCellChrome(header));
10921
+ }
10922
+ for (const undo of restore)
10923
+ undo();
10924
+ for (const state of targets) {
10925
+ const content = widths.get(state.key) ?? 0;
10926
+ // Nothing measurable — a layoutless DOM, or a column with no rendered
10927
+ // header. Leave the width alone rather than slamming it to the minimum.
10928
+ if (content <= 0)
10929
+ continue;
10930
+ state.width = Math.max(state.minWidth, Math.min(state.maxWidth, Math.ceil(content) + TableColumnManager.AUTO_SIZE_SLACK));
10931
+ state.flex = undefined;
10932
+ state.authored = true;
10158
10933
  }
10159
10934
  }
10160
10935
  // ── Visibility ──
@@ -10475,25 +11250,45 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
10475
11250
  return Number(cellValue) < Number(filterValue);
10476
11251
  case 'lte':
10477
11252
  return Number(cellValue) <= Number(filterValue);
10478
- // Date
10479
- case 'is': {
10480
- const d1 = new Date(cellValue).toDateString();
10481
- const d2 = new Date(filterValue).toDateString();
10482
- return d1 === d2;
10483
- }
11253
+ // Date. Both sides cross the cell layer's parser, so a `2026-03-15`
11254
+ // bound means that calendar day locally — the same day the cell paints
11255
+ // instead of UTC midnight, which shifted every comparison by a day
11256
+ // for negative UTC offsets and split a date-only bound from a
11257
+ // timestamped cell value on the same day.
11258
+ case 'is':
10484
11259
  case 'isNot': {
10485
- const d1 = new Date(cellValue).toDateString();
10486
- const d2 = new Date(filterValue).toDateString();
10487
- return d1 !== d2;
11260
+ const cellDay = this.calendarDay(cellValue);
11261
+ const filterDay = this.calendarDay(filterValue);
11262
+ // An unparseable value is on no day: it never "is" the filtered day,
11263
+ // and it is therefore always "isNot" it.
11264
+ const sameDay = cellDay !== null && filterDay !== null && cellDay === filterDay;
11265
+ return filter.operator === 'is' ? sameDay : !sameDay;
10488
11266
  }
10489
11267
  case 'before':
10490
- return new Date(cellValue) < new Date(filterValue);
10491
11268
  case 'onOrBefore':
10492
- return new Date(cellValue) <= new Date(filterValue);
10493
11269
  case 'after':
10494
- return new Date(cellValue) > new Date(filterValue);
10495
- case 'onOrAfter':
10496
- return new Date(cellValue) >= new Date(filterValue);
11270
+ case 'onOrAfter': {
11271
+ const cell = parseCellDate(cellValue);
11272
+ const bound = parseCellDate(filterValue);
11273
+ if (!cell || !bound)
11274
+ return false;
11275
+ const a = cell.getTime();
11276
+ // A date-only bound names a whole DAY, not the instant it starts at, so
11277
+ // the day's two edges bracket it. Comparing everything against its
11278
+ // opening instant made the four operators disagree with `is`: a cell at
11279
+ // 09:00 on the bound day "is" that day, yet counted as `after` it and
11280
+ // fell outside `onOrBefore` it. A bound that carries its own time is an
11281
+ // instant, and both edges collapse onto it.
11282
+ const dayStart = bound.getTime();
11283
+ const dayEnd = this.boundEnd(filterValue, bound);
11284
+ if (filter.operator === 'before')
11285
+ return a < dayStart;
11286
+ if (filter.operator === 'onOrBefore')
11287
+ return a <= dayEnd;
11288
+ if (filter.operator === 'after')
11289
+ return a > dayEnd;
11290
+ return a >= dayStart;
11291
+ }
10497
11292
  // Boolean
10498
11293
  case 'isTrue':
10499
11294
  return cellValue === true || cellValue === 'true' || cellValue === 1;
@@ -10503,6 +11298,28 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
10503
11298
  return true;
10504
11299
  }
10505
11300
  }
11301
+ /**
11302
+ * The local calendar day a value falls on, as a `YYYY-M-D` stamp, or `null`
11303
+ * when the value names no day at all.
11304
+ */
11305
+ calendarDay(value) {
11306
+ const date = parseCellDate(value);
11307
+ if (!date)
11308
+ return null;
11309
+ return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
11310
+ }
11311
+ /** True when the raw bound named a calendar day rather than an instant. */
11312
+ isDateOnlyBound(raw) {
11313
+ return typeof raw === 'string' && /^\d{4}(?:-\d{2}){0,2}$/.test(raw.trim());
11314
+ }
11315
+ /** Last instant covered by a bound — the day's end when it named a day. */
11316
+ boundEnd(raw, parsed) {
11317
+ if (!this.isDateOnlyBound(raw))
11318
+ return parsed.getTime();
11319
+ const end = new Date(parsed);
11320
+ end.setDate(end.getDate() + 1);
11321
+ return end.getTime() - 1;
11322
+ }
10506
11323
  /** Serialize filter model for server-side requests */
10507
11324
  toServerParams() {
10508
11325
  return {
@@ -10885,7 +11702,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
10885
11702
  return;
10886
11703
  table.setAttribute('role', 'grid');
10887
11704
  table.setAttribute('tabindex', '0');
10888
- table.setAttribute('aria-rowcount', String(this.ariaRowCount + 1)); // +1 header
11705
+ table.setAttribute('aria-rowcount', String(this.ariaRowCount + this.headerRowCount));
10889
11706
  table.setAttribute('aria-colcount', String(this.totalColCount));
10890
11707
  }
10891
11708
  /**
@@ -10919,6 +11736,16 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
10919
11736
  get ariaRowOffset() {
10920
11737
  return Math.max(0, this.resolveBound(this.options.ariaRowOffset));
10921
11738
  }
11739
+ /**
11740
+ * Rows the header contributes to `aria-rowcount`, and the offset every body
11741
+ * row's `aria-rowindex` starts from. Normally 1 — but list mode paints a
11742
+ * card per row and leaves the thead empty, and claiming a header row that
11743
+ * does not exist would count one row too many and start the body at index 2
11744
+ * with nothing at index 1.
11745
+ */
11746
+ get headerRowCount() {
11747
+ return this.getTable()?.querySelector('thead tr') ? 1 : 0;
11748
+ }
10922
11749
  /** Resolve a body row by its logical data-index (not its DOM position). */
10923
11750
  getBodyRow(index) {
10924
11751
  if (this.options.getRowElement)
@@ -10933,7 +11760,8 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
10933
11760
  const table = this.getTable();
10934
11761
  if (!table)
10935
11762
  return;
10936
- table.setAttribute('aria-rowcount', String(this.ariaRowCount + 1)); // +1 for header
11763
+ const headerRows = this.headerRowCount;
11764
+ table.setAttribute('aria-rowcount', String(this.ariaRowCount + headerRows));
10937
11765
  table.setAttribute('aria-colcount', String(this.totalColCount));
10938
11766
  // Header row
10939
11767
  const headerRow = table.querySelector('thead tr.column-header-row')
@@ -10965,7 +11793,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
10965
11793
  const bodyRows = table.querySelectorAll('tbody tr:not(.virtual-spacer):not(.table-fill-row)');
10966
11794
  bodyRows.forEach((row, i) => {
10967
11795
  row.setAttribute('role', 'row');
10968
- row.setAttribute('aria-rowindex', String(this.ariaRowOffset + i + 2)); // +2 for 1-based + header
11796
+ row.setAttribute('aria-rowindex', String(this.ariaRowOffset + i + 1 + headerRows)); // 1-based, after the header
10969
11797
  const isSelected = row.getAttribute('data-selected') === 'true';
10970
11798
  if (isSelected)
10971
11799
  row.setAttribute('aria-selected', 'true');
@@ -11381,8 +12209,10 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
11381
12209
  return `${num.toFixed(column.percentageFormat?.decimals ?? 2)}%`;
11382
12210
  }
11383
12211
  case 'date': {
11384
- const date = new Date(value);
11385
- if (Number.isNaN(date.getTime()))
12212
+ // Exported dates must read as the cell reads them: a calendar day is
12213
+ // local midnight, never UTC midnight (see table-date.ts).
12214
+ const date = parseCellDate(value);
12215
+ if (!date)
11386
12216
  return this.rawValue(value);
11387
12217
  const format = column.dateFormat || {};
11388
12218
  const dateStyle = format.format === 'custom' ? undefined : (format.format || 'short');
@@ -12286,10 +13116,13 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
12286
13116
  this.flatRows = [];
12287
13117
  this.enabled = false;
12288
13118
  this.defaultDepth = 0;
13119
+ /** Whether the default expansion depth has been seeded (first data only). */
13120
+ this.defaultsApplied = false;
12289
13121
  }
12290
13122
  attach(options) {
12291
13123
  this.options = options;
12292
13124
  this.enabled = true;
13125
+ this.defaultsApplied = false;
12293
13126
  // Pre-expand to default depth
12294
13127
  if (options.defaultExpansionDepth !== undefined && options.defaultExpansionDepth >= 0) {
12295
13128
  this.defaultDepth = options.defaultExpansionDepth;
@@ -12310,13 +13143,27 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
12310
13143
  for (const row of data) {
12311
13144
  const path = this.options.getPath(row);
12312
13145
  const key = path.join('/');
12313
- nodeMap.set(key, {
12314
- data: row,
12315
- children: new Set(),
12316
- depth: path.length - 1,
12317
- isGap: false,
12318
- });
12319
- // Ensure all ancestors exist (gap nodes)
13146
+ // A real row may already exist as a gap node generated for a descendant
13147
+ // delivered earlier; upgrade it in place so the children it collected
13148
+ // survive.
13149
+ const existing = nodeMap.get(key);
13150
+ if (existing) {
13151
+ existing.data = row;
13152
+ existing.depth = path.length - 1;
13153
+ existing.isGap = false;
13154
+ }
13155
+ else {
13156
+ nodeMap.set(key, {
13157
+ data: row,
13158
+ children: new Set(),
13159
+ depth: path.length - 1,
13160
+ isGap: false,
13161
+ });
13162
+ }
13163
+ // Ensure all ancestors exist (gap nodes), and register each one as a
13164
+ // child of its own parent: a generated node with no link upward leaves
13165
+ // its parent reporting hasChildren === false, so flatten() never
13166
+ // descends and the whole subtree — real rows included — disappears.
12320
13167
  for (let i = 1; i < path.length; i++) {
12321
13168
  const ancestorPath = path.slice(0, i);
12322
13169
  const ancestorKey = ancestorPath.join('/');
@@ -12328,6 +13175,11 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
12328
13175
  isGap: true,
12329
13176
  });
12330
13177
  }
13178
+ if (i > 1) {
13179
+ const ancestorParent = nodeMap.get(path.slice(0, i - 1).join('/'));
13180
+ if (ancestorParent)
13181
+ ancestorParent.children.add(ancestorKey);
13182
+ }
12331
13183
  }
12332
13184
  // Register as child of parent
12333
13185
  if (path.length > 1) {
@@ -12344,13 +13196,17 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
12344
13196
  rootKeys.push(key);
12345
13197
  }
12346
13198
  rootKeys.sort();
12347
- // Auto-expand to default depth on first process
12348
- if (this.expandedKeys.size === 0 && this.defaultDepth > 0) {
13199
+ // Auto-expand to default depth on the first process that sees rows.
13200
+ // `defaultExpansionDepth` describes the STARTING expansion only: keying
13201
+ // this off an empty expansion set re-seeded it on every render, so
13202
+ // collapsing the last expanded node (or collapseAll()) was silently undone.
13203
+ if (!this.defaultsApplied && this.defaultDepth > 0 && nodeMap.size > 0) {
12349
13204
  for (const [key, node] of nodeMap) {
12350
13205
  if (node.depth < this.defaultDepth && node.children.size > 0) {
12351
13206
  this.expandedKeys.add(key);
12352
13207
  }
12353
13208
  }
13209
+ this.defaultsApplied = true;
12354
13210
  }
12355
13211
  // Flatten tree respecting expand/collapse
12356
13212
  this.flatRows = [];
@@ -13104,6 +13960,11 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
13104
13960
  }
13105
13961
  }
13106
13962
 
13963
+ /**
13964
+ * The one string both loading affordances use — the zero-row message row shows
13965
+ * it, the refetch overlay announces it. Kept together so they can never drift.
13966
+ */
13967
+ const LOADING_LABEL = 'Loading…';
13107
13968
  (() => {
13108
13969
  let _classDecorators = [snice.element('snice-table')];
13109
13970
  let _classDescriptor;
@@ -13198,6 +14059,9 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
13198
14059
  let _density_decorators;
13199
14060
  let _density_initializers = [];
13200
14061
  let _density_extraInitializers = [];
14062
+ let _columnFit_decorators;
14063
+ let _columnFit_initializers = [];
14064
+ let _columnFit_extraInitializers = [];
13201
14065
  let _headerFilters_decorators;
13202
14066
  let _headerFilters_initializers = [];
13203
14067
  let _headerFilters_extraInitializers = [];
@@ -13273,6 +14137,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
13273
14137
  let _handleColumnReorderChange_decorators;
13274
14138
  let _handleLazyLoadChange_decorators;
13275
14139
  let _handleColumnMenuChange_decorators;
14140
+ let _handleColumnFitChange_decorators;
13276
14141
  let _dispatchPageChange_decorators;
13277
14142
  let _dispatchRowSelectionChanged_decorators;
13278
14143
  let _dispatchRowClicked_decorators;
@@ -13310,8 +14175,8 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
13310
14175
  _totalItems_decorators = [snice.property({ type: Number, attribute: 'total-items' })];
13311
14176
  _pageSizes_decorators = [snice.property({ type: Array, attribute: false })];
13312
14177
  _searchDebounce_decorators = [snice.property({ type: Number, attribute: 'search-debounce' })];
13313
- _columns_decorators = [snice.property({ attribute: false })];
13314
- _data_decorators = [snice.property({ attribute: false })];
14178
+ _columns_decorators = [snice.property({ attribute: false, hasChanged: () => true })];
14179
+ _data_decorators = [snice.property({ attribute: false, hasChanged: () => true })];
13315
14180
  _mode_decorators = [snice.property()];
13316
14181
  _currentSort_decorators = [snice.property({ type: Array, attribute: false })];
13317
14182
  _selector_decorators = [snice.property({ type: String, attribute: 'selector' })];
@@ -13324,6 +14189,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
13324
14189
  _editable_decorators = [snice.property({ type: Boolean, attribute: 'editable' })];
13325
14190
  _editMode_decorators = [snice.property({ attribute: 'edit-mode' })];
13326
14191
  _density_decorators = [snice.property({ attribute: 'density' })];
14192
+ _columnFit_decorators = [snice.property({ attribute: 'column-fit' })];
13327
14193
  _headerFilters_decorators = [snice.property({ type: Boolean, attribute: 'header-filters' })];
13328
14194
  _quickFilter_decorators = [snice.property({ type: Boolean, attribute: 'quick-filter' })];
13329
14195
  _rowReorder_decorators = [snice.property({ type: Boolean, attribute: 'row-reorder' })];
@@ -13371,6 +14237,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
13371
14237
  _handleColumnReorderChange_decorators = [snice.watch('columnReorder', { immediate: false })];
13372
14238
  _handleLazyLoadChange_decorators = [snice.watch('lazyLoad', 'lazyLoadThreshold', { immediate: false })];
13373
14239
  _handleColumnMenuChange_decorators = [snice.watch('columnMenu', { immediate: false })];
14240
+ _handleColumnFitChange_decorators = [snice.watch('columnFit', { immediate: false })];
13374
14241
  _dispatchPageChange_decorators = [snice.dispatch('page-change', { bubbles: true, composed: true })];
13375
14242
  _dispatchRowSelectionChanged_decorators = [snice.dispatch('table-row-selection-changed', { bubbles: true, composed: true })];
13376
14243
  _dispatchRowClicked_decorators = [snice.dispatch('row-clicked', { bubbles: true, composed: true })];
@@ -13420,6 +14287,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
13420
14287
  __esDecorate(this, null, _handleColumnReorderChange_decorators, { kind: "method", name: "handleColumnReorderChange", static: false, private: false, access: { has: obj => "handleColumnReorderChange" in obj, get: obj => obj.handleColumnReorderChange }, metadata: _metadata }, null, _instanceExtraInitializers);
13421
14288
  __esDecorate(this, null, _handleLazyLoadChange_decorators, { kind: "method", name: "handleLazyLoadChange", static: false, private: false, access: { has: obj => "handleLazyLoadChange" in obj, get: obj => obj.handleLazyLoadChange }, metadata: _metadata }, null, _instanceExtraInitializers);
13422
14289
  __esDecorate(this, null, _handleColumnMenuChange_decorators, { kind: "method", name: "handleColumnMenuChange", static: false, private: false, access: { has: obj => "handleColumnMenuChange" in obj, get: obj => obj.handleColumnMenuChange }, metadata: _metadata }, null, _instanceExtraInitializers);
14290
+ __esDecorate(this, null, _handleColumnFitChange_decorators, { kind: "method", name: "handleColumnFitChange", static: false, private: false, access: { has: obj => "handleColumnFitChange" in obj, get: obj => obj.handleColumnFitChange }, metadata: _metadata }, null, _instanceExtraInitializers);
13423
14291
  __esDecorate(this, null, _dispatchPageChange_decorators, { kind: "method", name: "dispatchPageChange", static: false, private: false, access: { has: obj => "dispatchPageChange" in obj, get: obj => obj.dispatchPageChange }, metadata: _metadata }, null, _instanceExtraInitializers);
13424
14292
  __esDecorate(this, null, _dispatchRowSelectionChanged_decorators, { kind: "method", name: "dispatchRowSelectionChanged", static: false, private: false, access: { has: obj => "dispatchRowSelectionChanged" in obj, get: obj => obj.dispatchRowSelectionChanged }, metadata: _metadata }, null, _instanceExtraInitializers);
13425
14293
  __esDecorate(this, null, _dispatchRowClicked_decorators, { kind: "method", name: "dispatchRowClicked", static: false, private: false, access: { has: obj => "dispatchRowClicked" in obj, get: obj => obj.dispatchRowClicked }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -13464,6 +14332,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
13464
14332
  __esDecorate(null, null, _editable_decorators, { kind: "field", name: "editable", static: false, private: false, access: { has: obj => "editable" in obj, get: obj => obj.editable, set: (obj, value) => { obj.editable = value; } }, metadata: _metadata }, _editable_initializers, _editable_extraInitializers);
13465
14333
  __esDecorate(null, null, _editMode_decorators, { kind: "field", name: "editMode", static: false, private: false, access: { has: obj => "editMode" in obj, get: obj => obj.editMode, set: (obj, value) => { obj.editMode = value; } }, metadata: _metadata }, _editMode_initializers, _editMode_extraInitializers);
13466
14334
  __esDecorate(null, null, _density_decorators, { kind: "field", name: "density", static: false, private: false, access: { has: obj => "density" in obj, get: obj => obj.density, set: (obj, value) => { obj.density = value; } }, metadata: _metadata }, _density_initializers, _density_extraInitializers);
14335
+ __esDecorate(null, null, _columnFit_decorators, { kind: "field", name: "columnFit", static: false, private: false, access: { has: obj => "columnFit" in obj, get: obj => obj.columnFit, set: (obj, value) => { obj.columnFit = value; } }, metadata: _metadata }, _columnFit_initializers, _columnFit_extraInitializers);
13467
14336
  __esDecorate(null, null, _headerFilters_decorators, { kind: "field", name: "headerFilters", static: false, private: false, access: { has: obj => "headerFilters" in obj, get: obj => obj.headerFilters, set: (obj, value) => { obj.headerFilters = value; } }, metadata: _metadata }, _headerFilters_initializers, _headerFilters_extraInitializers);
13468
14337
  __esDecorate(null, null, _quickFilter_decorators, { kind: "field", name: "quickFilter", static: false, private: false, access: { has: obj => "quickFilter" in obj, get: obj => obj.quickFilter, set: (obj, value) => { obj.quickFilter = value; } }, metadata: _metadata }, _quickFilter_initializers, _quickFilter_extraInitializers);
13469
14338
  __esDecorate(null, null, _rowReorder_decorators, { kind: "field", name: "rowReorder", static: false, private: false, access: { has: obj => "rowReorder" in obj, get: obj => obj.rowReorder, set: (obj, value) => { obj.rowReorder = value; } }, metadata: _metadata }, _rowReorder_initializers, _rowReorder_extraInitializers);
@@ -13507,6 +14376,11 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
13507
14376
  // Wait for next frame to ensure DOM is updated
13508
14377
  await new Promise(resolve => requestAnimationFrame(resolve));
13509
14378
  this.renderHeader();
14379
+ // The body render the columns assignment queued above can have been
14380
+ // consumed against a not-yet-committed <tbody>. Repainting the header
14381
+ // without the rows is what leaves a remote table showing headings over an
14382
+ // empty frame, so this pass owns both.
14383
+ this.renderBody();
13510
14384
  this.renderControls();
13511
14385
  return config;
13512
14386
  }
@@ -13536,8 +14410,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
13536
14410
  if (response.totalItems !== undefined) {
13537
14411
  this.totalItems = response.totalItems;
13538
14412
  }
13539
- this.loadError = null;
13540
- this.classList.remove('table--error');
14413
+ this.clearLoadError();
13541
14414
  this.loading = false;
13542
14415
  // Wait for next frame to ensure DOM is updated
13543
14416
  await new Promise(resolve => requestAnimationFrame(resolve));
@@ -13556,14 +14429,49 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
13556
14429
  this.loading = false;
13557
14430
  // Wait for next frame to ensure DOM is updated
13558
14431
  await new Promise(resolve => requestAnimationFrame(resolve));
14432
+ // Same guard the success path applies above: a newer request can settle
14433
+ // (and paint) inside this frame, and this failure's repaint would then be
14434
+ // a second full body rebuild owed to nobody.
14435
+ if (seq !== this.dataRequestSeq)
14436
+ return;
13559
14437
  this.renderBody();
13560
14438
  }
13561
14439
  }
14440
+ /**
14441
+ * Requeue a body render whose target had not been committed yet. A frame is
14442
+ * the right granularity — the shadow template commits between frames — and
14443
+ * the paint is dropped again (silently, as before) once the retries run out.
14444
+ *
14445
+ * The retry CHAINS: a template that needs more than one frame to commit is
14446
+ * the whole reason this exists, so a retry that still finds no <tbody> spends
14447
+ * one more of the budget rather than giving the frame back and quitting.
14448
+ */
14449
+ rearmBodyRender() {
14450
+ if (this.bodyRenderRetryQueued || this.bodyRenderRetries >= 3)
14451
+ return;
14452
+ this.bodyRenderRetryQueued = true;
14453
+ this.bodyRenderRetries++;
14454
+ requestAnimationFrame(() => {
14455
+ this.bodyRenderRetryQueued = false;
14456
+ if (this.tbody)
14457
+ this.renderBody();
14458
+ else
14459
+ this.rearmBodyRender();
14460
+ });
14461
+ }
13562
14462
  scheduleRender(what) {
13563
14463
  if (what === 'header' || what === 'both')
13564
14464
  this.pendingHeaderRender = true;
13565
- if (what === 'body' || what === 'both')
14465
+ if (what === 'body' || what === 'both') {
13566
14466
  this.pendingBodyRender = true;
14467
+ // A NEWLY requested paint gets a fresh budget. The bound exists to stop
14468
+ // one render from spinning against a template that structurally has no
14469
+ // <tbody> (the slotted-rows branch), not to disqualify the table forever:
14470
+ // without this reset, three misses during the slotted phase silently
14471
+ // cancel every body render that follows a later flip to the native table.
14472
+ if (!this.bodyRenderRetryQueued)
14473
+ this.bodyRenderRetries = 0;
14474
+ }
13567
14475
  if (this.renderQueued)
13568
14476
  return;
13569
14477
  this.renderQueued = true;
@@ -13578,14 +14486,28 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
13578
14486
  const doBody = this.pendingBodyRender;
13579
14487
  this.pendingHeaderRender = false;
13580
14488
  this.pendingBodyRender = false;
13581
- if (doHeader)
13582
- this.renderHeader();
14489
+ try {
14490
+ if (doHeader)
14491
+ this.renderHeader();
14492
+ }
14493
+ catch (error) {
14494
+ // Both flags are already consumed. Letting a header failure escape
14495
+ // would cancel the body paint queued alongside it and leave the rows
14496
+ // permanently stale — the exact "header fine, rows blank" symptom.
14497
+ console.error('Error rendering table header:', error);
14498
+ }
13583
14499
  if (doBody)
13584
14500
  this.renderBody();
13585
14501
  });
13586
14502
  });
13587
14503
  }
13588
14504
  debouncedDataRequest() {
14505
+ // The supersede decision belongs to the moment the NEW request is decided
14506
+ // on, not to the moment its timeout fires. Bumping here closes the 150ms
14507
+ // window in which an already-obsolete in-flight response still satisfied
14508
+ // `seq === this.dataRequestSeq` and painted rows for the previous sort /
14509
+ // filter / page under a header that already advertises the new one.
14510
+ this.dataRequestSeq++;
13589
14511
  // Set loading immediately for instant feedback
13590
14512
  if (!this.loading) {
13591
14513
  this.loading = true;
@@ -13673,6 +14595,72 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
13673
14595
  /* no border or radius — handled by .table-frame */
13674
14596
  }
13675
14597
 
14598
+ /* column-fit="squish": the frame's width is the constraint.
14599
+ *
14600
+ * The arithmetic (syncColumnWidthsToFrame → fitVisibleColumns) already
14601
+ * lands the columns on the frame's content width, and fixed layout hands
14602
+ * any rounding slack back to the columns, so the rightmost edge sits on
14603
+ * the frame's inner edge. These rules are the guarantee behind that:
14604
+ * hiding the horizontal overflow means a stray pixel — a sub-pixel
14605
+ * border, a browser rounding a percentage the other way — can never turn
14606
+ * into a scrollbar the mode promised would not appear. Vertical scrolling
14607
+ * is untouched. */
14608
+ :host([column-fit="squish"]) .table-frame {
14609
+ overflow-x: hidden;
14610
+ }
14611
+
14612
+ :host([column-fit="squish"]) table {
14613
+ max-width: 100%;
14614
+ }
14615
+
14616
+ /* Squished columns are narrower than their content by design, so the
14617
+ * content has to end in an ellipsis rather than wrap the row taller (or
14618
+ * push a word past the clip edge). */
14619
+ :host([column-fit="squish"]) th,
14620
+ :host([column-fit="squish"]) td {
14621
+ white-space: nowrap;
14622
+ }
14623
+
14624
+ :host([column-fit="squish"]) td > snice-cell-text,
14625
+ :host([column-fit="squish"]) td > .custom-cell,
14626
+ :host([column-fit="squish"]) td > .tree-label {
14627
+ display: block;
14628
+ overflow: hidden;
14629
+ text-overflow: ellipsis;
14630
+ }
14631
+
14632
+ /* The typed cells (email, status, link, phone, location, json, colour,
14633
+ * image, actions) each declare a 100px floor on their host AND on their
14634
+ * inner .cell-content. That floor is a sensible default at natural width
14635
+ * and fatal here: a squished column is routinely narrower than 100px, the
14636
+ * cell refuses to shrink to it, and the td's own overflow: hidden then
14637
+ * chops the label mid-glyph — no ellipsis, because the element that owns
14638
+ * the ellipsis never got small enough to trigger it.
14639
+ *
14640
+ * .cell-content lives in the cell's shadow root, so no descendant
14641
+ * selector from here can reach it. A custom property can: it inherits
14642
+ * across the boundary, the cells read it with the same 100px as the
14643
+ * fallback, and nothing outside a squished td sees any change. */
14644
+ :host([column-fit="squish"]) td {
14645
+ --snice-table-cell-min-width: 0;
14646
+ }
14647
+
14648
+ /* The header label is the table's own markup and needs the same promise:
14649
+ * "Department" clipped to "Departme" reads as a different word. A
14650
+ * sortable header puts the label in a flex row beside the sort
14651
+ * indicator, and a flex item only shrinks below its content once it is
14652
+ * allowed to — hence min-width: 0 alongside the ellipsis. A plain header
14653
+ * is bare text in the th, which ellipsises on the th itself. */
14654
+ :host([column-fit="squish"]) th {
14655
+ text-overflow: ellipsis;
14656
+ }
14657
+
14658
+ :host([column-fit="squish"]) th .sort-header > span {
14659
+ overflow: hidden;
14660
+ text-overflow: ellipsis;
14661
+ min-width: 0;
14662
+ }
14663
+
13676
14664
  /* Super-header (slotted area above column headers) */
13677
14665
  .table-superheader {
13678
14666
  background: var(--snice-color-surface, rgb(255 255 255));
@@ -13795,9 +14783,13 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
13795
14783
  background-color: var(--snice-color-surface-container-low, rgb(245 245 245));
13796
14784
  }
13797
14785
 
13798
- /* Row styling */
14786
+ /* Row styling. The zebra tint is an ALPHA overlay, not a surface step:
14787
+ surface-container-low sits ~2 luminance points from the body surface
14788
+ on dark themes, so striped rendered as no striping at all there.
14789
+ An overlay also composites over pinned/tinted cells instead of
14790
+ replacing their color. */
13799
14791
  :host([striped]) tbody tr:nth-child(even) {
13800
- background-color: var(--snice-color-surface-container-low, rgb(245 245 245));
14792
+ background-color: var(--snice-table-stripe-bg, var(--snice-color-overlay-stripe, hsl(0 0% 0% / 0.02)));
13801
14793
  }
13802
14794
 
13803
14795
  :host([hoverable]) tbody tr:hover {
@@ -14061,12 +15053,14 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
14061
15053
  justify-content: center;
14062
15054
  }
14063
15055
 
14064
- /* Loading fade */
14065
- tbody {
15056
+ /* Loading fade. Scoped to ROWS, never the whole tbody: with no rows the
15057
+ only thing in there is the loading indicator, and dimming that halves
15058
+ the contrast of the one element whose entire job is to be seen. */
15059
+ tbody > tr {
14066
15060
  transition: opacity var(--snice-transition-normal, 250ms);
14067
15061
  }
14068
15062
 
14069
- :host([loading]) tbody {
15063
+ :host([loading]) tbody > tr:not(.table-message-row) {
14070
15064
  opacity: 0.5;
14071
15065
  }
14072
15066
 
@@ -14081,6 +15075,33 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
14081
15075
  z-index: 2;
14082
15076
  }
14083
15077
 
15078
+ /* Zero-row loading: spinner + the word, centred in the empty body. */
15079
+ .table-loading-state {
15080
+ display: flex;
15081
+ flex-direction: column;
15082
+ align-items: center;
15083
+ justify-content: center;
15084
+ gap: var(--snice-spacing-xs, 0.5rem);
15085
+ color: var(--snice-color-text-secondary, rgb(82 82 82));
15086
+ }
15087
+
15088
+ .table-loading-label {
15089
+ font-size: var(--snice-font-size-sm, 0.875rem);
15090
+ line-height: 1;
15091
+ }
15092
+
15093
+ /* snice-progress draws its circular track at 20% alpha, which disappears
15094
+ entirely on a dark surface — the ring the field report saw was a
15095
+ near-black hairline on near-black. Both table spinners opt out and use
15096
+ the border token at full strength, so the track reads in either
15097
+ theme and the primary arc always has something to travel around. */
15098
+ .table-loading-state snice-progress,
15099
+ .table-loading-overlay snice-progress {
15100
+ --progress-track-opacity: 1;
15101
+ --progress-bg: var(--snice-color-border, rgb(226 226 226));
15102
+ --progress-color: var(--snice-color-primary, rgb(37 99 235));
15103
+ }
15104
+
14084
15105
  .no-data {
14085
15106
  text-align: center;
14086
15107
  padding: var(--snice-spacing-lg, 1.5rem);
@@ -14222,10 +15243,58 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
14222
15243
  background: var(--snice-color-primary, rgb(37 99 235));
14223
15244
  }
14224
15245
 
14225
- /* Pinned column separator */
15246
+ /* Pinned columns.
15247
+
15248
+ A frozen column must be opaque — scrolled cells pass underneath it —
15249
+ and a header keeps the header's own surface rather than borrowing the
15250
+ body's. The frozen EDGE (the inner-most pinned column on each side) is
15251
+ what the reader actually needs to see: a heavier divider plus a soft
15252
+ shadow cast INTO the scrolling region, mirrored per side. Between two
15253
+ columns pinned to the same side there is no boundary to mark, so they
15254
+ keep the ordinary cell border.
15255
+
15256
+ The shadow is a border-token tint, not black: a black drop shadow is
15257
+ invisible against a dark surface, which is exactly how pinning came to
15258
+ have no affordance at all in the dark theme. */
14226
15259
  .pinned-cell {
14227
15260
  background: var(--snice-color-surface, rgb(255 255 255));
14228
- box-shadow: 2px 0 4px -1px rgb(0 0 0 / 0.1);
15261
+ }
15262
+
15263
+ th.pinned-cell {
15264
+ background: var(--snice-color-surface-container-low, rgb(245 245 245));
15265
+ }
15266
+
15267
+ .pinned-cell--left.pinned-cell--edge {
15268
+ border-right: 2px solid var(--snice-color-border, rgb(226 226 226));
15269
+ box-shadow: 6px 0 8px -6px var(--snice-color-border, rgb(226 226 226));
15270
+ }
15271
+
15272
+ .pinned-cell--right.pinned-cell--edge {
15273
+ border-left: 2px solid var(--snice-color-border, rgb(226 226 226));
15274
+ box-shadow: -6px 0 8px -6px var(--snice-color-border, rgb(226 226 226));
15275
+ }
15276
+
15277
+ /* A right-pinned header is often the last cell in the row, where the
15278
+ shared rule above strips the right border. Its divider is on the left,
15279
+ so nothing to restore — but the edge border must survive that rule. */
15280
+ th:last-child.pinned-cell--right.pinned-cell--edge,
15281
+ td:last-child.pinned-cell--right.pinned-cell--edge {
15282
+ border-right: none;
15283
+ }
15284
+
15285
+ /* Pin marker: the only cue when the table is wide enough not to scroll. */
15286
+ .pin-indicator {
15287
+ display: inline-flex;
15288
+ align-items: center;
15289
+ margin-left: var(--snice-spacing-2xs, 0.25rem);
15290
+ color: var(--snice-color-text-secondary, rgb(82 82 82));
15291
+ opacity: 0.65;
15292
+ vertical-align: middle;
15293
+ pointer-events: none;
15294
+ }
15295
+
15296
+ .pin-indicator--right {
15297
+ transform: scaleX(-1);
14229
15298
  }
14230
15299
 
14231
15300
  /* Density: compact */
@@ -14648,7 +15717,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
14648
15717
  }
14649
15718
 
14650
15719
  :host([striped]) .snice-table--slotted .table-body::slotted(snice-row:nth-child(even)) {
14651
- background-color: var(--snice-color-surface-container-low, rgb(245 245 245));
15720
+ background-color: var(--snice-table-stripe-bg, var(--snice-color-overlay-stripe, hsl(0 0% 0% / 0.02)));
14652
15721
  }
14653
15722
 
14654
15723
  :host([hoverable]) .snice-table--slotted .table-body::slotted(snice-row:hover) {
@@ -14754,9 +15823,18 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
14754
15823
  }
14755
15824
  container.appendChild(controlsDiv);
14756
15825
  }
14757
- // Re-derive the fill row whenever the frame's box changes (host resized,
14758
- // fullscreen toggled) the leftover space below the rows changes with it.
15826
+ // Re-derive the frame-dependent layout whenever the frame's box changes
15827
+ // (host resized, fullscreen toggled): the columns share its width, and the
15828
+ // leftover space below the rows changes with its height.
14759
15829
  handleFrameResize() {
15830
+ // Only when the width actually moved. Fitting rewrites header widths, which
15831
+ // can change row heights, which can flip the frame's vertical scrollbar and
15832
+ // call us straight back — the guard stops that becoming a cycle.
15833
+ const width = this.shadowRoot?.querySelector('.table-frame')?.clientWidth ?? 0;
15834
+ if (width > 0 && width !== this.lastFittedFrameWidth) {
15835
+ this.lastFittedFrameWidth = width;
15836
+ this.syncColumnWidthsToFrame();
15837
+ }
14760
15838
  this.updateFillRow();
14761
15839
  }
14762
15840
  async initialize() {
@@ -14859,7 +15937,18 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
14859
15937
  }
14860
15938
  }
14861
15939
  this.data.splice(toIndex, 0, item);
14862
- this.data = [...this.data];
15940
+ // Reordering the SAME rows is not a delivery: a failed reload keeps its
15941
+ // error until data actually arrives, so a drag must not dismiss it.
15942
+ // Everything else the data watcher does (row index, detail prep, render)
15943
+ // is still wanted here — hence a narrow flag rather than
15944
+ // `settingSortedData`, which skips all of it.
15945
+ this.reorderingRows = true;
15946
+ try {
15947
+ this.data = [...this.data];
15948
+ }
15949
+ finally {
15950
+ this.reorderingRows = false;
15951
+ }
14863
15952
  this.selectedRows = selectedRows
14864
15953
  .map((row) => this.indexOfRow(row))
14865
15954
  .filter((index) => index >= 0);
@@ -14973,7 +16062,17 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
14973
16062
  this.renderHeader(); // Re-render header to show/hide sort indicators
14974
16063
  }
14975
16064
  handleSelectableChange() {
14976
- this.render(); // Re-render both header and body for checkbox columns
16065
+ this.render(); // host template (the structural mode can change)
16066
+ // The checkbox tool column is structural: like the sibling selection-mode
16067
+ // watcher, a post-mount toggle has to rebuild the header (select-all th)
16068
+ // AND every row (row checkbox) — the host template render leaves the
16069
+ // imperatively built thead/tbody untouched, so without this the two
16070
+ // disagree and every data cell sits one column off its header. Rows are
16071
+ // never recycled stale across the toggle: computeStructuralSig folds in
16072
+ // `sel=`. Skipped before the first render — the initial paint reads
16073
+ // `selectable` itself.
16074
+ if (this.thead)
16075
+ this.scheduleRender('both');
14977
16076
  queueMicrotask(() => this.syncSlottedSelectionState());
14978
16077
  }
14979
16078
  // C1: reactive columns. A new column set can change cell types/formatting
@@ -14982,8 +16081,12 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
14982
16081
  // construction is deferred to the coalescing queue (setter-stack landmine).
14983
16082
  handleColumnsAssignment() {
14984
16083
  this.renderedRows = new Map();
16084
+ // A new array is a NEW CONFIGURATION: its order / pinned / width / declared
16085
+ // set replace whatever the column model is holding, and states for columns
16086
+ // it no longer declares are dropped. initialize() is the render-path resync
16087
+ // that must NOT do this (it would undo a drag-resize on every paint).
14985
16088
  if (this.columns.length > 0)
14986
- this.columnManager.initialize(this.columns, this);
16089
+ this.columnManager.applyConfiguration(this.columns, this);
14987
16090
  // F: a column set change can add/remove an `aggregate` — resync the grouping
14988
16091
  // model's aggregator list and drop its stale flattened snapshot.
14989
16092
  this.grouping.setColumns(this.columns);
@@ -15056,9 +16159,21 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15056
16159
  // and the row-index map, then renders the body. The internal sort path
15057
16160
  // (sortLocalData) reassigns this.data with a re-sorted view of the SAME rows
15058
16161
  // and owns its own snapshot/render — settingSortedData tells us to skip.
15059
- handleDataAssignment() {
16162
+ handleDataAssignment(oldVal) {
15060
16163
  if (this.settingSortedData)
15061
16164
  return;
16165
+ // A load error describes the LAST delivery ATTEMPT, so a dataset arriving
16166
+ // from any source supersedes it — including the app falling back to local
16167
+ // rows (or to none) after the remote load failed. Clearing it only on the
16168
+ // remote success path left `loadError` and the `table--error` host class
16169
+ // alive forever, so the next zero-row render showed the stale ⚠️ where the
16170
+ // empty state belonged.
16171
+ //
16172
+ // A REORDER is not an arrival: the row-reorder handler republishes the same
16173
+ // rows in a new array, and dragging a row must not dismiss the ⚠️ the
16174
+ // contract says survives until data actually arrives.
16175
+ if (!this.reorderingRows)
16176
+ this.clearLoadError();
15062
16177
  this.unsortedData = [...this.data];
15063
16178
  // C3: rows delivered while a client-side sort is active must land in that
15064
16179
  // sorted order — the currentSort watcher only fires when the sort itself
@@ -15067,11 +16182,25 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15067
16182
  if (this.mode !== 'remote' && this.currentSort.length > 0)
15068
16183
  this.applyLocalSortOrder();
15069
16184
  this.rebuildRowIndex();
16185
+ // Re-delivering the SAME rows in a different order must not move the
16186
+ // selection to whatever row inherited the index (no-op for a new dataset).
16187
+ if (oldVal)
16188
+ this.remapSelectionAfterReorder(oldVal);
15070
16189
  this.masterDetail.prepare(this.data, row => this.rowValueSignature(row));
15071
16190
  if (this.settingDataImperative)
15072
16191
  return; // imperative setData(): caller renders
15073
16192
  this.scheduleRender('body');
15074
16193
  }
16194
+ /**
16195
+ * Drop the failed-load state: the ⚠️ message row's text and the `table--error`
16196
+ * host class always move together, so they are cleared together and from one
16197
+ * place. Idempotent, and deliberately does NOT render — every caller is either
16198
+ * mid-delivery (the data watcher renders) or already renders itself.
16199
+ */
16200
+ clearLoadError() {
16201
+ this.loadError = null;
16202
+ this.classList.remove('table--error');
16203
+ }
15075
16204
  handleDataChange() {
15076
16205
  this.renderBody();
15077
16206
  this.updateLoadingOverlay();
@@ -15091,11 +16220,12 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15091
16220
  overlay = document.createElement('div');
15092
16221
  overlay.className = 'table-loading-overlay';
15093
16222
  overlay.setAttribute('part', 'loading-overlay');
15094
- const spinner = document.createElement('snice-progress');
15095
- spinner.setAttribute('variant', 'circular');
15096
- spinner.setAttribute('indeterminate', '');
15097
- spinner.setAttribute('size', 'small');
15098
- overlay.appendChild(spinner);
16223
+ // The overlay sits ON TOP of readable rows, so it carries its label as
16224
+ // an aria-label rather than visible text — but it must still announce.
16225
+ overlay.setAttribute('role', 'status');
16226
+ overlay.setAttribute('aria-live', 'polite');
16227
+ overlay.setAttribute('aria-label', LOADING_LABEL);
16228
+ overlay.appendChild(this.createLoadingSpinner('medium'));
15099
16229
  frame.appendChild(overlay);
15100
16230
  }
15101
16231
  else if (!show && overlay) {
@@ -15158,11 +16288,26 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15158
16288
  this.sortLocalData();
15159
16289
  }
15160
16290
  handleCurrentPageChange() {
16291
+ // A clamp written by the display model itself is not a navigation — see
16292
+ // clampCurrentPage().
16293
+ if (this.clampingPage)
16294
+ return;
15161
16295
  if (this.paginationMode === 'server' && this.mode === 'remote')
15162
16296
  this.debouncedDataRequest();
15163
16297
  else
15164
16298
  this.scheduleRender('body');
15165
16299
  }
16300
+ clampCurrentPage(page) {
16301
+ if (page === this.currentPage)
16302
+ return;
16303
+ this.clampingPage = true;
16304
+ try {
16305
+ this.currentPage = page;
16306
+ }
16307
+ finally {
16308
+ this.clampingPage = false;
16309
+ }
16310
+ }
15166
16311
  handlePageSizeChange() {
15167
16312
  this.currentPage = 1;
15168
16313
  if (this.paginationMode === 'server' && this.mode === 'remote')
@@ -15178,7 +16323,9 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15178
16323
  }
15179
16324
  handleListChange() {
15180
16325
  this.renderedRows = new Map();
15181
- this.scheduleRender('body');
16326
+ // The header is part of the mode: entering list mode with a renderer drops
16327
+ // the columnar header, and leaving it must bring the header back.
16328
+ this.scheduleRender('both');
15182
16329
  }
15183
16330
  handleEditableChange() {
15184
16331
  if (this.editable)
@@ -15236,6 +16383,17 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15236
16383
  this.initColumnMenu();
15237
16384
  this.scheduleRender('header');
15238
16385
  }
16386
+ /**
16387
+ * Switching fit policy re-decides every unsized column's width, so the frame
16388
+ * has to be re-fitted from scratch: the cached width would otherwise make the
16389
+ * resize observer's "did the frame move?" guard swallow the very next pass.
16390
+ */
16391
+ handleColumnFitChange() {
16392
+ this.columnManager.setFitMode(this.columnFit);
16393
+ this.lastFittedFrameWidth = 0;
16394
+ this.syncColumnWidthsToFrame();
16395
+ this.scheduleRender('header');
16396
+ }
15239
16397
  /** Column definitions in their actual painted order. Column state owns
15240
16398
  * visibility/order; pinned columns are partitioned to their physical edges
15241
16399
  * so right-pinned cells do not overlap later unpinned columns. */
@@ -15252,9 +16410,124 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15252
16410
  .map((state) => byKey.get(state.key))
15253
16411
  .filter((column) => !!column);
15254
16412
  }
16413
+ /**
16414
+ * Cells one full-width row spans: every visible data column plus the tool
16415
+ * columns that precede them. Group, aggregate, detail, empty-state, and
16416
+ * virtual-spacer rows all measure themselves against this — a row that spans
16417
+ * MORE than the table has invents columns, and `table-layout: fixed` then
16418
+ * spends the frame's leftover width on the phantoms.
16419
+ */
16420
+ getTotalColumnSpan() {
16421
+ const managed = this.columnManager.getAllStates();
16422
+ const dataColumns = managed.length > 0
16423
+ ? this.columnManager.getVisibleColumns().length
16424
+ : this.columns.length;
16425
+ return dataColumns
16426
+ + (this.hasSelectionColumn() ? 1 : 0)
16427
+ + (this.masterDetail.isEnabled() ? 1 : 0)
16428
+ + (this.rowReorder && this.rowDnD.isEnabled() ? 1 : 0);
16429
+ }
16430
+ /**
16431
+ * Re-fit the columns nobody sized to the frame they are painted in.
16432
+ *
16433
+ * A column width is a CONTENT width, so the frame's budget has to be reduced
16434
+ * by each column's own padding + border and by the fixed tool columns before
16435
+ * it can be shared out. Both are read off the live header — the previous
16436
+ * render's, on the first pass — rather than guessed, so the fitted table
16437
+ * lands on the frame's width instead of a few pixels past it.
16438
+ *
16439
+ * Widths are written straight onto the header cells: under fixed layout the
16440
+ * first row owns the column grid, so no body repaint is needed and this can
16441
+ * run inside a resize observer without a render cascade.
16442
+ */
16443
+ syncColumnWidthsToFrame() {
16444
+ // The policy is the table's, the arithmetic is the manager's: hand it over
16445
+ // before every fit so a `columnFit` change is honoured on the next pass
16446
+ // whether it arrived as a property, an attribute, or an upgrade.
16447
+ this.columnManager.setFitMode(this.columnFit);
16448
+ const frame = this.shadowRoot?.querySelector('.table-frame');
16449
+ const headerRow = this.shadowRoot?.querySelector('thead tr.column-header-row');
16450
+ if (!frame || !headerRow)
16451
+ return false;
16452
+ const available = frame.clientWidth;
16453
+ // Layoutless DOM (or a display:none host): every measurement is 0 and
16454
+ // fitting to it would slam the columns to their minimums for nothing.
16455
+ if (!(available > 0))
16456
+ return false;
16457
+ const dataCells = Array.from(headerRow.querySelectorAll('th[data-key]'));
16458
+ // An active list renderer drops the columnar header entirely; there is no
16459
+ // column geometry to fit and no cell to measure the chrome from.
16460
+ if (dataCells.length === 0)
16461
+ return false;
16462
+ let reserved = 0;
16463
+ for (const cell of Array.from(headerRow.children)) {
16464
+ if (!cell.hasAttribute('data-key'))
16465
+ reserved += cell.getBoundingClientRect().width;
16466
+ }
16467
+ const chrome = horizontalCellChrome(dataCells[0]);
16468
+ if (!this.columnManager.fitVisibleColumns(available - reserved, chrome))
16469
+ return false;
16470
+ for (const cell of dataCells) {
16471
+ const state = this.columnManager.getState(cell.getAttribute('data-key') || '');
16472
+ if (state)
16473
+ cell.style.width = `${state.width}px`;
16474
+ }
16475
+ return true;
16476
+ }
16477
+ /**
16478
+ * Sticky geometry plus the visible affordance for a pinned cell.
16479
+ *
16480
+ * Freezing a column is only communicated if the reader can see where the
16481
+ * frozen region ends, so the inner-most pinned column on each side carries
16482
+ * the divider (`--edge`); the header additionally carries a pin marker, which
16483
+ * is the only cue when the table happens to be wide enough not to scroll.
16484
+ */
16485
+ applyPinnedCell(cell, key, state, zIndex) {
16486
+ if (state.pinned !== 'left' && state.pinned !== 'right')
16487
+ return;
16488
+ const side = state.pinned;
16489
+ const columns = side === 'left'
16490
+ ? this.columnManager.getPinnedLeft()
16491
+ : this.columnManager.getPinnedRight();
16492
+ const offsets = side === 'left'
16493
+ ? this.columnManager.getPinnedLeftOffsets()
16494
+ : this.columnManager.getPinnedRightOffsets();
16495
+ // The edge of the frozen region: the last left-pinned column, the first
16496
+ // right-pinned one. Inner boundaries between two pinned columns are just
16497
+ // ordinary cell borders.
16498
+ const edgeKey = side === 'left' ? columns[columns.length - 1]?.key : columns[0]?.key;
16499
+ cell.classList.add('pinned-cell', `pinned-cell--${side}`);
16500
+ if (key === edgeKey)
16501
+ cell.classList.add('pinned-cell--edge');
16502
+ cell.style.position = 'sticky';
16503
+ cell.style[side] = `${offsets.get(key) ?? 0}px`;
16504
+ cell.style.zIndex = zIndex;
16505
+ }
16506
+ /** The pin marker a frozen header carries, so pinning reads at any width. */
16507
+ static pinIndicator(side) {
16508
+ return `<span class="pin-indicator pin-indicator--${side}" aria-hidden="true">`
16509
+ + `<svg width="10" height="10" viewBox="0 0 16 16" fill="currentColor">`
16510
+ + `<path d="M9.5 1a1 1 0 0 0-.7 1.7l.3.3-3 3-2.4-.5a1 1 0 0 0-.9 1.7l3 3-3.6 3.6a.6.6 0 0 0 .8.8L6.6 12l3 3a1 1 0 0 0 1.7-.9l-.5-2.4 3-3 .3.3a1 1 0 0 0 1.4-1.4l-5.3-5.3A1 1 0 0 0 9.5 1Z"/>`
16511
+ + `</svg></span>`;
16512
+ }
16513
+ /**
16514
+ * List mode is only "list mode" once a renderer is installed: `list` alone is
16515
+ * a border treatment and still paints normal columnar cells. With a renderer
16516
+ * every row collapses to one full-width cell, so there is no column geometry
16517
+ * left for a header to label.
16518
+ */
16519
+ usesListRowRenderer() {
16520
+ return !!(this.list && this.listRenderer);
16521
+ }
15255
16522
  renderHeader() {
15256
16523
  if (!this.thead)
15257
16524
  return;
16525
+ // A card/directory list has no columns, so a columnar header would promise
16526
+ // fields that align with nothing. Suppress the data-column headers (and the
16527
+ // equally columnar group and header-filter rows) while a list renderer is
16528
+ // active; the tool columns below still have real body cells to head, so
16529
+ // select-all is kept rather than lost as collateral.
16530
+ const listRows = this.usesListRowRenderer();
15258
16531
  const headerRow = document.createElement('tr');
15259
16532
  headerRow.className = 'column-header-row';
15260
16533
  // Tool column headers — must match createRow order
@@ -15292,7 +16565,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15292
16565
  }
15293
16566
  headerRow.appendChild(selectCell);
15294
16567
  }
15295
- const visibleColumns = this.getVisibleColumnDefinitions();
16568
+ const visibleColumns = listRows ? [] : this.getVisibleColumnDefinitions();
15296
16569
  visibleColumns.forEach(column => {
15297
16570
  const th = document.createElement('th');
15298
16571
  th.setAttribute('data-key', column.key);
@@ -15309,23 +16582,10 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15309
16582
  }
15310
16583
  // Apply column width
15311
16584
  const state = this.columnManager.getState(column.key);
16585
+ const pinnedSide = state?.pinned === 'left' || state?.pinned === 'right' ? state.pinned : null;
15312
16586
  if (state) {
15313
16587
  th.style.width = `${state.width}px`;
15314
- // Pinned column sticky positioning
15315
- if (state.pinned === 'left') {
15316
- const offsets = this.columnManager.getPinnedLeftOffsets();
15317
- th.classList.add('pinned-cell');
15318
- th.style.position = 'sticky';
15319
- th.style.left = `${offsets.get(column.key) ?? 0}px`;
15320
- th.style.zIndex = '2';
15321
- }
15322
- else if (state.pinned === 'right') {
15323
- const offsets = this.columnManager.getPinnedRightOffsets();
15324
- th.classList.add('pinned-cell');
15325
- th.style.position = 'sticky';
15326
- th.style.right = `${offsets.get(column.key) ?? 0}px`;
15327
- th.style.zIndex = '2';
15328
- }
16588
+ this.applyPinnedCell(th, column.key, state, '2');
15329
16589
  }
15330
16590
  if (this.sortable && column.sortable !== false) {
15331
16591
  th.classList.add('sortable');
@@ -15333,11 +16593,22 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15333
16593
  // A role=button must be focusable and named, or the keyboard cannot
15334
16594
  // sort at all. Matches the declarative header in snice-header.ts.
15335
16595
  th.setAttribute('tabindex', '0');
15336
- th.setAttribute('aria-label', `Sort by ${column.label}`);
16596
+ // Frozen columns say so: the divider is a visual cue only.
16597
+ th.setAttribute('aria-label', pinnedSide
16598
+ ? `Sort by ${column.label}, pinned ${pinnedSide}`
16599
+ : `Sort by ${column.label}`);
15337
16600
  th.innerHTML = this.renderSortableHeader(column);
15338
16601
  }
15339
16602
  else {
15340
16603
  th.textContent = column.label;
16604
+ if (pinnedSide)
16605
+ th.setAttribute('aria-label', `${column.label}, pinned ${pinnedSide}`);
16606
+ }
16607
+ if (pinnedSide) {
16608
+ // Inside the label, not after it: `.sort-header` is a full-width flex
16609
+ // row, so a sibling would be pushed onto a second line.
16610
+ const label = th.querySelector('.sort-header > span') ?? th;
16611
+ label.insertAdjacentHTML('beforeend', SniceTable.pinIndicator(pinnedSide));
15341
16612
  }
15342
16613
  // Filter indicator
15343
16614
  if (this.filterEngine.hasColumnFilter(column.key)) {
@@ -15390,7 +16661,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15390
16661
  });
15391
16662
  this.thead.innerHTML = '';
15392
16663
  // Column groups header row (if any)
15393
- const groups = this.columnManager.getColumnGroups();
16664
+ const groups = listRows ? [] : this.columnManager.getColumnGroups();
15394
16665
  if (groups.length > 0) {
15395
16666
  const groupRow = document.createElement('tr');
15396
16667
  groupRow.className = 'column-group-row';
@@ -15412,9 +16683,12 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15412
16683
  groupRow.appendChild(groupedHeaders.content);
15413
16684
  this.thead.appendChild(groupRow);
15414
16685
  }
15415
- this.thead.appendChild(headerRow);
16686
+ // A list-mode header row with no tool columns has nothing to show; leaving
16687
+ // it out keeps the thead empty so it paints no strip above the cards.
16688
+ if (!listRows || headerRow.children.length > 0)
16689
+ this.thead.appendChild(headerRow);
15416
16690
  // Header filter row (if enabled)
15417
- if (this.headerFilters) {
16691
+ if (this.headerFilters && !listRows) {
15418
16692
  const filterRow = document.createElement('tr');
15419
16693
  filterRow.className = 'header-filter-row';
15420
16694
  if (this.rowReorder && this.rowDnD.isEnabled()) {
@@ -15451,6 +16725,11 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15451
16725
  });
15452
16726
  this.thead.appendChild(filterRow);
15453
16727
  }
16728
+ // The header is in the DOM, so its real chrome and tool-column widths are
16729
+ // measurable: fit the unsized columns to the frame before the browser
16730
+ // paints. Widths land on the header cells that were just created, so this
16731
+ // never re-enters renderHeader.
16732
+ this.syncColumnWidthsToFrame();
15454
16733
  }
15455
16734
  renderSortableHeader(column) {
15456
16735
  const sortItem = this.currentSort.find(s => s.column === column.key);
@@ -15482,9 +16761,90 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15482
16761
  </div>
15483
16762
  `;
15484
16763
  }
16764
+ /**
16765
+ * The single message row a zero-row body shows: loading spinner, load error,
16766
+ * or the empty state (the `empty-state` slot cloned fresh, else the default
16767
+ * placeholder). Returns null when there is data to render. Shared by the
16768
+ * ordinary and virtualized paths — the virtualizer wipes the tbody itself, so
16769
+ * it asks for this row rather than leaving a virtualized empty table blank.
16770
+ */
16771
+ /**
16772
+ * The spinner both loading affordances draw. `snice-progress` paints its
16773
+ * track at 20% alpha, which vanishes against a dark surface — the table pins
16774
+ * `--progress-track-opacity` in its own stylesheet so the ring reads on any
16775
+ * theme; the indicator arc keeps the primary color.
16776
+ */
16777
+ createLoadingSpinner(size) {
16778
+ const spinner = document.createElement('snice-progress');
16779
+ spinner.setAttribute('variant', 'circular');
16780
+ spinner.setAttribute('indeterminate', '');
16781
+ spinner.setAttribute('size', size);
16782
+ return spinner;
16783
+ }
16784
+ createEmptyBodyRow() {
16785
+ if (this.data.length > 0 || this.columns.length === 0)
16786
+ return null;
16787
+ const toolCols = (this.hasSelectionColumn() ? 1 : 0)
16788
+ + (this.masterDetail.isEnabled() ? 1 : 0)
16789
+ + (this.rowReorder && this.rowDnD.isEnabled() ? 1 : 0);
16790
+ const tr = document.createElement('tr');
16791
+ tr.className = 'table-message-row';
16792
+ const td = document.createElement('td');
16793
+ td.colSpan = this.columns.length + toolCols;
16794
+ td.className = 'no-data';
16795
+ tr.appendChild(td);
16796
+ if (this.loading) {
16797
+ // A bare ring in an otherwise empty rectangle reads as a rendering
16798
+ // artefact, not as "we are fetching your rows": at `small` it is 24px of
16799
+ // low-contrast stroke with nothing naming it. The zero-row body has the
16800
+ // space for a full-size spinner AND the word, and the pair is what makes
16801
+ // the state legible — visually and to a screen reader.
16802
+ const status = document.createElement('div');
16803
+ status.className = 'table-loading-state';
16804
+ status.setAttribute('role', 'status');
16805
+ status.setAttribute('aria-live', 'polite');
16806
+ status.appendChild(this.createLoadingSpinner('medium'));
16807
+ const label = document.createElement('span');
16808
+ label.className = 'table-loading-label';
16809
+ label.textContent = LOADING_LABEL;
16810
+ status.appendChild(label);
16811
+ td.appendChild(status);
16812
+ }
16813
+ else if (this.loadError) {
16814
+ // Surfaces a failed remote load instead of silently falling through to
16815
+ // the generic "No data" empty state.
16816
+ const errorDiv = document.createElement('div');
16817
+ errorDiv.className = 'table-error-message';
16818
+ errorDiv.textContent = `⚠️ ${this.loadError}`;
16819
+ td.appendChild(errorDiv);
16820
+ }
16821
+ else {
16822
+ const slotted = this.querySelector('[slot="empty-state"]');
16823
+ if (slotted) {
16824
+ td.appendChild(slotted.cloneNode(true));
16825
+ }
16826
+ else {
16827
+ const empty = document.createElement('snice-empty-state');
16828
+ empty.setAttribute('size', 'small');
16829
+ empty.setAttribute('icon', '📭');
16830
+ empty.setAttribute('title', 'No data');
16831
+ empty.setAttribute('description', 'No records to display.');
16832
+ td.appendChild(empty);
16833
+ }
16834
+ }
16835
+ return tr;
16836
+ }
15485
16837
  renderBody() {
15486
- if (!this.tbody)
16838
+ // The queue flush consumed `pendingBodyRender` before calling us, so a body
16839
+ // render that arrives while the shadow template has not committed its
16840
+ // <tbody> (a `table/config` response landing before @ready, a structural
16841
+ // branch flip mid-flight) would simply vanish — the header repaints and the
16842
+ // rows stay stale forever. Re-arm it for the next frame instead.
16843
+ if (!this.tbody) {
16844
+ this.rearmBodyRender();
15487
16845
  return;
16846
+ }
16847
+ this.bodyRenderRetries = 0;
15488
16848
  // Update column manager when columns change
15489
16849
  if (this.columns.length > 0) {
15490
16850
  this.columnManager.initialize(this.columns, this);
@@ -15516,8 +16876,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15516
16876
  // NOT the raw filtered data — otherwise the virtualizer windows the wrong
15517
16877
  // dataset and expand/collapse silently no-ops.
15518
16878
  if (this.virtualize && this.virtualizer.isEnabled()) {
15519
- this.virtualRowsSnapshot = this.getVirtualRows();
15520
- this.virtualizer.setTotalRows(this.virtualRowsSnapshot.length);
16879
+ this.syncVirtualRows();
15521
16880
  // The virtualizer's afterRender hook re-establishes grid ARIA/focus on
15522
16881
  // the freshly inserted window.
15523
16882
  // Still render pagination
@@ -15526,64 +16885,15 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15526
16885
  this.updateSelectAllState();
15527
16886
  return;
15528
16887
  }
15529
- if (this.data.length === 0 && this.columns.length > 0) {
16888
+ const emptyBodyRow = this.createEmptyBodyRow();
16889
+ if (emptyBodyRow) {
15530
16890
  // Empty / loading / error states replace the body with a single message
15531
16891
  // row. Reset the recycler map so a later data render starts from a clean
15532
16892
  // slate (no stale reuse candidates pointing at removed rows).
15533
16893
  this.tbody.innerHTML = '';
15534
16894
  this.renderedRows = new Map();
15535
- const toolCols = (this.hasSelectionColumn() ? 1 : 0)
15536
- + (this.masterDetail.isEnabled() ? 1 : 0)
15537
- + (this.rowReorder && this.rowDnD.isEnabled() ? 1 : 0);
15538
- const colSpan = this.columns.length + toolCols;
15539
- if (this.loading) {
15540
- // Show loading spinner
15541
- const tr = document.createElement('tr');
15542
- const td = document.createElement('td');
15543
- td.colSpan = colSpan;
15544
- td.className = 'no-data';
15545
- td.innerHTML = '<snice-progress variant="circular" indeterminate size="small"></snice-progress>';
15546
- tr.appendChild(td);
15547
- this.tbody.appendChild(tr);
15548
- return;
15549
- }
15550
- else if (this.loadError) {
15551
- // Show error state — surfaces a failed remote load instead of
15552
- // silently falling through to the generic "No data" empty state.
15553
- const tr = document.createElement('tr');
15554
- const td = document.createElement('td');
15555
- td.colSpan = colSpan;
15556
- td.className = 'no-data';
15557
- const errorDiv = document.createElement('div');
15558
- errorDiv.className = 'table-error-message';
15559
- errorDiv.textContent = `⚠️ ${this.loadError}`;
15560
- td.appendChild(errorDiv);
15561
- tr.appendChild(td);
15562
- this.tbody.appendChild(tr);
15563
- return;
15564
- }
15565
- else {
15566
- // Show empty state
15567
- const tr = document.createElement('tr');
15568
- const td = document.createElement('td');
15569
- td.colSpan = colSpan;
15570
- td.className = 'no-data';
15571
- const slotted = this.querySelector('[slot="empty-state"]');
15572
- if (slotted) {
15573
- td.appendChild(slotted.cloneNode(true));
15574
- }
15575
- else {
15576
- const empty = document.createElement('snice-empty-state');
15577
- empty.setAttribute('size', 'small');
15578
- empty.setAttribute('icon', '📭');
15579
- empty.setAttribute('title', 'No data');
15580
- empty.setAttribute('description', 'No records to display.');
15581
- td.appendChild(empty);
15582
- }
15583
- tr.appendChild(td);
15584
- this.tbody.appendChild(tr);
15585
- return;
15586
- }
16895
+ this.tbody.appendChild(emptyBodyRow);
16896
+ return;
15587
16897
  }
15588
16898
  // Apply client-side filters (cached snapshot — see getFilteredData).
15589
16899
  const filteredData = this.getFilteredData();
@@ -15595,15 +16905,23 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15595
16905
  if (this.pagination && this.paginationMode === 'client'
15596
16906
  && !this.usesGroupingDisplayModel() && !this.treeData.isEnabled()) {
15597
16907
  this.totalItems = filteredData.length;
15598
- const startIndex = (this.currentPage - 1) * this.pageSize;
16908
+ // Re-clamp before slicing. A shorter re-delivery (or a filter) can leave
16909
+ // `currentPage` past the new last page; slicing at the stale offset lands
16910
+ // off the end and renders an EMPTY body while the pager still advertises
16911
+ // the tail rows — blank until the user clicks a page button. Same
16912
+ // [1, totalPages] clamp goToPage() applies, and the same one the grouping
16913
+ // and tree display models already do before their own slice.
16914
+ // An empty set is left alone: a render before the rows arrive must not
16915
+ // rewrite the host's declared starting page.
16916
+ const totalPages = filteredData.length > 0
16917
+ ? Math.ceil(filteredData.length / this.pageSize)
16918
+ : this.currentPage;
16919
+ const page = Math.max(1, Math.min(this.currentPage, totalPages));
16920
+ this.clampCurrentPage(page);
16921
+ const startIndex = (page - 1) * this.pageSize;
15599
16922
  displayData = filteredData.slice(startIndex, startIndex + this.pageSize);
15600
16923
  }
15601
- const extraCols = (this.hasSelectionColumn() ? 1 : 0) + (this.masterDetail.isEnabled() ? 1 : 0) + (this.rowReorder && this.rowDnD.isEnabled() ? 1 : 0);
15602
- const managedColumns = this.columnManager.getAllStates();
15603
- const visibleColumnCount = managedColumns.length > 0
15604
- ? this.columnManager.getVisibleColumns().length
15605
- : this.columns.length;
15606
- const totalColSpan = visibleColumnCount + extraCols;
16924
+ const totalColSpan = this.getTotalColumnSpan();
15607
16925
  const structuralSig = this.computeStructuralSig();
15608
16926
  // Task B: describe the desired body as an ordered list of keyed entries,
15609
16927
  // then reconcile the tbody to match. Rows whose object + signature are
@@ -15699,6 +17017,19 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15699
17017
  restampIndex: index,
15700
17018
  create: () => this.createRow(rowData, index, treeRow),
15701
17019
  });
17020
+ // Tree data must compose with master-detail exactly like the ordinary
17021
+ // and grouping paths: createRow renders the toggle for every tree row,
17022
+ // so omitting this entry left that control inert. Generated gap nodes
17023
+ // (index -1) carry no data row and therefore no panel.
17024
+ if (index >= 0 && this.masterDetail.isEnabled() && this.masterDetail.isExpanded(index)) {
17025
+ entries.push({
17026
+ key: `__detail_${index}`,
17027
+ sig: 'detail',
17028
+ alwaysRebuild: true,
17029
+ create: () => this.masterDetail.createDetailRow(rowData, index, totalColSpan) ??
17030
+ document.createElement('tr'),
17031
+ });
17032
+ }
15702
17033
  });
15703
17034
  }
15704
17035
  else {
@@ -15756,6 +17087,20 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15756
17087
  * data row, so column widths, pinning, and alignment track the table
15757
17088
  * engine natively. Removed entirely when content fills (or overflows) the
15758
17089
  * frame, restoring the plain last-row styling.
17090
+ *
17091
+ * INVARIANT — the filler may only consume space the frame ALREADY has; space
17092
+ * that exists only BECAUSE of the filler must be given back. The filler is
17093
+ * table content, and `@observe('resize', '.table-frame')` re-derives it from
17094
+ * the frame's box, so on a host whose height is content-driven (the
17095
+ * documented `:host { height: 100% }` resolving against an auto or
17096
+ * max-content container — a stretched grid/flex item, a plain block parent)
17097
+ * the filler is an input to the very measurement that produced it: the frame
17098
+ * grows, the observer fires, the leftover computes positive again, and the
17099
+ * table inflates by one slack per animation frame without bound. No static
17100
+ * check distinguishes a content-driven frame from a genuinely sized one, so
17101
+ * this measures instead: apply the filler, then confirm the frame kept its
17102
+ * height. Any growth is self-inflicted and is subtracted from the filler;
17103
+ * once nothing honest is left, the frame had no free space to fill at all.
15759
17104
  */
15760
17105
  updateFillRow() {
15761
17106
  const frame = this.shadowRoot?.querySelector('.table-frame');
@@ -15770,22 +17115,45 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15770
17115
  const lastRow = rows[rows.length - 1];
15771
17116
  if (!lastRow)
15772
17117
  return;
15773
- const leftover = Math.floor(frame.clientHeight - tableEl.offsetHeight);
17118
+ // Baseline: the frame's height with no filler of ours in the box. Every
17119
+ // later reading is compared against it, so the correction below can never
17120
+ // hand back more than the filler itself created.
17121
+ const baseline = frame.clientHeight;
17122
+ let leftover = Math.floor(baseline - tableEl.offsetHeight);
15774
17123
  if (leftover <= 1)
15775
17124
  return;
15776
17125
  const fill = lastRow.cloneNode(true);
15777
17126
  fill.className = 'table-fill-row';
15778
- for (const attr of ['data-index', 'data-selected', 'role', 'aria-rowindex', 'aria-selected', 'tabindex', 'id']) {
17127
+ // `part` goes with the rest of the row identity: the filler is not "each
17128
+ // native body row", so a consumer's ::part(row)/::part(cell) rule must not
17129
+ // paint a phantom row across it.
17130
+ for (const attr of ['data-index', 'data-selected', 'role', 'aria-rowindex', 'aria-selected', 'tabindex', 'id', 'part']) {
15779
17131
  fill.removeAttribute(attr);
15780
17132
  }
15781
17133
  fill.setAttribute('aria-hidden', 'true');
15782
17134
  fill.querySelectorAll('td').forEach(td => {
15783
17135
  td.replaceChildren();
15784
- for (const attr of ['role', 'aria-colindex', 'tabindex', 'aria-selected'])
17136
+ for (const attr of ['role', 'aria-colindex', 'tabindex', 'aria-selected', 'part'])
15785
17137
  td.removeAttribute(attr);
15786
17138
  });
15787
17139
  fill.style.height = `${leftover}px`;
15788
17140
  tbody.appendChild(fill);
17141
+ // Feedback check. Reading `clientHeight` flushes layout, so each pass sees
17142
+ // the frame as it stands WITH the filler in it. A frame that kept its
17143
+ // baseline is genuinely sized and the filler stays; growth means the frame
17144
+ // is (partly) sized by its own content, so give exactly that back. Two
17145
+ // corrections settle every real case — a fully self-made leftover reaches
17146
+ // zero on the first — and the bounded loop guarantees termination.
17147
+ for (let attempt = 0; attempt < 3; attempt++) {
17148
+ const selfMade = frame.clientHeight - baseline;
17149
+ if (selfMade <= 0)
17150
+ return;
17151
+ leftover -= selfMade;
17152
+ if (leftover <= 1)
17153
+ break;
17154
+ fill.style.height = `${leftover}px`;
17155
+ }
17156
+ fill.remove();
15789
17157
  }
15790
17158
  // ── Task B: render-path reconciler ──────────────────────────────────────
15791
17159
  //
@@ -15943,7 +17311,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
15943
17311
  restampRowIndex(el, index) {
15944
17312
  el.setAttribute('data-index', String(index));
15945
17313
  const isSelected = this.selectedRows.includes(index);
15946
- el.setAttribute('data-selected', String(isSelected));
17314
+ this.stampRowSelection(el, isSelected);
15947
17315
  const checkbox = el.querySelector('snice-checkbox.row-select');
15948
17316
  if (checkbox) {
15949
17317
  checkbox.setAttribute('data-row-index', String(index));
@@ -16130,24 +17498,30 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
16130
17498
  span.textContent = rendered == null ? '' : String(rendered);
16131
17499
  return span;
16132
17500
  }
16133
- const tagName = this.getCellTagName(column.type);
17501
+ // A declared display formatter resolves the whole pipeline here — the
17502
+ // documented precedence is `formatter` first, `valueFormatter` as the
17503
+ // fallback — and its output is TEXT, so the column renders through the text
17504
+ // cell: a rating/progress/link/boolean cell cannot carry a display string
17505
+ // (its value channel is typed) and renders structure rather than text. This
17506
+ // is the rule the declarative <snice-row> path already uses.
17507
+ const displayText = this.cellDisplayText(column, value, row);
17508
+ const tagName = displayText === null ? this.getCellTagName(column.type) : 'snice-cell-text';
16134
17509
  const el = document.createElement(tagName);
16135
17510
  // Base attributes. Only force `align` if the column definition specifies
16136
17511
  // one; otherwise let the cell pick its type-appropriate default (numbers
16137
17512
  // right, rating/boolean center, text left). Hard-coding 'left' here fought
16138
- // the cell's own type-based text-align and made number values drift.
17513
+ // the cell's own type-based text-align and made number values drift — and a
17514
+ // formatted column keeps its TYPE's default, not the text cell's.
16139
17515
  el.setAttribute('type', column.type || 'text');
16140
17516
  if (column.align)
16141
17517
  el.setAttribute('align', column.align);
17518
+ else if (displayText !== null)
17519
+ el.setAttribute('align', defaultCellAlign(column.type));
16142
17520
  el.setAttribute('in-table', 'true');
16143
- // A declared display formatter makes the cell's value its DISPLAY text:
16144
- // the pipeline (valueGetter -> formatter, valueFormatter as the fallback)
16145
- // is resolved here, once, so the `value` channel a caller reads is what the
16146
- // cell actually shows. The cell is marked `formatted` and handed a column
16147
- // without the formatters, which keeps it from formatting again and tells
16148
- // the shared presentation contract to paint the text over any type-specific
16149
- // rendering (a rating/progress/link cell cannot render display text).
16150
- const displayText = this.cellDisplayText(column, value, row);
17521
+ // The cell's value IS its display value: with a formatter declared the cell
17522
+ // receives the formatted text and a column with the formatters removed, so
17523
+ // nothing formats twice and the `value` channel a caller reads is what the
17524
+ // cell shows.
16151
17525
  const cellColumn = displayText === null
16152
17526
  ? column
16153
17527
  : { ...column, formatter: undefined, valueFormatter: undefined };
@@ -16155,7 +17529,6 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
16155
17529
  // JSON, action, and tag cells without a lossy attribute conversion. Keep a
16156
17530
  // primitive attribute for declarative inspection and CSS selectors.
16157
17531
  if (displayText !== null) {
16158
- el.setAttribute('formatted', '');
16159
17532
  el.value = displayText;
16160
17533
  el.setAttribute('value', displayText);
16161
17534
  }
@@ -16165,9 +17538,12 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
16165
17538
  el.data = value;
16166
17539
  }
16167
17540
  else {
16168
- el.value = value ?? '';
16169
- if (value === null || typeof value !== 'object') {
16170
- el.setAttribute('value', String(value ?? ''));
17541
+ // An empty row value (null, or a field the row never carried) falls back
17542
+ // to the typed cell's own documented empty value, never to a blanket ''.
17543
+ const resolved = value ?? emptyCellValue(column.type);
17544
+ el.value = resolved;
17545
+ if (resolved !== null && typeof resolved !== 'object') {
17546
+ el.setAttribute('value', String(resolved));
16171
17547
  }
16172
17548
  }
16173
17549
  // Column definition as property (for formatter access etc.)
@@ -16384,16 +17760,36 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
16384
17760
  updateRowSelectionState() {
16385
17761
  if (!this.tbody)
16386
17762
  return;
16387
- const rows = this.tbody.querySelectorAll('tr');
16388
- rows.forEach((row, index) => {
17763
+ // Key off each row's own `data-index`, never its DOM position: group
17764
+ // headers, aggregate footers, virtual spacers, and the inert fill row share
17765
+ // the tbody, so a positional walk hands a raw-data index to a non-data row
17766
+ // (and drifts every real row after it — on page 2 of a client-paged table
17767
+ // the offsets never lined up at all).
17768
+ const rows = this.tbody.querySelectorAll('tr[data-index]');
17769
+ rows.forEach(row => {
17770
+ const index = Number(row.getAttribute('data-index'));
16389
17771
  const isSelected = this.selectedRows.includes(index);
16390
- row.setAttribute('data-selected', String(isSelected));
17772
+ this.stampRowSelection(row, isSelected);
16391
17773
  const checkbox = row.querySelector('snice-checkbox.row-select');
16392
17774
  if (checkbox) {
16393
17775
  checkbox.checked = isSelected;
16394
17776
  }
16395
17777
  });
16396
17778
  }
17779
+ /**
17780
+ * The two selection reflections a row carries, written together: `data-selected`
17781
+ * drives the CSS, `aria-selected` is the documented a11y contract ("selected
17782
+ * data rows expose aria-selected"). The keyboard module stamps ARIA only on a
17783
+ * full body render, so the delta paths below have to carry it themselves —
17784
+ * otherwise a screen reader never hears an interaction-driven selection.
17785
+ */
17786
+ stampRowSelection(row, isSelected) {
17787
+ row.setAttribute('data-selected', String(isSelected));
17788
+ if (isSelected)
17789
+ row.setAttribute('aria-selected', 'true');
17790
+ else
17791
+ row.removeAttribute('aria-selected');
17792
+ }
16397
17793
  // A2: reflect one row's selection into the DOM (located by data-index). The
16398
17794
  // reconciler (Task B) re-stamps data-index on moved rows, so this stays
16399
17795
  // correct after reorder. No-op if the row isn't in the current window.
@@ -16403,7 +17799,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
16403
17799
  const row = this.tbody.querySelector(`tr[data-index="${index}"]`);
16404
17800
  if (!row)
16405
17801
  return;
16406
- row.setAttribute('data-selected', String(isSelected));
17802
+ this.stampRowSelection(row, isSelected);
16407
17803
  const checkbox = row.querySelector('snice-checkbox.row-select');
16408
17804
  if (checkbox)
16409
17805
  checkbox.checked = isSelected;
@@ -16504,14 +17900,54 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
16504
17900
  this.settingSortedData = false;
16505
17901
  }
16506
17902
  sortLocalData() {
17903
+ const previous = this.data;
16507
17904
  this.applyLocalSortOrder();
16508
17905
  // Sort replaced this.data (new order + reference) — resync the row-index
16509
17906
  // map and drop the stale filtered snapshot before re-rendering. Render is
16510
17907
  // queued (not synchronous): sortLocalData is reachable from the currentSort
16511
17908
  // setter stack via handleControlledSort, and cells must never construct there.
16512
17909
  this.rebuildRowIndex();
17910
+ this.remapSelectionAfterReorder(previous);
16513
17911
  this.scheduleRender('body');
16514
17912
  }
17913
+ /**
17914
+ * Selection is anchored to the ROW, not to its position. `selectedRows` holds
17915
+ * indices into `data`, so any reorder of that array — a local sort, or a
17916
+ * re-delivery of the same row objects in another order — has to re-resolve
17917
+ * them; otherwise the indices keep pointing at whatever row now occupies the
17918
+ * slot and the user's checkmark silently jumps to a different row (the same
17919
+ * remap the row-reorder/DnD path performs).
17920
+ *
17921
+ * Only a permutation is remapped: same length, same identities, no duplicates.
17922
+ * A genuinely new dataset keeps the documented raw-index semantics untouched,
17923
+ * and rows carrying no identity (duplicate objects) have nothing to anchor to.
17924
+ * Call AFTER rebuildRowIndex() — the remap reads the fresh row-index map.
17925
+ */
17926
+ remapSelectionAfterReorder(previous) {
17927
+ if (this.selectedRows.length === 0)
17928
+ return;
17929
+ if (previous === this.data || previous.length !== this.data.length)
17930
+ return;
17931
+ const identities = new Set(this.data);
17932
+ if (identities.size !== this.data.length)
17933
+ return;
17934
+ // BOTH sides have to be duplicate-free for "same length + subset" to mean
17935
+ // permutation: previous=[A,A] against data=[A,B] satisfies the subset test
17936
+ // while actually being a NEW dataset, and remapping it would move the
17937
+ // selection off the row the raw-index contract points at.
17938
+ if (new Set(previous).size !== previous.length)
17939
+ return;
17940
+ for (const row of previous)
17941
+ if (!identities.has(row))
17942
+ return;
17943
+ const remapped = this.selectedRows
17944
+ .map((index) => this.indexOfRow(previous[index]))
17945
+ .filter((index) => index >= 0);
17946
+ if (remapped.length === this.selectedRows.length
17947
+ && remapped.every((index, i) => index === this.selectedRows[i]))
17948
+ return;
17949
+ this.selectedRows = remapped;
17950
+ }
16515
17951
  dispatchRowSelectionChanged(rowIndex, selected) {
16516
17952
  return {
16517
17953
  selectedRows: this.selectedRows,
@@ -16631,20 +18067,37 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
16631
18067
  const scrollContainer = this.getScrollContainer();
16632
18068
  if (!scrollContainer)
16633
18069
  return;
16634
- this.virtualRowsSnapshot = this.getVirtualRows();
18070
+ this.syncVirtualRows();
16635
18071
  this.virtualizer.attach({
16636
18072
  rowHeight: (displayIndex) => this.getVirtualRowHeight(displayIndex),
16637
18073
  bufferPx: this.virtualBuffer,
16638
18074
  totalRows: this.virtualRowsSnapshot.length,
16639
18075
  scrollContainer,
18076
+ // The host template swaps its structural branch (slotted rows ⇄ native
18077
+ // table) without re-attaching. Re-resolving keeps the window painting into
18078
+ // the LIVE tbody instead of the detached branch's orphaned one.
18079
+ resolveScrollContainer: () => this.getScrollContainer(),
16640
18080
  renderRange: (start, end) => this.renderRowRange(start, end),
18081
+ // Spacers span the real grid. A spacer wider than the table invents
18082
+ // columns for fixed layout to spend the frame's spare width on.
18083
+ resolveColumnSpan: () => this.getTotalColumnSpan(),
16641
18084
  // Pinned rows live outside the windowed range so they are always present,
16642
18085
  // exactly like the non-virtual path (renderBody).
16643
18086
  renderPinnedTop: () => this.renderPinnedRows(this.pinnedTopRows, 'top'),
16644
18087
  renderPinnedBottom: () => this.renderPinnedRows(this.pinnedBottomRows, 'bottom'),
18088
+ // A zero-row window still owes the user the empty/loading/error row the
18089
+ // ordinary path renders.
18090
+ renderEmptyBody: () => this.createEmptyBodyRow(),
16645
18091
  // Scroll-driven windows bypass renderBody; restore grid roles, logical
16646
18092
  // row indices, and roving focus after every inserted range.
16647
- afterRender: () => this.keyboard.refresh(),
18093
+ afterRender: () => {
18094
+ this.keyboard.refresh();
18095
+ // Every window rebuilds the tbody from scratch, taking any filler with
18096
+ // it. Virtualization is not exempt from the fill-row contract, and
18097
+ // renderBody()'s own tail call never runs on the virtual path, so the
18098
+ // filler is re-evaluated here instead.
18099
+ this.updateFillRow();
18100
+ },
16648
18101
  });
16649
18102
  }
16650
18103
  getVirtualRowHeight(displayIndex) {
@@ -16663,6 +18116,30 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
16663
18116
  * virtualized); otherwise it is the filtered data. Each descriptor carries
16664
18117
  * the logical index used for `data-index`, selection, and detail lookups.
16665
18118
  */
18119
+ /**
18120
+ * Re-derive the windowed row model and hand it to the virtualizer.
18121
+ *
18122
+ * `virtualRowsSnapshot` is the ONLY state the paint pipeline caches, and
18123
+ * `renderRowRange` paints from it. Any caller that resolves a display index
18124
+ * against a FRESH `getVirtualRows()` and then asks the virtualizer to paint
18125
+ * (scrollToRow, the keyboard ensure-visible hook) must republish that model
18126
+ * first, or it scrolls to an offset computed in one list and paints rows from
18127
+ * another — the requested row never appears. Returns the fresh model so the
18128
+ * caller indexes exactly what will be painted.
18129
+ *
18130
+ * `paint` false leaves the repaint to the caller (scrollToIndex refreshes the
18131
+ * window itself). Those callers must not paint from here: the virtualizer's
18132
+ * afterRender hook re-runs keyboard.refresh(), whose ensure-visible callback
18133
+ * lands back in ensureKeyboardRowRendered — an unbounded recursion.
18134
+ */
18135
+ syncVirtualRows(paint = true) {
18136
+ this.virtualRowsSnapshot = this.getVirtualRows();
18137
+ // Not yet attached during setupVirtualization; attach() takes the count.
18138
+ if (this.virtualizer.isEnabled()) {
18139
+ this.virtualizer.setTotalRows(this.virtualRowsSnapshot.length, paint);
18140
+ }
18141
+ return this.virtualRowsSnapshot;
18142
+ }
16666
18143
  getVirtualRows() {
16667
18144
  const filtered = this.getFilteredData();
16668
18145
  // F: the flattened group+row+aggregate list is the virtual model when
@@ -16685,14 +18162,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
16685
18162
  const rows = this.virtualRowsSnapshot.length > 0
16686
18163
  ? this.virtualRowsSnapshot
16687
18164
  : this.getVirtualRows();
16688
- const extraCols = (this.hasSelectionColumn() ? 1 : 0) +
16689
- (this.masterDetail.isEnabled() ? 1 : 0) +
16690
- (this.rowReorder && this.rowDnD.isEnabled() ? 1 : 0);
16691
- const managedColumns = this.columnManager.getAllStates();
16692
- const visibleColumnCount = managedColumns.length > 0
16693
- ? this.columnManager.getVisibleColumns().length
16694
- : this.columns.length;
16695
- const totalColSpan = visibleColumnCount + extraCols;
18165
+ const totalColSpan = this.getTotalColumnSpan();
16696
18166
  const structuralSig = this.computeStructuralSig();
16697
18167
  // Task B: recycle rows that stay in the window across a scroll shift. A row
16698
18168
  // still visible after scrolling keeps its data-object key and signature, so
@@ -16738,9 +18208,10 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
16738
18208
  // Master-detail: render the expanded detail row in-window. The detail
16739
18209
  // A fixed detailHeight participates in virtual spacer math through
16740
18210
  // getVirtualRowHeight. Auto-height content is still rendered in-window
16741
- // and contributes through normal table layout. Tree rows never carry
16742
- // detail panels, so skip them.
16743
- if (!treeRow && this.masterDetail.isEnabled() && this.masterDetail.isExpanded(dataIndex)) {
18211
+ // and contributes through normal table layout. Tree rows compose with
18212
+ // master-detail like any other row; only generated gap nodes (index -1)
18213
+ // have no data row to describe.
18214
+ if (dataIndex >= 0 && this.masterDetail.isEnabled() && this.masterDetail.isExpanded(dataIndex)) {
16744
18215
  const detailRow = this.masterDetail.createDetailRow(data, dataIndex, totalColSpan);
16745
18216
  if (detailRow)
16746
18217
  fragment.appendChild(detailRow);
@@ -16766,7 +18237,8 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
16766
18237
  // data row rather than an unrelated header/subtotal.
16767
18238
  if (this.usesGroupingDisplayModel() || this.treeData.isEnabled()) {
16768
18239
  const row = this.data[index];
16769
- const displayIndex = this.getVirtualRows().findIndex((entry) => entry.data === row);
18240
+ // Resolve against the model the virtualizer is about to paint from.
18241
+ const displayIndex = this.syncVirtualRows(false).findIndex((entry) => entry.data === row);
16770
18242
  if (displayIndex >= 0)
16771
18243
  this.virtualizer.scrollToIndex(displayIndex);
16772
18244
  return;
@@ -16775,6 +18247,14 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
16775
18247
  }
16776
18248
  /** Rows currently reachable by grid keyboard navigation, in display order. */
16777
18249
  getKeyboardColumnCount() {
18250
+ // List mode paints tool cells plus ONE full-width cell per row, and its
18251
+ // header no longer mirrors the body — count the body shape directly.
18252
+ if (this.usesListRowRenderer()) {
18253
+ return 1
18254
+ + (this.rowReorder && this.rowDnD.isEnabled() ? 1 : 0)
18255
+ + (this.masterDetail.isEnabled() ? 1 : 0)
18256
+ + (this.hasSelectionColumn() ? 1 : 0);
18257
+ }
16778
18258
  const rendered = this.shadowRoot?.querySelectorAll('thead tr.column-header-row > th').length ?? 0;
16779
18259
  if (rendered > 0)
16780
18260
  return rendered;
@@ -16830,7 +18310,9 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
16830
18310
  const row = this.getKeyboardRows()[rowIndex];
16831
18311
  if (row === undefined)
16832
18312
  return;
16833
- const displayIndex = this.getVirtualRows().findIndex((entry) => entry.data === row);
18313
+ // The window is painted from `virtualRowsSnapshot`; republish it so the
18314
+ // offset we scroll to and the rows that land there come from one model.
18315
+ const displayIndex = this.syncVirtualRows(false).findIndex((entry) => entry.data === row);
16834
18316
  if (displayIndex >= 0)
16835
18317
  this.virtualizer.scrollToIndex(displayIndex);
16836
18318
  }
@@ -16958,8 +18440,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
16958
18440
  return items;
16959
18441
  const totalPages = Math.max(1, Math.ceil(items.length / this.pageSize));
16960
18442
  const page = Math.max(1, Math.min(this.currentPage, totalPages));
16961
- if (page !== this.currentPage)
16962
- this.currentPage = page;
18443
+ this.clampCurrentPage(page);
16963
18444
  const start = (page - 1) * this.pageSize;
16964
18445
  return items.slice(start, start + this.pageSize);
16965
18446
  }
@@ -16979,8 +18460,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
16979
18460
  return rows;
16980
18461
  const totalPages = Math.max(1, Math.ceil(rows.length / this.pageSize));
16981
18462
  const page = Math.max(1, Math.min(this.currentPage, totalPages));
16982
- if (page !== this.currentPage)
16983
- this.currentPage = page;
18463
+ this.clampCurrentPage(page);
16984
18464
  const start = (page - 1) * this.pageSize;
16985
18465
  return rows.slice(start, start + this.pageSize);
16986
18466
  }
@@ -17042,16 +18522,21 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
17042
18522
  this.renderBody();
17043
18523
  this.dispatchColumnPinChange(key, false);
17044
18524
  }
18525
+ // A measured width is the column's width, not the header's: the body has to
18526
+ // be repainted with it too, or the th and its tds carry different numbers
18527
+ // and the next body-only re-render paints the stale one.
17045
18528
  autoSizeColumn(key) {
17046
18529
  if (this.tbody) {
17047
18530
  this.columnManager.autoSizeColumn(key, this.tbody);
17048
18531
  this.renderHeader();
18532
+ this.renderBody();
17049
18533
  }
17050
18534
  }
17051
18535
  autoSizeAllColumns() {
17052
18536
  if (this.tbody) {
17053
18537
  this.columnManager.autoSizeAll(this.tbody);
17054
18538
  this.renderHeader();
18539
+ this.renderBody();
17055
18540
  }
17056
18541
  }
17057
18542
  moveColumn(key, toIndex) {
@@ -17311,8 +18796,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
17311
18796
  this.masterDetail.onHeightChange = () => {
17312
18797
  if (!this.virtualize || !this.virtualizer.isEnabled())
17313
18798
  return;
17314
- this.virtualRowsSnapshot = this.getVirtualRows();
17315
- this.virtualizer.setTotalRows(this.virtualRowsSnapshot.length);
18799
+ this.syncVirtualRows();
17316
18800
  };
17317
18801
  this.masterDetail.prepare(this.data);
17318
18802
  this.renderHeader();
@@ -17539,6 +19023,9 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
17539
19023
  // The row recycler cannot infer that a callback identity changed from
17540
19024
  // row data alone. Force fresh rows so list markup replaces table cells.
17541
19025
  this.renderedRows = new Map();
19026
+ // Installing a renderer removes the column geometry the header describes;
19027
+ // clearing it back to null restores columnar rows AND their header.
19028
+ this.renderHeader();
17542
19029
  this.renderBody();
17543
19030
  }
17544
19031
  setListViewRenderer(fn) {
@@ -17557,7 +19044,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
17557
19044
  tr.style.height = `${this.rowHeight}px`;
17558
19045
  }
17559
19046
  const isSelected = this.selectedRows.includes(index);
17560
- tr.setAttribute('data-selected', String(isSelected));
19047
+ this.stampRowSelection(tr, isSelected);
17561
19048
  // Row DnD handle
17562
19049
  if (this.rowReorder && this.rowDnD.isEnabled()) {
17563
19050
  tr.appendChild(this.rowDnD.createDragHandle());
@@ -17628,7 +19115,18 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
17628
19115
  const toggle = this.treeData.createToggle(treeRow);
17629
19116
  td.appendChild(toggle);
17630
19117
  const textSpan = document.createElement('span');
17631
- textSpan.textContent = value == null ? '' : String(value);
19118
+ textSpan.className = 'tree-label';
19119
+ // The group cell keeps its plain label next to the indent/chevron, but
19120
+ // it is still a cell: renderCell owns it outright, and otherwise the
19121
+ // documented display pipeline (formatter, then valueFormatter) decides
19122
+ // the text. Only a column with neither shows the raw working value.
19123
+ if (typeof column.renderCell === 'function') {
19124
+ textSpan.appendChild(this.createCellElement(column, value, rowData));
19125
+ }
19126
+ else {
19127
+ textSpan.textContent = this.cellDisplayText(column, value, rowData)
19128
+ ?? (value == null ? '' : String(value));
19129
+ }
17632
19130
  // Clicking the text label also toggles expand/collapse
17633
19131
  if (treeRow.hasChildren) {
17634
19132
  textSpan.style.cursor = 'pointer';
@@ -17647,21 +19145,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
17647
19145
  const state = this.columnManager.getState(column.key);
17648
19146
  if (state) {
17649
19147
  td.style.width = `${state.width}px`;
17650
- // Pinned column sticky positioning
17651
- if (state.pinned === 'left') {
17652
- const offsets = this.columnManager.getPinnedLeftOffsets();
17653
- td.classList.add('pinned-cell');
17654
- td.style.position = 'sticky';
17655
- td.style.left = `${offsets.get(column.key) ?? 0}px`;
17656
- td.style.zIndex = '1';
17657
- }
17658
- else if (state.pinned === 'right') {
17659
- const offsets = this.columnManager.getPinnedRightOffsets();
17660
- td.classList.add('pinned-cell');
17661
- td.style.position = 'sticky';
17662
- td.style.right = `${offsets.get(column.key) ?? 0}px`;
17663
- td.style.zIndex = '1';
17664
- }
19148
+ this.applyPinnedCell(td, column.key, state, '1');
17665
19149
  }
17666
19150
  tr.appendChild(td);
17667
19151
  });
@@ -17825,20 +19309,7 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
17825
19309
  const state = this.columnManager.getState(column.key);
17826
19310
  if (state) {
17827
19311
  td.style.width = `${state.width}px`;
17828
- if (state.pinned === 'left') {
17829
- const offsets = this.columnManager.getPinnedLeftOffsets();
17830
- td.classList.add('pinned-cell');
17831
- td.style.position = 'sticky';
17832
- td.style.left = `${offsets.get(column.key) ?? 0}px`;
17833
- td.style.zIndex = '1';
17834
- }
17835
- else if (state.pinned === 'right') {
17836
- const offsets = this.columnManager.getPinnedRightOffsets();
17837
- td.classList.add('pinned-cell');
17838
- td.style.position = 'sticky';
17839
- td.style.right = `${offsets.get(column.key) ?? 0}px`;
17840
- td.style.zIndex = '1';
17841
- }
19312
+ this.applyPinnedCell(td, column.key, state, '1');
17842
19313
  }
17843
19314
  tr.appendChild(td);
17844
19315
  });
@@ -18019,6 +19490,26 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
18019
19490
  // C1: reactive, no attribute reflection. A post-mount assignment (or the
18020
19491
  // setData/setColumns aliases) re-renders through the microtask-coalesced
18021
19492
  // queue — see handleColumnsAssignment / handleDataAssignment.
19493
+ //
19494
+ // `hasChanged: () => true` — ASSIGNMENT IS THE SIGNAL, NOT IDENTITY.
19495
+ // The core dirty-check (notEqual in packages/core/src/utils.ts, applied by the
19496
+ // @property setter in element.ts) is identity-based and stays that way for
19497
+ // every other property: that is the right default for reactivity in general.
19498
+ // But `data` and `columns` are bulk payloads that callers routinely mutate in
19499
+ // place and then publish by re-assigning the SAME array reference
19500
+ // (`rows.push(row); table.data = rows`). The documented contract is that
19501
+ // assigning `data`/`columns` rerenders, so these two properties opt OUT of the
19502
+ // identity gate locally rather than weakening it globally.
19503
+ //
19504
+ // Tradeoff — double-render cost: a genuinely redundant assignment
19505
+ // (`table.data = table.data` with nothing changed) now costs a render that the
19506
+ // identity gate used to swallow, and a caller who assigns both `columns` and
19507
+ // `data` pays two schedule calls instead of one. It is bounded: both watchers
19508
+ // go through scheduleRender(), which coalesces a burst within a tick into a
19509
+ // single microtask flush, so the worst case is one extra body paint per tick,
19510
+ // never per assignment. Deep-equality diffing was rejected as the alternative
19511
+ // — it is O(rows x columns) on every assignment, i.e. a cost paid by the
19512
+ // common (genuinely-changed) path to save the rare redundant one.
18022
19513
  this.columns = (__runInitializers(this, _searchDebounce_extraInitializers), __runInitializers(this, _columns_initializers, []));
18023
19514
  this.data = (__runInitializers(this, _columns_extraInitializers), __runInitializers(this, _data_initializers, []));
18024
19515
  /**
@@ -18039,7 +19530,8 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
18039
19530
  this.selector = __runInitializers(this, _selector_initializers, '');
18040
19531
  this.selectorOptions = (__runInitializers(this, _selector_extraInitializers), __runInitializers(this, _selectorOptions_initializers, []));
18041
19532
  this.loading = (__runInitializers(this, _selectorOptions_extraInitializers), __runInitializers(this, _loading_initializers, false));
18042
- // Set when a remote-mode load fails; cleared on the next successful load.
19533
+ // Set when a remote-mode load fails; cleared by clearLoadError() as soon as
19534
+ // ANY fresh dataset arrives (see there).
18043
19535
  // Not a @property — renderBody() reads it directly, no attribute needed.
18044
19536
  this.loadError = (__runInitializers(this, _loading_extraInitializers), null);
18045
19537
  this.virtualize = __runInitializers(this, _virtualize_initializers, false);
@@ -18049,7 +19541,16 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
18049
19541
  this.editable = (__runInitializers(this, _columnResize_extraInitializers), __runInitializers(this, _editable_initializers, false));
18050
19542
  this.editMode = (__runInitializers(this, _editable_extraInitializers), __runInitializers(this, _editMode_initializers, 'cell'));
18051
19543
  this.density = (__runInitializers(this, _editMode_extraInitializers), __runInitializers(this, _density_initializers, 'standard'));
18052
- this.headerFilters = (__runInitializers(this, _density_extraInitializers), __runInitializers(this, _headerFilters_initializers, false));
19544
+ /**
19545
+ * How the columns relate to the frame's width.
19546
+ *
19547
+ * `scroll` keeps every column at or above its `minWidth` and lets the frame
19548
+ * scroll horizontally when they no longer fit. `squish` makes the frame the
19549
+ * hard constraint instead: minimums relax, content ellipsises, and the table
19550
+ * never scrolls sideways. See the `ColumnFit` docs in snice-table.types.ts.
19551
+ */
19552
+ this.columnFit = (__runInitializers(this, _density_extraInitializers), __runInitializers(this, _columnFit_initializers, 'scroll'));
19553
+ this.headerFilters = (__runInitializers(this, _columnFit_extraInitializers), __runInitializers(this, _headerFilters_initializers, false));
18053
19554
  this.quickFilter = (__runInitializers(this, _headerFilters_extraInitializers), __runInitializers(this, _quickFilter_initializers, false));
18054
19555
  this.rowReorder = (__runInitializers(this, _quickFilter_extraInitializers), __runInitializers(this, _rowReorder_initializers, false));
18055
19556
  this.columnReorder = (__runInitializers(this, _rowReorder_extraInitializers), __runInitializers(this, _columnReorder_initializers, false));
@@ -18101,7 +19602,9 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
18101
19602
  this.table = __runInitializers(this, _table_initializers, void 0);
18102
19603
  this.thead = (__runInitializers(this, _table_extraInitializers), __runInitializers(this, _thead_initializers, void 0));
18103
19604
  this.tbody = (__runInitializers(this, _thead_extraInitializers), __runInitializers(this, _tbody_initializers, void 0));
18104
- this.unsortedData = (__runInitializers(this, _tbody_extraInitializers), []);
19605
+ /** Frame width the columns were last fitted to — see handleFrameResize. */
19606
+ this.lastFittedFrameWidth = (__runInitializers(this, _tbody_extraInitializers), 0);
19607
+ this.unsortedData = [];
18105
19608
  this.dataRequestTimeout = null;
18106
19609
  // A1: row-object → its index in `this.data`. Replaces the O(n) `data.indexOf`
18107
19610
  // that ran per row on every selection toggle (was O(n²) for select-all).
@@ -18133,6 +19636,10 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
18133
19636
  // C1: set while the imperative setData() runs so the data @watch resyncs the
18134
19637
  // model but skips the auto-render (the caller drives the body fill).
18135
19638
  this.settingDataImperative = false;
19639
+ // Set while the row-reorder handler re-publishes `this.data` as a new array
19640
+ // of the SAME rows. Tells the data @watch that no fresh dataset arrived, so a
19641
+ // pending load error must survive the drag.
19642
+ this.reorderingRows = false;
18136
19643
  // C1/C2: microtask-coalesced render queue. Reactive assignments schedule a
18137
19644
  // render here instead of calling renderHeader/renderBody synchronously —
18138
19645
  // MANDATORY, because happy-dom crashes constructing cell elements inside a
@@ -18140,6 +19647,32 @@ if(typeof globalThis.Snice==="undefined"){console.warn("[snice] snice-runtime.mi
18140
19647
  this.renderQueued = false;
18141
19648
  this.pendingHeaderRender = false;
18142
19649
  this.pendingBodyRender = false;
19650
+ // Re-arm bookkeeping for a body render that found no <tbody> to paint into.
19651
+ // Bounded: the slotted-rows template legitimately has no <tbody>, and an
19652
+ // unbounded retry there would spin every frame.
19653
+ this.bodyRenderRetries = 0;
19654
+ this.bodyRenderRetryQueued = false;
19655
+ /**
19656
+ * Write a page number the display model just derived (a re-clamp after the
19657
+ * row set shortened), WITHOUT re-entering the render scheduler.
19658
+ *
19659
+ * The three clamp sites all sit inside a display-model computation whose
19660
+ * caller paints immediately afterwards with the clamped page — the ordinary
19661
+ * body path slices with it and then renders the pager, the grouping and tree
19662
+ * models return the clamped slice. Letting the @watch fire here queued a
19663
+ * second, byte-identical body pass per delivery — the measured cost, and what
19664
+ * the regression test pins. Suppressing the write's watcher makes the clamp
19665
+ * cost zero extra passes and makes an oscillating clamp structurally unable
19666
+ * to loop.
19667
+ *
19668
+ * All three sites are reachable only under `paginationMode === 'client'`, so
19669
+ * the watcher's server branch (debouncedDataRequest, whose `loading = true`
19670
+ * re-enters renderBody SYNCHRONOUSLY through the loading watcher — from the
19671
+ * middle of a render) is not reachable from a clamp today. It is the reason
19672
+ * to keep this guard rather than to drop the watcher suppression if a fourth
19673
+ * clamp site ever lands outside that guard.
19674
+ */
19675
+ this.clampingPage = false;
18143
19676
  /**
18144
19677
  * A structural fingerprint of everything `createRow` reads that is NOT
18145
19678
  * per-row data: visible columns + their widths/pinning/offsets, tool columns,