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