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
@@ -5,18 +5,18 @@ Calendar with date selection and event support.
5
5
  ## Properties
6
6
 
7
7
  ```typescript
8
- value: Date | string = new Date();
8
+ value: Date | string | null = null; // null = nothing selected (initial state); today is marked by highlightToday, not by a selection
9
9
  view: 'month'|'week'|'day' = 'month';
10
10
  events: CalendarEvent[] = [];
11
11
  minDate: Date | string = ''; // attribute: min-date
12
12
  maxDate: Date | string = ''; // attribute: max-date
13
13
  disabledDates: (Date | string)[] = []; // JS-only; no attribute
14
14
  highlightToday: boolean = true; // attribute: highlight-today
15
- showWeekNumbers: boolean = false; // attribute: show-week-numbers
15
+ showWeekNumbers: boolean = false; // attribute: show-week-numbers — adds a leading week-number column (ISO-8601 when firstDayOfWeek=1, otherwise the week containing Jan 1 is week 1)
16
16
  firstDayOfWeek: number = 0; // attribute: first-day-of-week (0=Sun, 1=Mon)
17
17
  locale: string = 'en-US';
18
18
  noDaySelect: boolean = false; // attribute: no-day-select — display-only: day clicks don't select/highlight or fire calendar-change; event bars stay interactive
19
- cellSizing: 'square'|'stretch' = 'square'; // attribute: cell-sizing — square: cells as tall as the column is wide; stretch: rows collapse to content + event-lane reservation
19
+ cellSizing: 'square'|'stretch' = 'square'; // attribute: cell-sizing — square: cells as tall as the column is wide; stretch: rows collapse to content (every event lane shown). Sizes the cell, and the cell's height sets the visible event-lane count
20
20
  ```
21
21
 
22
22
  ## Methods
@@ -33,7 +33,11 @@ cellSizing: 'square'|'stretch' = 'square'; // attribute: cell-sizing — square:
33
33
 
34
34
  - `calendar-change` -> `{ value: Date, calendar: SniceCalendarElement }`
35
35
  - `calendar-event-click` -> `{ event: CalendarEvent, calendar: SniceCalendarElement }`
36
- - `calendar-more-click` -> `{ date: Date, count: number, calendar: SniceCalendarElement }` — "+N more" chip clicked; does NOT fire `calendar-change`
36
+ - `calendar-more-click` -> `{ date: Date, count: number, calendar: SniceCalendarElement }` — "+N more" chip clicked; does NOT fire `calendar-change`. **Cancelable**: `preventDefault()` suppresses the built-in day panel (see "+N more" default action)
37
+
38
+ ## Methods (overlay)
39
+
40
+ - `closeEventPopover(options?: { returnFocus?: boolean })` - Closes the shared overlay (event popover or "+N more" day panel)
37
41
 
38
42
  ## CSS Parts
39
43
 
@@ -41,6 +45,8 @@ cellSizing: 'square'|'stretch' = 'square'; // attribute: cell-sizing — square:
41
45
  - `header` - Header with title and navigation buttons
42
46
  - `grid` - Day cells grid
43
47
  - `more-chip` - Per-day "+N more" overflow chip
48
+ - `more-list` / `more-item` / `more-dot` / `more-panel-date` - Built-in "+N more" day panel internals
49
+ - `week-number` / `week-number-header` - Week-number column cells (`show-week-numbers`)
44
50
  - `event-bar` / `event-avatar` / `event-tooltip` / `event-popover` - Event bar internals
45
51
 
46
52
  ## Basic Usage
@@ -58,21 +64,31 @@ calendar.events = [
58
64
  ];
59
65
  // Ranged events render as continuous stripes: one bar per week row, chopped
60
66
  // at week boundaries (squared corners on the continuing side, title repeats).
61
- // Concurrent events stack into lanes (start asc, longer first on ties);
62
- // max 3 lanes, deeper stacks collapse to a per-day "+N more" chip.
67
+ // Concurrent events stack into lanes (start asc, longer first on ties).
68
+ // Visible lanes are DERIVED PER WEEK from the day cell's height (see
69
+ // "Event-lane budget"); deeper stacks collapse to a per-day "+N more" chip.
63
70
  // Chip: part="more-chip", role="button" tabindex=0 (Enter/Space), click ->
64
71
  // calendar-more-click { date, count }; the click never falls through to day
65
- // selection. Use it to open your own day/agenda view:
66
- // calendar.addEventListener('calendar-more-click',
67
- // (e) => showDay(e.detail.date, e.detail.count));
72
+ // selection. Built-in default action, no wiring required see
73
+ // '"+N more" default action' below. Replace it with your own day/agenda view:
74
+ // calendar.addEventListener('calendar-more-click', (e) => {
75
+ // e.preventDefault(); // cancels the built-in panel
76
+ // showDay(e.detail.date, e.detail.count);
77
+ // });
68
78
  // Bars: part="event-bar", click -> calendar-event-click.
69
79
  // Styling: color -> background; avatar?: string | {src?,name?,alt?} ->
70
80
  // <snice-avatar> on each bar (part="event-avatar"; name -> initials fallback);
81
+ // Adornment, not payload: a bar segment narrower than 4.5rem renders the
82
+ // title alone (class calendar__event-bar--compact, tighter padding) — the
83
+ // avatar would leave ~3 characters in a default-width month cell. Measured
84
+ // per segment, so a multi-day span keeps it; re-decided on resize.
71
85
  // className?: string -> added to bar classes AND part list, so
72
86
  // ::part(<className>) themes specific events from outside.
73
87
  // Tooltips: event.tooltip?: string (static), or on the element:
74
88
  // eventTooltip: (event) => string | Node | Promise<string | Node> // lazy/rich; wins over event.tooltip
75
- // Stale async results discarded on pointer-leave. Overlay part="event-tooltip".
89
+ // Stale async results discarded on pointer-leave. Overlay part="event-tooltip";
90
+ // surface = --snice-tooltip-bg / --snice-tooltip-color (same pair as
91
+ // <snice-tooltip>), so it stays legible in both themes.
76
92
  // Popovers (click-to-open, interactive; STRICT per-event opt-in):
77
93
  // event.popover?: boolean | string | Node | (() => Node)
78
94
  // Resolution: inline content -> element eventPopover(event) => string|Node|Promise
@@ -81,12 +97,99 @@ calendar.events = [
81
97
  // to bar; loading state while pending; stale results discarded.
82
98
  // Bars with popover: role="button" tabindex=0, Enter/Space opens.
83
99
  // calendar-event-click still fires. closeEventPopover() closes programmatically.
100
+ // Overlay placement (both tooltip and popover): position: absolute inside
101
+ // ::part(base), NOT viewport-fixed — :host has `contain: layout style`, which
102
+ // makes the host the containing block for fixed descendants. Anchored to the
103
+ // bar (popover below / tooltip above), flipped at the viewport edge, clamped
104
+ // horizontally. Containment also makes the host a stacking context, so while
105
+ // an overlay shows the host carries a read-only `overlay-open` attribute and
106
+ // z-index: var(--snice-z-floating, 1000); restyle via
107
+ // snice-calendar[overlay-open] { z-index: ... }.
84
108
 
85
109
  calendar.addEventListener('calendar-change', (e) => {
86
110
  console.log('Selected:', e.detail.value);
87
111
  });
88
112
  ```
89
113
 
114
+ ## "+N more" Default Action
115
+
116
+ Clicking (or Enter/Space on) the chip opens the shared overlay listing that
117
+ day's hidden events — no listener required.
118
+
119
+ - Panel = the same `.calendar__popover` element the event popover uses:
120
+ `role="dialog"`, `part="event-popover"`, Escape / outside-press dismiss,
121
+ focus moves into the panel and returns to the chip on close.
122
+ - `aria-label` names the day; `part="more-panel-date"` heading + `part="more-list"`
123
+ (`role="list"`) of `<button part="more-item">` entries, each a
124
+ `part="more-dot"` colour dot (`event.color`) plus the title.
125
+ - Entry click -> `calendar-event-click`; if the event has `popover` content the
126
+ same overlay drills into it, otherwise the panel closes.
127
+ - Suppress with `preventDefault()` on `calendar-more-click`:
128
+
129
+ ```typescript
130
+ calendar.addEventListener('calendar-more-click', (e) => {
131
+ e.preventDefault(); // built-in panel does not open
132
+ openMyDayView(e.detail.date);
133
+ });
134
+ ```
135
+
136
+ ## Event-Lane Budget
137
+
138
+ Visible lanes per week = how many fit the day cell's height. No flat cap.
139
+
140
+ ```
141
+ lanes = max(1, floor((cellHeight - 2.125rem - chip) / 1.375rem))
142
+ chip = 1rem when the stack overflows (the "+N more" strip), else 0
143
+ ```
144
+
145
+ Cell height comes from whatever sizes the cell:
146
+ - `cell-sizing="square"` (default): `100cqw / 7` — the column width; wider calendar -> taller cells -> more lanes. Capped by the built-in `max-width: 37.5rem` (~5.4rem cell -> 2 lanes, 1 with the chip); lift with `::part(base) { max-width: none }`. Narrow calendars show fewer lanes than the flat 3-lane cap this replaced.
147
+ - Definite host height: `snice-calendar { height: 40rem }` (or a sized flex/grid parent) — the six week rows share the room under the header, so the budget grows with it.
148
+ - `cell-sizing="stretch"` with no imposed height: no budget — rows collapse to content and every lane is shown, no chip.
149
+
150
+ Notes:
151
+ - Re-derived on resize (`@observe('resize')`) — no need to re-assign `events`.
152
+ - Row reservation is a floor, applied only when the cell is shorter than the stack it shows: 3.5rem for one lane, 4.5rem for one lane + chip. Above that all rows stay equal; below it (square cells under ~4.5rem, i.e. calendars under ~500px wide) the busy row still grows.
153
+ - No layout to measure (headless DOM, `display: none`, detached) -> falls back to 3 lanes.
154
+
155
+ ```typescript
156
+ // Same events, more visible lanes:
157
+ calendar.style.height = '40rem';
158
+ ```
159
+
160
+ ## Sizing In A Constrained Host
161
+
162
+ The month fits its own host box — the bottom weeks are not clipped.
163
+
164
+ - Host height is **auto** (default): week rows take the square baseline
165
+ (`100cqw / 7`), and a busy row grows by its lane reservation. The host's
166
+ height is whatever that adds up to.
167
+ - Host height is **larger** than that: the six rows share the surplus and the
168
+ lane budget grows with them.
169
+ - Host height is **smaller** (`height`, `max-height`, `aspect-ratio`, a sized
170
+ flex/grid parent): the rows are capped at their equal share of the room under
171
+ the header, so all six weeks stay inside the box. The lane budget follows the
172
+ compressed row — events that no longer fit collapse into the "+N more" chip
173
+ instead of being drawn outside their cell, and a row too short for even one
174
+ lane shows the chip alone.
175
+
176
+ Limits:
177
+ - Measures its OWN box only. An ancestor that clips/scrolls while the calendar's
178
+ height stays auto (fixed wrapper + `overflow:hidden`, flex item without
179
+ `min-height:0`) is undetectable — constrain the calendar, not the wrapper.
180
+ - Row floor = the cell's own padding + rules (~17px default). Under a ~200px
181
+ host the grid overflows again rather than stacking week over week.
182
+ - `view="week"` / `view="day"` keep their own taller row minimums; not capped.
183
+
184
+ Authoring note: a month grid is a header, a weekday strip and six square-ish
185
+ week rows, so its natural box is TALLER than it is wide. `aspect-ratio: 1` on
186
+ the host is not a neutral size hint — it asks the grid to lose ~1.5 week rows.
187
+ Constrain the width and let the height follow:
188
+
189
+ ```html
190
+ <snice-calendar style="min-width: 360px; max-width: 26rem; width: 100%"></snice-calendar>
191
+ ```
192
+
90
193
  ## Keyboard Navigation
91
194
 
92
195
  - Arrow keys: navigate dates
@@ -71,6 +71,10 @@ cb.code = 'const x = 1;';
71
71
 
72
72
  Grammar-based: `format="pretty"` (grammars: json, typescript, css, snice)
73
73
 
74
+ - Preserve author line breaks and blank lines; renormalize leading indentation; space rules apply within a line
75
+ - Rule-driven breaks (after `;`, before `}`) fire only where no author break exists; suppressed inside `()`/`[]`; closers break only when their group is multi-line
76
+ - One-liners (e.g. minified JSON) still pretty-print
77
+
74
78
  Imperative: `setFormatter(fn)` overrides grammar formatters
75
79
 
76
80
  ```typescript
@@ -63,7 +63,7 @@ doc.downloadAs('markdown', 'my-doc.md');
63
63
 
64
64
  ## Icon Sets
65
65
 
66
- - `default` - Text labels and emoji
66
+ - `default` - Text labels and embedded SVG icons
67
67
  - `material` - Material Symbols Outlined (load font in document)
68
68
  - `fontawesome` - Font Awesome 6 solid (load font in document)
69
69
  - Fonts cascade from light DOM into shadow DOM
@@ -39,6 +39,7 @@ Circular: `base`, `circle`, `circle-bg`, `circle-bar`, `label`
39
39
  --progress-radius: 4px;
40
40
  --progress-bg: var(--snice-color-border, rgb(226 226 226));
41
41
  --progress-color: var(--snice-color-primary, rgb(37 99 235));
42
+ --progress-track-opacity: 0.2; /* circular track ring; raise to 1 on dark surfaces — 0.2 of any color vanishes on near-black */
42
43
  --progress-animation-duration: 1.5s;
43
44
  ```
44
45
 
@@ -139,5 +139,5 @@ receipt.taxes = [
139
139
  - Subtotal auto-computed from items if not set
140
140
  - Total auto-computed as subtotal + tax - discount + tip if not set
141
141
  - `taxes` array overrides single `tax` property
142
- - SKU, notes, per-item discounts only in `detailed` variant
142
+ - SKU, notes, per-item discount lines only in `detailed` variant; an item `discount` still reduces the subtotal in every variant
143
143
  - Print styles included for all variants
@@ -14,13 +14,14 @@ striped:boolean=false; searchable:boolean=false; filterable:boolean=false; sorta
14
14
  hoverable:boolean=true; clickable:boolean=false; list:boolean=false; loading:boolean=false; pagination:boolean=false; virtualize:boolean=false; editable:boolean=false; columnResize:boolean=false; headerFilters:boolean=false; // attrs column-resize/header-filters
15
15
  quickFilter:boolean=false; rowReorder:boolean=false; columnReorder:boolean=false; columnMenu:boolean=false; lazyLoad:boolean=false; // attrs quick-filter/row-reorder/column-reorder/column-menu/lazy-load
16
16
  mode: 'local'|'remote' = 'local';
17
- columns:ColumnDefinition[]=[]; data:any[]=[]; currentSort:{column:string;direction:'asc'|'desc'}[]=[]; selectedRows:number[]=[]; // JS-only; selectedRows are raw-data indices
17
+ columns:ColumnDefinition[]=[]; data:any[]=[]; currentSort:{column:string;direction:'asc'|'desc'}[]=[]; selectedRows:number[]=[]; // JS-only; selectedRows are raw-data indices, re-resolved to the same row objects when `data` is reordered (local sort, reordered re-delivery)
18
18
  listRenderer:((row,index)=>string|HTMLElement)|null=null; // JS-only; full-width custom rows in list mode
19
19
  searchText:string=''; searchDebounce:number=500; // searchText JS-only; attr search-debounce
20
20
  selectionMode:'none'|'single'|'multiple'='multiple'; selector:string=''; selectorOptions:{value:string;label:string}[]=[]; // attr selection-mode; options JS-only
21
21
  groupBy:string|string[]=''; groupDefaults:{expanded?:boolean}={}; // JS-only
22
22
  paginationMode:'client'|'server'='client'; pageSize:number=10; currentPage:number=1; totalItems:number=0; pageSizes:number[]=[10,25,50,100]; // attrs pagination-mode/page-size/current-page/total-items; pageSizes JS-only
23
23
  rowHeight:number=48; virtualBuffer:number=200; editMode:'cell'|'row'='cell'; density:'compact'|'standard'|'comfortable'='standard'; // attrs row-height/virtual-buffer/edit-mode
24
+ columnFit:'scroll'|'squish'='scroll'; // attr column-fit
24
25
  lazyLoadThreshold:number=200; // lazy-load-threshold, px
25
26
  // <snice-column>; camelCase properties reflect to matching kebab-case attributes unless marked JS-only
26
27
  key:string=''; label:string=''; type:ColumnType='text'; align:'left'|'center'|'right'='left'; width:string=''; aggregate?:Aggregator; // custom function JS-only
@@ -37,7 +38,7 @@ interface CellProps {value:any;align:'left'|'center'|'right';type:string;column:
37
38
  class TextCell {multiline:boolean=false;maxLines?:number} // max-lines
38
39
  class NumberCell {decimals:number=0;thousandsSeparator:boolean=false;prefix:string='';suffix:string='';negativeStyle:'parentheses'|'red'|'minus'='minus';highlight:boolean=false} // thousands-separator, negative-style
39
40
  class CurrencyCell {decimals:number=2;thousandsSeparator:boolean=true;currency:string='USD';currencyDisplay:'symbol'|'code'|'name'='symbol';locale:string='en-US';negativeStyle:'parentheses'|'red'|'minus'='red';highlight:boolean=false} // thousands-separator, currencydisplay, negative-style
40
- class DateCell {dateFormat:'short'|'medium'|'long'|'full'|'custom'='short';customFormat?:string;locale:string='en-US';relativeTime:boolean=false;showTime:boolean=false} // date-format, custom-format, relative-time, show-time
41
+ class DateCell {dateFormat:'short'|'medium'|'long'|'full'|'custom'='short';customFormat?:string;locale:string='en-US';relativeTime:boolean=false;showTime:boolean=false} // date-format, custom-format, relative-time, show-time; date-only values ('2026-03-15','2026-03','2026') are calendar days read at LOCAL midnight — same day in every timezone, in display/export/filters; values with a time (…T10:00, …Z, epoch, Date) stay instants; a date-only filter bound spans the whole day, so before/onOrBefore/after/onOrAfter agree with `is` (onOrBefore '2026-03-15' keeps '2026-03-15T09:00'); impossible days ('2026-02-30','2026-13-01') are rejected, not rolled over, and render as the raw value
41
42
  class BooleanCell {trueValue:string='true';falseValue:string='false';useSymbols:boolean=true;trueSymbol:string='svg';falseSymbol:string='svg'} // true-value, false-value, use-symbols, true-symbol, false-symbol
42
43
  class PercentageCell {decimals:number=2;showTrend:boolean=false;trendValue:number|null=null;colorize:boolean=true} // attrs showtrend/trendvalue (no hyphens)
43
44
  class SparklineCell {chartType:'line'|'bar'|'area'='line';color:string='var(--snice-color-primary)';width:number=80;height:number=24;showDots:boolean=false;showBaseline:boolean=false;strokeWidth:number=1.5;minValue?:number;maxValue?:number;data:number[]=[]} // assign data in JS; color/width/height attrs unchanged; other camel names use explicit kebab attrs
@@ -63,46 +64,45 @@ interface ColumnDefinition {
63
64
  }
64
65
  type Aggregator='sum'|'avg'|'min'|'max'|'count'|((values:any[],rows:any[])=>any);
65
66
  type NumberFormat={decimals?:number;thousandsSeparator?:boolean;prefix?:string;suffix?:string;negativeStyle?:'parentheses'|'red'|'minus'}; type DateFormat={format?:'short'|'medium'|'long'|'full'|'custom';customFormat?:string;locale?:string};
66
- type BooleanFormat={trueValue?:string;falseValue?:string;useSymbols?:boolean;trueSymbol?:string;falseSymbol?:string}; type RatingFormat={max?:number;symbol?:string;emptySymbol?:string;color?:string};
67
- type ProgressFormat={max?:number;showPercentage?:boolean;color?:string;colorize?:boolean;backgroundColor?:string;height?:string}; type SparklineFormat={type?:'line'|'bar'|'area';color?:string;width?:number;height?:number;showDots?:boolean;showBaseline?:boolean;strokeWidth?:number;minValue?:number;maxValue?:number};
68
- type PercentageFormat={decimals?:number;showTrend?:boolean;trendValue?:number|null;colorize?:boolean}; type PhoneFormat={phone?:string;displayText?:string;showIcon?:boolean;format?:boolean;country?:string};
69
- type StatusFormat={status?:string;label?:string;showDot?:boolean;variant?:'online'|'offline'|'busy'|'away'|'custom'}; type TagFormat={variant?:string};
70
- type ActionsFormat={actions:ActionButton[]}; type ActionButton={action:string;label?:string;icon?:string;variant?:'primary'|'secondary'|'danger'|'success';title?:string;disabled?:boolean};
71
- type LinkFormat={href?:string;target?:string;external?:boolean;icon?:string;text?:string}; type EmailFormat={email?:string;showIcon?:boolean;displayText?:string};
72
- type ColorFormat={color?:string;size?:'small'|'medium'|'large';displayFormat?:'hex'|'rgb'|'hsl'|'name';showSwatch?:boolean;showHex?:boolean;showRgb?:boolean;swatchSize?:'small'|'medium'|'large'};
73
- type CurrencyFormat={currency?:string;locale?:string;display?:'symbol'|'code'|'name';currencyDisplay?:'symbol'|'code'|'name';decimals?:number;thousandsSeparator?:boolean;negativeStyle?:'parentheses'|'red'|'minus'};
67
+ type BooleanFormat={trueValue?:string;falseValue?:string;useSymbols?:boolean;trueSymbol?:string;falseSymbol?:string}; type RatingFormat={max?:number;symbol?:string;emptySymbol?:string;color?:string}; type ProgressFormat={max?:number;showPercentage?:boolean;color?:string;colorize?:boolean;backgroundColor?:string;height?:string}; type SparklineFormat={type?:'line'|'bar'|'area';color?:string;width?:number;height?:number;showDots?:boolean;showBaseline?:boolean;strokeWidth?:number;minValue?:number;maxValue?:number};
68
+ type PercentageFormat={decimals?:number;showTrend?:boolean;trendValue?:number|null;colorize?:boolean}; type PhoneFormat={phone?:string;displayText?:string;showIcon?:boolean;format?:boolean;country?:string}; type StatusFormat={status?:string;label?:string;showDot?:boolean;variant?:'online'|'offline'|'busy'|'away'|'custom'}; type TagFormat={variant?:string};
69
+ type ActionsFormat={actions:ActionButton[]}; type ActionButton={action:string;label?:string;icon?:string;variant?:'primary'|'secondary'|'danger'|'success';title?:string;disabled?:boolean}; type LinkFormat={href?:string;target?:string;external?:boolean;icon?:string;text?:string}; type EmailFormat={email?:string;showIcon?:boolean;displayText?:string};
70
+ type ColorFormat={color?:string;size?:'small'|'medium'|'large';displayFormat?:'hex'|'rgb'|'hsl'|'name';showSwatch?:boolean;showHex?:boolean;showRgb?:boolean;swatchSize?:'small'|'medium'|'large'}; type CurrencyFormat={currency?:string;locale?:string;display?:'symbol'|'code'|'name';currencyDisplay?:'symbol'|'code'|'name';decimals?:number;thousandsSeparator?:boolean;negativeStyle?:'parentheses'|'red'|'minus'};
74
71
  type ImageFormat={src?:string;fallback?:string;shape?:'rounded'|'square'|'circle';variant?:'rounded'|'square'|'circle';size?:'small'|'medium'|'large';alt?:string;lazy?:boolean}; type JsonFormat={maxDepth?:number;expanded?:boolean;collapsed?:boolean;showToggle?:boolean};
75
72
  type LocationFormat={address?:string;latitude?:string|number;longitude?:string|number;showMapLink?:boolean;mapProvider?:'google'|'openstreetmap'|'apple';showIcon?:boolean;lat?:number;lng?:number};
76
73
  type CellStyle={backgroundColor?:string;color?:string;fontWeight?:'normal'|'bold'|'lighter';fontStyle?:'normal'|'italic';fontSize?:string;textDecoration?:'none'|'underline'|'line-through'}; type ConditionalFormat={condition:(value:any,row?:any)=>boolean;style?:CellStyle;className?:string};
77
74
  ```
78
75
 
79
- - `table.columns =`/`table.data =` rerender; `setData()` is non-eager, so call `renderBody()` when unpaired.
76
+ - `table.columns =`/`table.data =` rerender on EVERY assignment — identity is not consulted, so re-assigning the SAME array reference after mutating it in place (`rows.push(r); table.data = rows`) rerenders. Tradeoff: a truly redundant assignment costs a render (coalesced per microtask, so a burst still paints once). `setData()` is non-eager, so call `renderBody()` when unpaired. A `columns` assignment is a NEW CONFIGURATION, not a patch: the assigned array owns painted order, `pinned`, `width` and the column set (undeclared keys are dropped from the model; a re-declared key returns visible), which also discards user column state (drag-resize, manual reorder, `setColumnVisible()` hides) for the columns it re-declares — on EVERY assignment including a same-reference one, so mutating the published definitions is NOT an escape hatch; keep user column state by not re-assigning `columns` and using `setColumnVisible()`/`moveColumn()`/`pinColumn()`/`unpinColumn()` instead. A delivery under a non-empty `currentSort` renders sorted (local; remote keeps the server order). Rows recycle by identity but repaint when their own field values change, so an in-place patch + re-delivery updates that row — shallow compare, so a mutation inside a nested object value needs a new row object.
80
77
  - `searchable` filters locally or requests remotely; `quickFilter` shows a model-backed input; `filterable` is the legacy remote selector.
81
- - Currency uses `snice-cell-currency`. Formatter/valueFormatter, tooltip, base/conditional styles, all declared format aliases, object progress/JSON/sparkline values, sparkline baselines, and already-percent semantics work across Table/declarative/standalone paths.
82
- - `rowReorder`, `columnReorder`, `lazyLoad`, and its threshold are post-mount reactive. List renderers execute in list mode. Density emits `density-change`.
83
- - Limits: pagination shows "Showing 1–25 of 60" or numbered `aria-label="Page N"` (no "Page X of Y"); remote has a 150ms debounce, re-requests only on `currentPage`/`currentSort`/`pageSize`, and keeps rows on a failed reload (⚠️ row + empty-state only with no data); local sort/search are client-side on `data` only.
78
+ - Currency uses `snice-cell-currency`. Formatter/valueFormatter, tooltip, base/conditional styles, all declared format aliases, object progress/JSON/sparkline values, sparkline baselines, and already-percent semantics work across Table/declarative/standalone paths. Display pipeline: `valueGetter` -> `formatter` (wins) -> `valueFormatter` (fallback), on every path — cells, aggregate footers, `<snice-row>`, CSV/clipboard export; a column declaring either formatter renders through `snice-cell-text` (keeping its type's alignment) and the cell's `value` property/attribute IS the display value — except the tree group column, whose resolved label is plain text beside the indent/chevron (no typed cell, so no `value` attribute). An empty row value (null, or a field the row never carried) falls back to the typed cell's own empty value — `false` boolean, `0` rating/progress/duration/filesize, `null` JSON, `''` otherwise — so an absent boolean never reads as `true` and an absent progress never reads as `{}` (table-rendered cells; a declarative `<snice-row>` hands the cell the empty value unchanged).
79
+ - `virtualize` windows the body but keeps every zero-row state: loading spinner, `⚠️` load error, and the `empty-state` slot clone (or the default "No data" placeholder) all render on the virtual path too.
80
+ - Zero-row `loading` renders `<div class="table-loading-state" role="status" aria-live="polite">` = medium `snice-progress` + a `.table-loading-label` reading `Loading…`. With rows present the `loading-overlay` part replaces it and carries `role="status"`/`aria-label="Loading…"`. Both raise `--progress-track-opacity` so the track reads on dark surfaces.
81
+ - `type:'accounting'` = currency notation: symbol + grouping + parenthesised negatives, positives padded with one NBSP for column alignment. Defaults 2 decimals / grouping on / USD / en-US. `numberFormat.decimals|thousandsSeparator` and `currencyFormat.currency|locale|currencyDisplay` override; `numberFormat.prefix|suffix` replaces the symbol. `-12840.5` -> `($12,840.50)`. Generic `<snice-cell>` writes an inline `text-align` only when `align` is declared, so an undeclared cell keeps its TYPE's alignment from snice-cell.css (numeric types right, boolean/rating/sparkline centre).
82
+ - `rowReorder`, `columnReorder`, `lazyLoad`, and its threshold are post-mount reactive. List renderers execute in list mode. An ACTIVE `listRenderer` (needs `list` too) also suppresses the columnar header — column-group row, per-column `<th>`, and header-filter row all drop, since a card row has no column geometry; tool-column headers stay, so `selectable` keeps select-all. Clearing `listRenderer` or `list` restores the header; `list` alone (no renderer) keeps columnar cells AND its header. With no header to click, list-mode sorting runs through the toolbar (`setToolbar({showSort:true})`) or `toggleSort()`/`currentSort`. Density emits `density-change`. `selectable` and `selectionMode` are post-mount reactive too: either one rebuilds header + body, so the checkbox tool column appears/disappears in both.
83
+ - Selection is anchored to the row, not the position: a local sort or a re-delivery of the same row objects in another order re-resolves `selectedRows`, so `getSelectedData()` and the highlight stay on the same rows. A different row set keeps the raw-index semantics.
84
+ - Limits: pagination shows "Showing 1–25 of 60" or numbered `aria-label="Page N"` (no "Page X of Y"); remote has a 150ms debounce, re-requests only on `currentPage`/`currentSort`/`pageSize` (the re-request supersedes any in-flight request WHEN THE CHANGE IS MADE, not when the debounce fires — a response landing inside the window is discarded, and a superseded failure is silent: only the newest request reports `table-load-error`), and keeps rows on a failed reload (⚠️ row + empty-state only with no data). The error state (⚠️ row + `table--error` class) describes the last delivery ATTEMPT and clears on ANY arriving dataset — successful re-request, `setData()`, or a `table.data =` assignment (including `[]`, which then shows the empty state, not the stale error) — the assignment need NOT be a new array (same-reference clears it too), a drag row-reorder republishes the same rows and does NOT clear, and an already-in-flight request still reports its own failure when it settles. Local sort/search are client-side on `data` only. Client pagination re-clamps `currentPage` to the new last page when a filter or a shorter re-delivery leaves it past the end (an empty set keeps the declared page), so the body always shows the rows the summary claims.
84
85
 
85
86
  ## Methods
86
87
 
87
88
  - `getTableConfig()` - Request/apply controller config; `getTableData()` - request/apply current rows; `setData(data)` - non-eager bulk load; `setColumns(columns)` - assign and schedule both renders
88
89
  - `renderControls()` - Render legacy controls; `renderHeader()` - render headers/tools/filters; `renderSortableHeader(column)` - return sortable-header HTML; `renderBody()` - render display model; `renderPagination()` - render pager
89
90
  - `createCellElement(column,value,row?)` - Build rendered cell; `getCellTagName(type)` - resolve runtime cell tag
90
- - `getSelectedData()` - Resolve raw selected indices; `setSelectabilityCheck(fn)` - install row predicate; `updateRowSelectionState()` - sync rows; `updateSelectAllState()` - sync select-all
91
- - `toggleSort(key,multi?)` - Cycle direction; `setSortComparator(key,fn)` - install comparator; `goToPage(page)` - clamp/navigate; `setPageSize(size)` - resize/reset page
91
+ - `getSelectedData()` - Resolve raw selected indices; `setSelectabilityCheck(fn)` - install row predicate; `updateRowSelectionState()` - sync rows (keyed by each row's data index, so group/aggregate/spacer/filler rows are never marked selected); `updateSelectAllState()` - sync select-all (also runs on every body render, so a new row set re-derives checked/indeterminate)
92
+ - `toggleSort(key,multi?)` - Cycle direction; `setSortComparator(key,fn)` - install comparator (receives valueGetter-derived values); `goToPage(page)` - clamp/navigate; `setPageSize(size)` - resize/reset page
92
93
  - `setColumnFilter(column,operator,value)` - Set one filter; `removeColumnFilter(column)` - remove it; `setQuickFilter(text)` - global search; `setFilterModel(model)` - replace model; `getFilterModel()` - read model; `clearAllFilters()` - clear all
93
94
  - Filter operators: text `contains/notContains/equals/notEquals/startsWith/endsWith/isEmpty/isNotEmpty`; number `eq/neq/gt/gte/lt/lte/isEmpty/isNotEmpty`; date `is/isNot/before/onOrBefore/after/onOrAfter/isEmpty/isNotEmpty`; boolean `isTrue/isFalse`
94
95
  - `setColumnVisible(key,visible)` - Set visibility; `showAllColumns()` - show all; `hideAllColumns()` - hide hideable; `getColumnVisibility()` - read map; `pinColumn(key,side)` - pin; `unpinColumn(key)` - unpin
95
- - `autoSizeColumn(key)` - Measure one; `autoSizeAllColumns()` - measure all; `moveColumn(key,index)` - move; `setColumnGroups(groups)` - add grouped headers
96
- - `scrollToRow(index)` - Reveal data row; `scrollToColumn(key)` - reveal header; `getScrollPosition()` - read `{top,left}`; `setRowHeight(px)` - set fixed height; `setRowHeightCallback(fn)` - set rendered/virtual height
96
+ - `autoSizeColumn(key)` - Fit one column to its content; `autoSizeAllColumns()` - fit all. Both measure the rendered header + body under content-driven layout, so they SHRINK as well as grow, and repaint header AND body with the measured width. `moveColumn(key,index)` - move; `setColumnGroups(groups)` - add grouped headers
97
+ - `scrollToRow(index)` - Reveal data row (recomputes the virtual window in the same call — a programmatic scroll fires no `scroll` event, so the row is in the DOM on return); `scrollToColumn(key)` - reveal header; `getScrollPosition()` - read `{top,left}`; `setRowHeight(px)` - set fixed height; `setRowHeightCallback(fn)` - set rendered/virtual height
97
98
  - `startEdit(row,key)` - Start cell/row edit; `commitEdit()` - validate/commit; `cancelEdit()` - cancel; `setCellEditableCheck(fn)` - install predicate. Parsers/setters receive the real row; setters may return a value or row
98
99
  - `exportCSV(options?)` - Download filtered raw data; `printTable(options?)` - print visible text with toolbar/footer/checkbox options; `copyToClipboard(options?)` - copy formatted (default) or raw filtered data; `toggleFullscreen()` - toggle native/fallback fullscreen
99
- - `setToolbar(options)` - Install search/sort/filter/export/fullscreen UI; `setListViewRenderer(fn)` - render full-width custom list rows
100
+ - `setToolbar(options)` - Install search/sort/filter/export/fullscreen UI, `{showSearch?,showSort?,showFilter?,showExport?,searchPlaceholder?}`; fullscreen always appears; sort/filter buttons edit the header/menu models; `setListViewRenderer(fn)` - render full-width custom list rows
100
101
  - `setDetailPanel(options)` - Configure content/height/lazy lifecycle/icons; `expandRow(i)` - expand; `collapseRow(i)` - collapse; `toggleRowExpansion(i)` - toggle; `expandAllRows()` - expand all; `collapseAllRows()` - collapse all
101
- - `setTreeData(options)` - Configure path hierarchy; `expandTreeNode(key)` - expand; `collapseTreeNode(key)` - collapse; `toggleTreeNode(key)` - toggle; `expandAllTreeNodes()` - expand all; `collapseAllTreeNodes()` - collapse all
102
+ - `setTreeData(options)` - Configure path hierarchy; `expandTreeNode(key)` - expand; `collapseTreeNode(key)` - collapse; `toggleTreeNode(key)` - toggle; `expandAllTreeNodes()` - expand all; `collapseAllTreeNodes()` - collapse all. Every skipped path level becomes a gap node (`['Eng','Team','Alice']` alone renders Eng+Team+Alice); `defaultExpansionDepth` seeds the STARTING expansion once, so a full collapse sticks; the group column runs the normal display pipeline (`renderCell` > `formatter` > `valueFormatter`) beside the indent/chevron; tree rows render detail panels when `setDetailPanel()` is configured (gap nodes, index `-1`, never do)
102
103
  - `pinRowTop(row)` - Pin above; `pinRowBottom(row)` - pin below; `unpinRow(row)` - unpin same object; `clearPinnedRows()` - clear both areas
103
104
  - `<snice-column>`: `setFormatter(fn)` - set formatter; `addConditionalFormat(rule)` - append; `removeConditionalFormat(i)` - remove; `clearConditionalFormats()` - clear; `getColumnDefinition()` - build definition
104
105
  - `<snice-row>`: `select()` - select if allowed; `deselect()` - deselect; `focusRow()` - focus/scroll; `getCellValue(key)` - read; `setCellValue(key,value)` - update; `getCellElement(key)` - find cell; `updateCells()` - reconfigure cells; `highlight(ms?)` - temporary highlight
105
- - Toolbar options: `{showSearch?,showSort?,showFilter?,showExport?,searchPlaceholder?}`; fullscreen always appears; sort/filter buttons edit the header/menu models.
106
106
  - Remote `table/data` payload: `{search,sort,filter,selector,page?,pageSize?}`; response: `{data,totalItems?}`. Automatic initial request requires `mode="remote"`; page requests also require `pagination-mode="server"`.
107
107
  - CSV/clipboard resolve raw selected identities before intersecting filters, so active filters cannot retarget selection.
108
108
 
@@ -121,16 +121,18 @@ type CellStyle={backgroundColor?:string;color?:string;fontWeight?:'normal'|'bold
121
121
 
122
122
  ## Slots
123
123
 
124
- - `<snice-table>`: `columns`, `rows`, `header`, `empty-state` (cloned into the shadow body on EACH zero-row render — the slotted original is only a template); no default slot. `<snice-column>`: default inert content slot. Cells: no slots.
125
- - CDN Table bundle includes column, row, and cell dependencies. Grouping/aggregation upgrades declarative rows to the native table model.
124
+ - `<snice-table>`: `columns`, `rows`, `header`, `empty-state` (cloned into the shadow body on EACH zero-row render, virtualized or not — the slotted original is only a template); no default slot. `<snice-column>`: default inert content slot. Cells: no slots. CDN Table bundle includes column, row, and cell dependencies. Grouping/aggregation upgrades declarative rows to the native table model.
126
125
 
127
126
  ## CSS Parts
128
127
 
129
- - `<snice-table>`: `body`, `superheader`, `controls`, `toolbar`, `pagination`, `loading-overlay` (refetch spinner while `loading` with rows present), `row` (each native body row), `cell` (each native body cell), `filter-button` (column filter trigger), `sort-indicator` (sort direction); `<snice-row>`: `container`, `checkbox-cell`, `cell`; cells: `content`; actions: `action-button`; JSON: `toggle`; tag: `tag`; link/email/phone/location: `link`
128
+ - `<snice-table>`: `body`, `superheader`, `controls`, `toolbar`, `pagination`, `loading-overlay` (refetch spinner while `loading` with rows present), `row` (each native body row), `cell` (each native body cell — the inert filler that pads a body shorter than its host, virtualized or not, is neither and exposes no part), `filter-button` (column filter trigger), `sort-indicator` (sort direction); `<snice-row>`: `container`, `checkbox-cell`, `cell`; cells: `content`; actions: `action-button`; JSON: `toggle`; tag: `tag`; link/email/phone/location: `link`
129
+ - `column-fit` (default `scroll`) = the fitting policy. `scroll`: `minWidth` wins, `.table-frame` scrolls horizontally when the columns no longer fit. `squish`: the FRAME wins — `minWidth` relaxes to a 24px floor, every visible column shares the frame width, cell content AND header labels ellipsise (typed cells too: squish relaxes their 100px floor via `--snice-table-cell-min-width`, without which they clip mid-glyph), `.table-frame` gets `overflow-x:hidden`, and the rightmost column edge sits on the frame's inner edge at any width. Squish resize is allowed and REBALANCES: the dragged column keeps its width (capped so the rest can still sit on the floor) and the others scale to fill what is left. Declared widths are honoured while an unsized column can absorb the difference; when the authored widths alone overflow, every column is scaled by the same factor. Reactive — assigning `columnFit` re-fits the painted columns. Use squish for a table inside a card/tile/split pane; use scroll for a wide dataset (pinned columns only pay off when something scrolls).
130
+ - Column width: a column that declares no `width` SHARES the frame — grown or shrunk on every render and frame resize, floored at `minWidth` (default 50), after which the frame scrolls instead of collapsing. A declared `width`, a drag-resize, and an auto-size are authored widths and are never refitted. Pinned cells carry `pinned-cell` + `pinned-cell--left|right`; the inner-most column per side adds `pinned-cell--edge` (2px divider + shadow) and its header a `.pin-indicator` pin glyph, so pinning reads even when nothing scrolls.
131
+ - Height fill: the filler only consumes space the frame ALREADY has. A content-driven host (`:host{height:100%}` against an auto/max-content parent — stretched grid/flex item, plain block) would make the filler an input to its own resize trigger, so the table applies it, verifies the frame kept its height, and hands back any self-caused growth (partial filler, or none). Size the host (explicit height / flex or grid track / sized ancestor) to get a full-height grid.
130
132
 
131
133
  ## CSS Custom Properties
132
134
 
133
- - `--snice-table-body-bg`, `--snice-table-group-header-bg`, `--snice-table-group-header-color`, `--snice-table-group-count-bg`, `--snice-table-group-count-color`, `--snice-table-aggregate-bg`, `--snice-table-aggregate-border-color`, `--snice-table-aggregate-color`, `--snice-table-aggregate-label-color`, `--snice-table-cell-padding`, `--snice-table-cell-border` (vertical; `none` drops grid lines), `--snice-table-row-border` (horizontal)
135
+ - `--snice-table-stripe-bg` (`striped` row tint; defaults to `--snice-color-overlay-stripe`, an alpha tint tuned per theme), `--snice-table-body-bg`, `--snice-table-group-header-bg`, `--snice-table-group-header-color`, `--snice-table-group-count-bg`, `--snice-table-group-count-color`, `--snice-table-aggregate-bg`, `--snice-table-aggregate-border-color`, `--snice-table-aggregate-color`, `--snice-table-aggregate-label-color`, `--snice-table-cell-padding`, `--snice-table-cell-border` (vertical; `none` drops grid lines), `--snice-table-row-border` (horizontal), `--snice-table-cell-min-width` (typed-cell width floor, default `6.25rem`; squish sets it to `0`)
134
136
 
135
137
  ## Basic Usage
136
138
 
@@ -141,10 +143,7 @@ table.data = employees;
141
143
  table.setToolbar({showSearch:true,showSort:true,showFilter:true,showExport:true});
142
144
  ```
143
145
 
144
- ## Keyboard Navigation
146
+ ## Keyboard & Accessibility
145
147
 
146
148
  - Grid: arrows, Home/End, Page Up/Down; Enter edits; Space/Shift+Space toggles selection; Ctrl/Cmd+A selects filtered selectable rows. Group/tree/detail buttons: Tab then Enter/Space.
147
-
148
- ## Accessibility
149
-
150
- - Native table uses ARIA grid roles, live counts, roving focus, `aria-sort`, and `aria-selected`; group/tree/detail controls expose labels/`aria-expanded`, and group selection supports mixed state.
149
+ - Native table uses ARIA grid roles, live counts, roving focus, `aria-sort`, and `aria-selected`; group/tree/detail controls expose labels/`aria-expanded`, and group selection supports mixed state. `aria-selected="true"` is written with `data-selected` on every selection path, including click/checkbox/select-all deltas — not only on a full body render.
@@ -56,6 +56,7 @@ CSS-only design token system. No component -- just import the stylesheet.
56
56
  --snice-color-overlay-hover /* hsl(0 0% 0% / 0.04) — hover tint over any surface */
57
57
  --snice-color-overlay-selected /* hsl(primary / 0.08) — selected row/item tint */
58
58
  --snice-color-overlay-selected-hover /* hsl(primary / 0.12) — selected + hovered */
59
+ --snice-color-overlay-stripe /* hsl(0 0% 0%/.02) light, hsl(0 0% 100%/.11) dark — zebra row tint; dark needs the bigger step */
59
60
  ```
60
61
 
61
62
  ### Spacing
package/docs/ai/events.md CHANGED
@@ -317,6 +317,36 @@ cancels an older timer and dispatches the new result immediately. Throttle
317
317
  trailing detail is latest-wins; its deadline is last actual dispatch + the
318
318
  newly resolved interval.
319
319
 
320
+ ### cancelable — suppressible default actions
321
+
322
+ `cancelable` is an `EventInit` field, so `@dispatch('x', { cancelable: true })`
323
+ DOES produce a cancelable event and listeners can call `preventDefault()`.
324
+
325
+ But the decorated method returns the **detail**, not `dispatchEvent`'s boolean,
326
+ so the component cannot see whether the event was cancelled. A component with a
327
+ default action to suppress must dispatch by hand and read the result:
328
+
329
+ ```typescript
330
+ // ❌ Cancelable, but the component can never act on the cancellation
331
+ @dispatch('more-click', { cancelable: true })
332
+ private emitMore(date: Date) { return { date }; }
333
+
334
+ // ✅ Default action the app can cancel
335
+ private emitMore(date: Date): boolean {
336
+ return this.dispatchEvent(new CustomEvent('more-click', {
337
+ detail: { date }, bubbles: true, composed: true, cancelable: true,
338
+ }));
339
+ }
340
+
341
+ private onChipClick(date: Date) {
342
+ if (!this.emitMore(date)) return; // preventDefault() -> app owns it
343
+ this.openBuiltInPanel(date);
344
+ }
345
+ ```
346
+
347
+ In the library: `snice-link`'s `navigate`, `snice-stepper`'s `step-change`,
348
+ `snice-calendar`'s `calendar-more-click`.
349
+
320
350
  ### scope — dispatch target
321
351
 
322
352
  Default: `this.dispatchEvent(event)` — event originates from the host element. `scope` redirects the dispatch to another target so the event behaves as if it originated there. Use with `@on({ scope })` to express cross-cutting events without bubbling.
package/docs/ai/theme.md CHANGED
@@ -58,6 +58,7 @@ Modern syntax with alpha: `hsl(0 0% 0% / 0.15)` (not `hsla(0, 0%, 0%, 0.15)`)
58
58
  --snice-color-overlay-hover: hsl(0 0% 0% / 0.04);
59
59
  --snice-color-overlay-selected: hsl(var(--snice-color-blue-500) / 0.08);
60
60
  --snice-color-overlay-selected-hover: hsl(var(--snice-color-blue-500) / 0.12);
61
+ --snice-color-overlay-stripe: hsl(0 0% 0% / 0.02); /* dark: hsl(0 0% 100% / 0.11) */
61
62
  ```
62
63
 
63
64
  ## Layers (z-index)