snice 5.2.2 → 6.0.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 (653) hide show
  1. package/README.md +5 -8
  2. package/adapters/react/SniceProvider.js +1 -1
  3. package/adapters/react/SniceRouter.js +1 -1
  4. package/adapters/react/audio-recorder.d.ts +0 -4
  5. package/adapters/react/audio-recorder.d.ts.map +1 -1
  6. package/adapters/react/audio-recorder.js +1 -1
  7. package/adapters/react/audio-recorder.js.map +1 -1
  8. package/adapters/react/audio-recorder.tsx +1 -5
  9. package/adapters/react/avatar.d.ts +0 -1
  10. package/adapters/react/avatar.d.ts.map +1 -1
  11. package/adapters/react/avatar.js +1 -1
  12. package/adapters/react/avatar.js.map +1 -1
  13. package/adapters/react/avatar.tsx +1 -2
  14. package/adapters/react/breadcrumbs.d.ts +0 -1
  15. package/adapters/react/breadcrumbs.d.ts.map +1 -1
  16. package/adapters/react/breadcrumbs.js +1 -1
  17. package/adapters/react/breadcrumbs.js.map +1 -1
  18. package/adapters/react/breadcrumbs.tsx +1 -2
  19. package/adapters/react/camera-annotate.d.ts +0 -5
  20. package/adapters/react/camera-annotate.d.ts.map +1 -1
  21. package/adapters/react/camera-annotate.js +1 -1
  22. package/adapters/react/camera-annotate.js.map +1 -1
  23. package/adapters/react/camera-annotate.tsx +1 -6
  24. package/adapters/react/card.d.ts +0 -2
  25. package/adapters/react/card.d.ts.map +1 -1
  26. package/adapters/react/card.js +1 -1
  27. package/adapters/react/card.js.map +1 -1
  28. package/adapters/react/card.tsx +1 -3
  29. package/adapters/react/carousel.d.ts +0 -1
  30. package/adapters/react/carousel.d.ts.map +1 -1
  31. package/adapters/react/carousel.js +1 -1
  32. package/adapters/react/carousel.js.map +1 -1
  33. package/adapters/react/carousel.tsx +1 -2
  34. package/adapters/react/cart.d.ts +0 -1
  35. package/adapters/react/cart.d.ts.map +1 -1
  36. package/adapters/react/cart.js +1 -1
  37. package/adapters/react/cart.js.map +1 -1
  38. package/adapters/react/cart.tsx +1 -2
  39. package/adapters/react/chart.d.ts +0 -5
  40. package/adapters/react/chart.d.ts.map +1 -1
  41. package/adapters/react/chart.js +1 -1
  42. package/adapters/react/chart.js.map +1 -1
  43. package/adapters/react/chart.tsx +1 -6
  44. package/adapters/react/chat.d.ts +4 -1
  45. package/adapters/react/chat.d.ts.map +1 -1
  46. package/adapters/react/chat.js +2 -2
  47. package/adapters/react/chat.js.map +1 -1
  48. package/adapters/react/chat.tsx +6 -3
  49. package/adapters/react/chip.d.ts +0 -1
  50. package/adapters/react/chip.d.ts.map +1 -1
  51. package/adapters/react/chip.js +1 -1
  52. package/adapters/react/chip.js.map +1 -1
  53. package/adapters/react/chip.tsx +1 -2
  54. package/adapters/react/command-palette.d.ts +0 -4
  55. package/adapters/react/command-palette.d.ts.map +1 -1
  56. package/adapters/react/command-palette.js +1 -1
  57. package/adapters/react/command-palette.js.map +1 -1
  58. package/adapters/react/command-palette.tsx +1 -5
  59. package/adapters/react/comments.d.ts +0 -1
  60. package/adapters/react/comments.d.ts.map +1 -1
  61. package/adapters/react/comments.js +1 -1
  62. package/adapters/react/comments.js.map +1 -1
  63. package/adapters/react/comments.tsx +1 -2
  64. package/adapters/react/countdown.d.ts +0 -4
  65. package/adapters/react/countdown.d.ts.map +1 -1
  66. package/adapters/react/countdown.js +1 -1
  67. package/adapters/react/countdown.js.map +1 -1
  68. package/adapters/react/countdown.tsx +1 -5
  69. package/adapters/react/date-time-picker.d.ts +0 -1
  70. package/adapters/react/date-time-picker.d.ts.map +1 -1
  71. package/adapters/react/date-time-picker.js +1 -1
  72. package/adapters/react/date-time-picker.js.map +1 -1
  73. package/adapters/react/date-time-picker.tsx +1 -2
  74. package/adapters/react/file-upload.d.ts +0 -2
  75. package/adapters/react/file-upload.d.ts.map +1 -1
  76. package/adapters/react/file-upload.js +1 -1
  77. package/adapters/react/file-upload.js.map +1 -1
  78. package/adapters/react/file-upload.tsx +1 -3
  79. package/adapters/react/heatmap.d.ts +0 -4
  80. package/adapters/react/heatmap.d.ts.map +1 -1
  81. package/adapters/react/heatmap.js +1 -1
  82. package/adapters/react/heatmap.js.map +1 -1
  83. package/adapters/react/heatmap.tsx +1 -5
  84. package/adapters/react/image.d.ts +0 -2
  85. package/adapters/react/image.d.ts.map +1 -1
  86. package/adapters/react/image.js +1 -1
  87. package/adapters/react/image.js.map +1 -1
  88. package/adapters/react/image.tsx +1 -3
  89. package/adapters/react/kanban.d.ts +0 -4
  90. package/adapters/react/kanban.d.ts.map +1 -1
  91. package/adapters/react/kanban.js +1 -1
  92. package/adapters/react/kanban.js.map +1 -1
  93. package/adapters/react/kanban.tsx +1 -5
  94. package/adapters/react/key-value.d.ts +0 -2
  95. package/adapters/react/key-value.d.ts.map +1 -1
  96. package/adapters/react/key-value.js +1 -1
  97. package/adapters/react/key-value.js.map +1 -1
  98. package/adapters/react/key-value.tsx +1 -3
  99. package/adapters/react/leaderboard.d.ts +0 -1
  100. package/adapters/react/leaderboard.d.ts.map +1 -1
  101. package/adapters/react/leaderboard.js +1 -1
  102. package/adapters/react/leaderboard.js.map +1 -1
  103. package/adapters/react/leaderboard.tsx +1 -2
  104. package/adapters/react/map.d.ts +0 -3
  105. package/adapters/react/map.d.ts.map +1 -1
  106. package/adapters/react/map.js +1 -1
  107. package/adapters/react/map.js.map +1 -1
  108. package/adapters/react/map.tsx +1 -4
  109. package/adapters/react/markdown.d.ts +1 -1
  110. package/adapters/react/markdown.d.ts.map +1 -1
  111. package/adapters/react/markdown.js +1 -1
  112. package/adapters/react/markdown.js.map +1 -1
  113. package/adapters/react/markdown.tsx +2 -2
  114. package/adapters/react/matchRoute.d.ts +1 -1
  115. package/adapters/react/matchRoute.js +38 -4
  116. package/adapters/react/matchRoute.js.map +1 -1
  117. package/adapters/react/message-strip.d.ts +1 -1
  118. package/adapters/react/message-strip.js +1 -1
  119. package/adapters/react/message-strip.tsx +2 -2
  120. package/adapters/react/music-player.d.ts +0 -1
  121. package/adapters/react/music-player.d.ts.map +1 -1
  122. package/adapters/react/music-player.js +1 -1
  123. package/adapters/react/music-player.js.map +1 -1
  124. package/adapters/react/music-player.tsx +1 -2
  125. package/adapters/react/org-chart.d.ts +0 -1
  126. package/adapters/react/org-chart.d.ts.map +1 -1
  127. package/adapters/react/org-chart.js +1 -1
  128. package/adapters/react/org-chart.js.map +1 -1
  129. package/adapters/react/org-chart.tsx +1 -2
  130. package/adapters/react/paint.d.ts +0 -1
  131. package/adapters/react/paint.d.ts.map +1 -1
  132. package/adapters/react/paint.js +1 -1
  133. package/adapters/react/paint.js.map +1 -1
  134. package/adapters/react/paint.tsx +1 -2
  135. package/adapters/react/podcast-player.d.ts +0 -2
  136. package/adapters/react/podcast-player.d.ts.map +1 -1
  137. package/adapters/react/podcast-player.js +1 -1
  138. package/adapters/react/podcast-player.js.map +1 -1
  139. package/adapters/react/podcast-player.tsx +1 -3
  140. package/adapters/react/product-card.d.ts +0 -3
  141. package/adapters/react/product-card.d.ts.map +1 -1
  142. package/adapters/react/product-card.js +1 -1
  143. package/adapters/react/product-card.js.map +1 -1
  144. package/adapters/react/product-card.tsx +1 -4
  145. package/adapters/react/qr-reader.d.ts +0 -4
  146. package/adapters/react/qr-reader.d.ts.map +1 -1
  147. package/adapters/react/qr-reader.js +1 -1
  148. package/adapters/react/qr-reader.js.map +1 -1
  149. package/adapters/react/qr-reader.tsx +1 -5
  150. package/adapters/react/recipe.d.ts +0 -5
  151. package/adapters/react/recipe.d.ts.map +1 -1
  152. package/adapters/react/recipe.js +1 -1
  153. package/adapters/react/recipe.js.map +1 -1
  154. package/adapters/react/recipe.tsx +1 -6
  155. package/adapters/react/route-specificity.d.ts +1 -0
  156. package/adapters/react/spotlight.d.ts +0 -2
  157. package/adapters/react/spotlight.d.ts.map +1 -1
  158. package/adapters/react/spotlight.js +1 -1
  159. package/adapters/react/spotlight.js.map +1 -1
  160. package/adapters/react/spotlight.tsx +1 -3
  161. package/adapters/react/tag-input.d.ts +0 -4
  162. package/adapters/react/tag-input.d.ts.map +1 -1
  163. package/adapters/react/tag-input.js +1 -1
  164. package/adapters/react/tag-input.js.map +1 -1
  165. package/adapters/react/tag-input.tsx +1 -5
  166. package/adapters/react/terminal.d.ts +0 -1
  167. package/adapters/react/terminal.d.ts.map +1 -1
  168. package/adapters/react/terminal.js +1 -1
  169. package/adapters/react/terminal.js.map +1 -1
  170. package/adapters/react/terminal.tsx +1 -2
  171. package/adapters/react/time-picker.d.ts +0 -1
  172. package/adapters/react/time-picker.d.ts.map +1 -1
  173. package/adapters/react/time-picker.js +1 -1
  174. package/adapters/react/time-picker.js.map +1 -1
  175. package/adapters/react/time-picker.tsx +1 -2
  176. package/adapters/react/timer.d.ts +0 -1
  177. package/adapters/react/timer.d.ts.map +1 -1
  178. package/adapters/react/timer.js +1 -1
  179. package/adapters/react/timer.js.map +1 -1
  180. package/adapters/react/timer.tsx +1 -2
  181. package/adapters/react/useRequestHandler.js +1 -1
  182. package/adapters/react/video-player.d.ts +0 -6
  183. package/adapters/react/video-player.d.ts.map +1 -1
  184. package/adapters/react/video-player.js +1 -1
  185. package/adapters/react/video-player.js.map +1 -1
  186. package/adapters/react/video-player.tsx +1 -7
  187. package/adapters/react/virtual-scroller.d.ts +0 -1
  188. package/adapters/react/virtual-scroller.d.ts.map +1 -1
  189. package/adapters/react/virtual-scroller.js +1 -1
  190. package/adapters/react/virtual-scroller.js.map +1 -1
  191. package/adapters/react/virtual-scroller.tsx +1 -2
  192. package/bin/templates/default/tests/services/jwt.test.ts +8 -1
  193. package/bin/templates/react/tests/services/jwt.test.ts +10 -3
  194. package/dist/cdn/accordion/snice-accordion.js +1 -1
  195. package/dist/cdn/accordion/snice-accordion.min.js +1 -1
  196. package/dist/cdn/action-bar/README.md +2 -2
  197. package/dist/cdn/action-bar/snice-action-bar.js +2 -2
  198. package/dist/cdn/action-bar/snice-action-bar.js.map +1 -1
  199. package/dist/cdn/action-bar/snice-action-bar.min.js +2 -2
  200. package/dist/cdn/action-bar/snice-action-bar.min.js.map +1 -1
  201. package/dist/cdn/activity-feed/snice-activity-feed.js +1 -1
  202. package/dist/cdn/activity-feed/snice-activity-feed.min.js +1 -1
  203. package/dist/cdn/alert/snice-alert.js +1 -1
  204. package/dist/cdn/alert/snice-alert.js.map +1 -1
  205. package/dist/cdn/alert/snice-alert.min.js +1 -1
  206. package/dist/cdn/alert/snice-alert.min.js.map +1 -1
  207. package/dist/cdn/app-tiles/snice-app-tiles.js +1 -1
  208. package/dist/cdn/app-tiles/snice-app-tiles.min.js +1 -1
  209. package/dist/cdn/approval-flow/snice-approval-flow.js +1 -1
  210. package/dist/cdn/approval-flow/snice-approval-flow.js.map +1 -1
  211. package/dist/cdn/approval-flow/snice-approval-flow.min.js +1 -1
  212. package/dist/cdn/approval-flow/snice-approval-flow.min.js.map +1 -1
  213. package/dist/cdn/audio-recorder/snice-audio-recorder.js +1 -1
  214. package/dist/cdn/audio-recorder/snice-audio-recorder.min.js +1 -1
  215. package/dist/cdn/availability/snice-availability.js +1 -1
  216. package/dist/cdn/availability/snice-availability.min.js +1 -1
  217. package/dist/cdn/avatar/snice-avatar.js +1 -1
  218. package/dist/cdn/avatar/snice-avatar.min.js +1 -1
  219. package/dist/cdn/avatar-group/snice-avatar-group.js +1 -1
  220. package/dist/cdn/avatar-group/snice-avatar-group.min.js +1 -1
  221. package/dist/cdn/badge/snice-badge.js +1 -1
  222. package/dist/cdn/badge/snice-badge.js.map +1 -1
  223. package/dist/cdn/badge/snice-badge.min.js +1 -1
  224. package/dist/cdn/badge/snice-badge.min.js.map +1 -1
  225. package/dist/cdn/banner/README.md +1 -1
  226. package/dist/cdn/banner/snice-banner.js +2 -2
  227. package/dist/cdn/banner/snice-banner.js.map +1 -1
  228. package/dist/cdn/banner/snice-banner.min.js +2 -2
  229. package/dist/cdn/banner/snice-banner.min.js.map +1 -1
  230. package/dist/cdn/binpack/snice-binpack.js +1 -1
  231. package/dist/cdn/binpack/snice-binpack.min.js +1 -1
  232. package/dist/cdn/book/snice-book.js +1 -1
  233. package/dist/cdn/book/snice-book.min.js +1 -1
  234. package/dist/cdn/booking/snice-booking.js +1 -1
  235. package/dist/cdn/booking/snice-booking.min.js +1 -1
  236. package/dist/cdn/breadcrumbs/snice-breadcrumbs.js +1 -1
  237. package/dist/cdn/breadcrumbs/snice-breadcrumbs.js.map +1 -1
  238. package/dist/cdn/breadcrumbs/snice-breadcrumbs.min.js +1 -1
  239. package/dist/cdn/breadcrumbs/snice-breadcrumbs.min.js.map +1 -1
  240. package/dist/cdn/button/snice-button.js +1 -1
  241. package/dist/cdn/button/snice-button.js.map +1 -1
  242. package/dist/cdn/button/snice-button.min.js +1 -1
  243. package/dist/cdn/button/snice-button.min.js.map +1 -1
  244. package/dist/cdn/calendar/snice-calendar.js +1 -1
  245. package/dist/cdn/calendar/snice-calendar.min.js +1 -1
  246. package/dist/cdn/camera/snice-camera.js +1 -1
  247. package/dist/cdn/camera/snice-camera.min.js +1 -1
  248. package/dist/cdn/camera-annotate/snice-camera-annotate.js +1 -1
  249. package/dist/cdn/camera-annotate/snice-camera-annotate.min.js +1 -1
  250. package/dist/cdn/candlestick/snice-candlestick.js +1 -1
  251. package/dist/cdn/candlestick/snice-candlestick.min.js +1 -1
  252. package/dist/cdn/card/snice-card.js +1 -1
  253. package/dist/cdn/card/snice-card.min.js +1 -1
  254. package/dist/cdn/carousel/snice-carousel.js +1 -1
  255. package/dist/cdn/carousel/snice-carousel.min.js +1 -1
  256. package/dist/cdn/cart/snice-cart.js +1 -1
  257. package/dist/cdn/cart/snice-cart.min.js +1 -1
  258. package/dist/cdn/chart/snice-chart.js +1 -1
  259. package/dist/cdn/chart/snice-chart.js.map +1 -1
  260. package/dist/cdn/chart/snice-chart.min.js +1 -1
  261. package/dist/cdn/chart/snice-chart.min.js.map +1 -1
  262. package/dist/cdn/chat/README.md +2 -2
  263. package/dist/cdn/chat/snice-chat.js +785 -89
  264. package/dist/cdn/chat/snice-chat.js.map +1 -1
  265. package/dist/cdn/chat/snice-chat.min.js +71 -75
  266. package/dist/cdn/chat/snice-chat.min.js.map +1 -1
  267. package/dist/cdn/checkbox/snice-checkbox.js +1 -1
  268. package/dist/cdn/checkbox/snice-checkbox.min.js +1 -1
  269. package/dist/cdn/chip/snice-chip.js +1 -1
  270. package/dist/cdn/chip/snice-chip.js.map +1 -1
  271. package/dist/cdn/chip/snice-chip.min.js +1 -1
  272. package/dist/cdn/chip/snice-chip.min.js.map +1 -1
  273. package/dist/cdn/code-block/snice-code-block.js +1 -1
  274. package/dist/cdn/code-block/snice-code-block.min.js +1 -1
  275. package/dist/cdn/color-display/snice-color-display.js +1 -1
  276. package/dist/cdn/color-display/snice-color-display.min.js +1 -1
  277. package/dist/cdn/color-picker/snice-color-picker.js +1 -1
  278. package/dist/cdn/color-picker/snice-color-picker.min.js +1 -1
  279. package/dist/cdn/command-palette/README.md +1 -1
  280. package/dist/cdn/command-palette/snice-command-palette.js +2 -2
  281. package/dist/cdn/command-palette/snice-command-palette.js.map +1 -1
  282. package/dist/cdn/command-palette/snice-command-palette.min.js +2 -2
  283. package/dist/cdn/command-palette/snice-command-palette.min.js.map +1 -1
  284. package/dist/cdn/comments/snice-comments.js +1 -1
  285. package/dist/cdn/comments/snice-comments.min.js +1 -1
  286. package/dist/cdn/countdown/snice-countdown.js +1 -1
  287. package/dist/cdn/countdown/snice-countdown.min.js +1 -1
  288. package/dist/cdn/cropper/snice-cropper.js +1 -1
  289. package/dist/cdn/cropper/snice-cropper.min.js +1 -1
  290. package/dist/cdn/data-card/snice-data-card.js +1 -1
  291. package/dist/cdn/data-card/snice-data-card.js.map +1 -1
  292. package/dist/cdn/data-card/snice-data-card.min.js +1 -1
  293. package/dist/cdn/data-card/snice-data-card.min.js.map +1 -1
  294. package/dist/cdn/date-picker/snice-date-picker.js +1 -1
  295. package/dist/cdn/date-picker/snice-date-picker.min.js +1 -1
  296. package/dist/cdn/date-range-picker/snice-date-range-picker.js +1 -1
  297. package/dist/cdn/date-range-picker/snice-date-range-picker.min.js +1 -1
  298. package/dist/cdn/date-time-picker/snice-date-time-picker.js +1 -1
  299. package/dist/cdn/date-time-picker/snice-date-time-picker.min.js +1 -1
  300. package/dist/cdn/diff/snice-diff.js +1 -1
  301. package/dist/cdn/diff/snice-diff.min.js +1 -1
  302. package/dist/cdn/divider/snice-divider.js +1 -1
  303. package/dist/cdn/divider/snice-divider.min.js +1 -1
  304. package/dist/cdn/doc/snice-doc.js +1 -1
  305. package/dist/cdn/doc/snice-doc.min.js +1 -1
  306. package/dist/cdn/draw/snice-draw.js +1 -1
  307. package/dist/cdn/draw/snice-draw.min.js +1 -1
  308. package/dist/cdn/drawer/snice-drawer.js +1 -1
  309. package/dist/cdn/drawer/snice-drawer.js.map +1 -1
  310. package/dist/cdn/drawer/snice-drawer.min.js +1 -1
  311. package/dist/cdn/drawer/snice-drawer.min.js.map +1 -1
  312. package/dist/cdn/empty-state/snice-empty-state.js +1 -1
  313. package/dist/cdn/empty-state/snice-empty-state.js.map +1 -1
  314. package/dist/cdn/empty-state/snice-empty-state.min.js +1 -1
  315. package/dist/cdn/empty-state/snice-empty-state.min.js.map +1 -1
  316. package/dist/cdn/estimate/snice-estimate.js +1 -1
  317. package/dist/cdn/estimate/snice-estimate.min.js +1 -1
  318. package/dist/cdn/file-gallery/snice-file-gallery.js +1 -1
  319. package/dist/cdn/file-gallery/snice-file-gallery.min.js +1 -1
  320. package/dist/cdn/file-upload/snice-file-upload.js +1 -1
  321. package/dist/cdn/file-upload/snice-file-upload.min.js +1 -1
  322. package/dist/cdn/flip-card/snice-flip-card.js +1 -1
  323. package/dist/cdn/flip-card/snice-flip-card.min.js +1 -1
  324. package/dist/cdn/flow/snice-flow.js +1 -1
  325. package/dist/cdn/flow/snice-flow.min.js +1 -1
  326. package/dist/cdn/form-layout/snice-form-layout.js +1 -1
  327. package/dist/cdn/form-layout/snice-form-layout.min.js +1 -1
  328. package/dist/cdn/funnel/snice-funnel.js +1 -1
  329. package/dist/cdn/funnel/snice-funnel.js.map +1 -1
  330. package/dist/cdn/funnel/snice-funnel.min.js +1 -1
  331. package/dist/cdn/funnel/snice-funnel.min.js.map +1 -1
  332. package/dist/cdn/gantt/snice-gantt.js +1 -1
  333. package/dist/cdn/gantt/snice-gantt.min.js +1 -1
  334. package/dist/cdn/gauge/snice-gauge.js +1 -1
  335. package/dist/cdn/gauge/snice-gauge.min.js +1 -1
  336. package/dist/cdn/grid/snice-grid.js +1 -1
  337. package/dist/cdn/grid/snice-grid.min.js +1 -1
  338. package/dist/cdn/heatmap/snice-heatmap.js +1 -1
  339. package/dist/cdn/heatmap/snice-heatmap.min.js +1 -1
  340. package/dist/cdn/image/snice-image.js +1 -1
  341. package/dist/cdn/image/snice-image.js.map +1 -1
  342. package/dist/cdn/image/snice-image.min.js +1 -1
  343. package/dist/cdn/image/snice-image.min.js.map +1 -1
  344. package/dist/cdn/input/snice-input.js +1 -1
  345. package/dist/cdn/input/snice-input.js.map +1 -1
  346. package/dist/cdn/input/snice-input.min.js +1 -1
  347. package/dist/cdn/input/snice-input.min.js.map +1 -1
  348. package/dist/cdn/invoice/README.md +1 -1
  349. package/dist/cdn/invoice/snice-invoice.js +2 -2
  350. package/dist/cdn/invoice/snice-invoice.js.map +1 -1
  351. package/dist/cdn/invoice/snice-invoice.min.js +2 -2
  352. package/dist/cdn/invoice/snice-invoice.min.js.map +1 -1
  353. package/dist/cdn/kanban/snice-kanban.js +1 -1
  354. package/dist/cdn/kanban/snice-kanban.min.js +1 -1
  355. package/dist/cdn/key-value/snice-key-value.js +1 -1
  356. package/dist/cdn/key-value/snice-key-value.min.js +1 -1
  357. package/dist/cdn/kpi/snice-kpi.js +1 -1
  358. package/dist/cdn/kpi/snice-kpi.js.map +1 -1
  359. package/dist/cdn/kpi/snice-kpi.min.js +1 -1
  360. package/dist/cdn/kpi/snice-kpi.min.js.map +1 -1
  361. package/dist/cdn/layout/snice-layout.js +1 -1
  362. package/dist/cdn/layout/snice-layout.min.js +1 -1
  363. package/dist/cdn/leaderboard/snice-leaderboard.js +1 -1
  364. package/dist/cdn/leaderboard/snice-leaderboard.min.js +1 -1
  365. package/dist/cdn/link/snice-link.js +1 -1
  366. package/dist/cdn/link/snice-link.min.js +1 -1
  367. package/dist/cdn/link-preview/snice-link-preview.js +1 -1
  368. package/dist/cdn/link-preview/snice-link-preview.min.js +1 -1
  369. package/dist/cdn/list/snice-list.js +1 -1
  370. package/dist/cdn/list/snice-list.js.map +1 -1
  371. package/dist/cdn/list/snice-list.min.js +1 -1
  372. package/dist/cdn/list/snice-list.min.js.map +1 -1
  373. package/dist/cdn/location/snice-location.js +1 -1
  374. package/dist/cdn/location/snice-location.js.map +1 -1
  375. package/dist/cdn/location/snice-location.min.js +1 -1
  376. package/dist/cdn/location/snice-location.min.js.map +1 -1
  377. package/dist/cdn/login/snice-login.js +3 -1
  378. package/dist/cdn/login/snice-login.js.map +1 -1
  379. package/dist/cdn/login/snice-login.min.js +1 -1
  380. package/dist/cdn/login/snice-login.min.js.map +1 -1
  381. package/dist/cdn/map/snice-map.js +1 -1
  382. package/dist/cdn/map/snice-map.min.js +1 -1
  383. package/dist/cdn/markdown/README.md +2 -2
  384. package/dist/cdn/markdown/snice-markdown.js +16 -3
  385. package/dist/cdn/markdown/snice-markdown.js.map +1 -1
  386. package/dist/cdn/markdown/snice-markdown.min.js +3 -3
  387. package/dist/cdn/markdown/snice-markdown.min.js.map +1 -1
  388. package/dist/cdn/masonry/snice-masonry.js +1 -1
  389. package/dist/cdn/masonry/snice-masonry.min.js +1 -1
  390. package/dist/cdn/menu/snice-menu.js +2 -2
  391. package/dist/cdn/menu/snice-menu.js.map +1 -1
  392. package/dist/cdn/menu/snice-menu.min.js +2 -2
  393. package/dist/cdn/menu/snice-menu.min.js.map +1 -1
  394. package/dist/cdn/message-strip/snice-message-strip.js +9 -9
  395. package/dist/cdn/message-strip/snice-message-strip.js.map +1 -1
  396. package/dist/cdn/message-strip/snice-message-strip.min.js +3 -3
  397. package/dist/cdn/message-strip/snice-message-strip.min.js.map +1 -1
  398. package/dist/cdn/metric-table/snice-metric-table.js +1 -1
  399. package/dist/cdn/metric-table/snice-metric-table.min.js +1 -1
  400. package/dist/cdn/modal/snice-modal.js +1 -1
  401. package/dist/cdn/modal/snice-modal.js.map +1 -1
  402. package/dist/cdn/modal/snice-modal.min.js +1 -1
  403. package/dist/cdn/modal/snice-modal.min.js.map +1 -1
  404. package/dist/cdn/music-player/snice-music-player.js +1 -1
  405. package/dist/cdn/music-player/snice-music-player.min.js +1 -1
  406. package/dist/cdn/nav/snice-nav.js +1 -1
  407. package/dist/cdn/nav/snice-nav.min.js +1 -1
  408. package/dist/cdn/network-graph/snice-network-graph.js +1 -1
  409. package/dist/cdn/network-graph/snice-network-graph.js.map +1 -1
  410. package/dist/cdn/network-graph/snice-network-graph.min.js +1 -1
  411. package/dist/cdn/network-graph/snice-network-graph.min.js.map +1 -1
  412. package/dist/cdn/notification-center/snice-notification-center.js +1 -1
  413. package/dist/cdn/notification-center/snice-notification-center.js.map +1 -1
  414. package/dist/cdn/notification-center/snice-notification-center.min.js +1 -1
  415. package/dist/cdn/notification-center/snice-notification-center.min.js.map +1 -1
  416. package/dist/cdn/order-tracker/snice-order-tracker.js +1 -1
  417. package/dist/cdn/order-tracker/snice-order-tracker.min.js +1 -1
  418. package/dist/cdn/org-chart/snice-org-chart.js +1 -1
  419. package/dist/cdn/org-chart/snice-org-chart.min.js +1 -1
  420. package/dist/cdn/pagination/snice-pagination.js +1 -1
  421. package/dist/cdn/pagination/snice-pagination.min.js +1 -1
  422. package/dist/cdn/paint/snice-paint.js +1 -1
  423. package/dist/cdn/paint/snice-paint.min.js +1 -1
  424. package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +1 -1
  425. package/dist/cdn/pdf-viewer/snice-pdf-viewer.min.js +1 -1
  426. package/dist/cdn/permission-matrix/snice-permission-matrix.js +1 -1
  427. package/dist/cdn/permission-matrix/snice-permission-matrix.min.js +1 -1
  428. package/dist/cdn/podcast-player/snice-podcast-player.js +1 -1
  429. package/dist/cdn/podcast-player/snice-podcast-player.min.js +1 -1
  430. package/dist/cdn/popover/snice-popover.js +1 -1
  431. package/dist/cdn/popover/snice-popover.min.js +1 -1
  432. package/dist/cdn/pricing-table/snice-pricing-table.js +1 -1
  433. package/dist/cdn/pricing-table/snice-pricing-table.min.js +1 -1
  434. package/dist/cdn/product-card/snice-product-card.js +3 -3
  435. package/dist/cdn/product-card/snice-product-card.js.map +1 -1
  436. package/dist/cdn/product-card/snice-product-card.min.js +3 -3
  437. package/dist/cdn/product-card/snice-product-card.min.js.map +1 -1
  438. package/dist/cdn/progress/README.md +1 -1
  439. package/dist/cdn/progress/snice-progress.js +2 -2
  440. package/dist/cdn/progress/snice-progress.js.map +1 -1
  441. package/dist/cdn/progress/snice-progress.min.js +2 -2
  442. package/dist/cdn/progress/snice-progress.min.js.map +1 -1
  443. package/dist/cdn/progress-ring/snice-progress-ring.js +2 -2
  444. package/dist/cdn/progress-ring/snice-progress-ring.js.map +1 -1
  445. package/dist/cdn/progress-ring/snice-progress-ring.min.js +2 -2
  446. package/dist/cdn/progress-ring/snice-progress-ring.min.js.map +1 -1
  447. package/dist/cdn/qr-code/snice-qr-code.js +1 -1
  448. package/dist/cdn/qr-code/snice-qr-code.min.js +1 -1
  449. package/dist/cdn/qr-reader/snice-qr-reader.js +1 -1
  450. package/dist/cdn/qr-reader/snice-qr-reader.min.js +1 -1
  451. package/dist/cdn/radio/snice-radio.js +1 -1
  452. package/dist/cdn/radio/snice-radio.min.js +1 -1
  453. package/dist/cdn/range-slider/snice-range-slider.js +1 -1
  454. package/dist/cdn/range-slider/snice-range-slider.min.js +1 -1
  455. package/dist/cdn/rating/snice-rating.js +1 -1
  456. package/dist/cdn/rating/snice-rating.min.js +1 -1
  457. package/dist/cdn/receipt/snice-receipt.js +1 -1
  458. package/dist/cdn/receipt/snice-receipt.min.js +1 -1
  459. package/dist/cdn/recipe/snice-recipe.js +1 -1
  460. package/dist/cdn/recipe/snice-recipe.min.js +1 -1
  461. package/dist/cdn/runtime/README.md +2 -2
  462. package/dist/cdn/runtime/snice-runtime.esm.js +644 -172
  463. package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
  464. package/dist/cdn/runtime/snice-runtime.esm.min.js +8 -8
  465. package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
  466. package/dist/cdn/runtime/snice-runtime.js +644 -172
  467. package/dist/cdn/runtime/snice-runtime.js.map +1 -1
  468. package/dist/cdn/runtime/snice-runtime.min.js +8 -8
  469. package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
  470. package/dist/cdn/sankey/snice-sankey.js +1 -1
  471. package/dist/cdn/sankey/snice-sankey.js.map +1 -1
  472. package/dist/cdn/sankey/snice-sankey.min.js +1 -1
  473. package/dist/cdn/sankey/snice-sankey.min.js.map +1 -1
  474. package/dist/cdn/segmented-control/snice-segmented-control.js +1 -1
  475. package/dist/cdn/segmented-control/snice-segmented-control.min.js +1 -1
  476. package/dist/cdn/select/snice-select.js +1 -1
  477. package/dist/cdn/select/snice-select.min.js +1 -1
  478. package/dist/cdn/skeleton/snice-skeleton.js +1 -1
  479. package/dist/cdn/skeleton/snice-skeleton.min.js +1 -1
  480. package/dist/cdn/slider/snice-slider.js +1 -1
  481. package/dist/cdn/slider/snice-slider.min.js +1 -1
  482. package/dist/cdn/sortable/snice-sortable.js +1 -1
  483. package/dist/cdn/sortable/snice-sortable.min.js +1 -1
  484. package/dist/cdn/sparkline/snice-sparkline.js +1 -1
  485. package/dist/cdn/sparkline/snice-sparkline.min.js +1 -1
  486. package/dist/cdn/spinner/snice-spinner.js +1 -1
  487. package/dist/cdn/spinner/snice-spinner.min.js +1 -1
  488. package/dist/cdn/split-button/snice-split-button.js +1 -1
  489. package/dist/cdn/split-button/snice-split-button.js.map +1 -1
  490. package/dist/cdn/split-button/snice-split-button.min.js +1 -1
  491. package/dist/cdn/split-button/snice-split-button.min.js.map +1 -1
  492. package/dist/cdn/split-pane/snice-split-pane.js +1 -1
  493. package/dist/cdn/split-pane/snice-split-pane.min.js +1 -1
  494. package/dist/cdn/spotlight/snice-spotlight.js +1 -1
  495. package/dist/cdn/spotlight/snice-spotlight.min.js +1 -1
  496. package/dist/cdn/stat-group/snice-stat-group.js +1 -1
  497. package/dist/cdn/stat-group/snice-stat-group.min.js +1 -1
  498. package/dist/cdn/step-input/snice-step-input.js +1 -1
  499. package/dist/cdn/step-input/snice-step-input.min.js +1 -1
  500. package/dist/cdn/stepper/snice-stepper.js +1 -1
  501. package/dist/cdn/stepper/snice-stepper.min.js +1 -1
  502. package/dist/cdn/switch/snice-switch.js +1 -1
  503. package/dist/cdn/switch/snice-switch.min.js +1 -1
  504. package/dist/cdn/table/README.md +1 -1
  505. package/dist/cdn/table/snice-table.js +14 -4
  506. package/dist/cdn/table/snice-table.js.map +1 -1
  507. package/dist/cdn/table/snice-table.min.js +14 -4
  508. package/dist/cdn/table/snice-table.min.js.map +1 -1
  509. package/dist/cdn/tabs/snice-tabs.js +1 -1
  510. package/dist/cdn/tabs/snice-tabs.min.js +1 -1
  511. package/dist/cdn/tag/snice-tag.js +1 -1
  512. package/dist/cdn/tag/snice-tag.min.js +1 -1
  513. package/dist/cdn/tag-input/snice-tag-input.js +2 -2
  514. package/dist/cdn/tag-input/snice-tag-input.js.map +1 -1
  515. package/dist/cdn/tag-input/snice-tag-input.min.js +2 -2
  516. package/dist/cdn/tag-input/snice-tag-input.min.js.map +1 -1
  517. package/dist/cdn/terminal/snice-terminal.js +1 -1
  518. package/dist/cdn/terminal/snice-terminal.min.js +1 -1
  519. package/dist/cdn/testimonial/snice-testimonial.js +1 -1
  520. package/dist/cdn/testimonial/snice-testimonial.min.js +1 -1
  521. package/dist/cdn/textarea/snice-textarea.js +1 -1
  522. package/dist/cdn/textarea/snice-textarea.min.js +1 -1
  523. package/dist/cdn/time-picker/README.md +1 -1
  524. package/dist/cdn/time-picker/snice-time-picker.js +18 -13
  525. package/dist/cdn/time-picker/snice-time-picker.js.map +1 -1
  526. package/dist/cdn/time-picker/snice-time-picker.min.js +3 -3
  527. package/dist/cdn/time-picker/snice-time-picker.min.js.map +1 -1
  528. package/dist/cdn/time-range-picker/snice-time-range-picker.js +1 -1
  529. package/dist/cdn/time-range-picker/snice-time-range-picker.min.js +1 -1
  530. package/dist/cdn/timeline/snice-timeline.js +1 -1
  531. package/dist/cdn/timeline/snice-timeline.js.map +1 -1
  532. package/dist/cdn/timeline/snice-timeline.min.js +1 -1
  533. package/dist/cdn/timeline/snice-timeline.min.js.map +1 -1
  534. package/dist/cdn/timer/snice-timer.js +1 -1
  535. package/dist/cdn/timer/snice-timer.min.js +1 -1
  536. package/dist/cdn/toast/snice-toast.js +1 -1
  537. package/dist/cdn/toast/snice-toast.js.map +1 -1
  538. package/dist/cdn/toast/snice-toast.min.js +1 -1
  539. package/dist/cdn/toast/snice-toast.min.js.map +1 -1
  540. package/dist/cdn/tooltip/snice-tooltip.js +1 -1
  541. package/dist/cdn/tooltip/snice-tooltip.min.js +1 -1
  542. package/dist/cdn/tree/snice-tree.js +1 -1
  543. package/dist/cdn/tree/snice-tree.min.js +1 -1
  544. package/dist/cdn/treemap/snice-treemap.js +1 -1
  545. package/dist/cdn/treemap/snice-treemap.js.map +1 -1
  546. package/dist/cdn/treemap/snice-treemap.min.js +1 -1
  547. package/dist/cdn/treemap/snice-treemap.min.js.map +1 -1
  548. package/dist/cdn/user-card/snice-user-card.js +1 -1
  549. package/dist/cdn/user-card/snice-user-card.min.js +1 -1
  550. package/dist/cdn/video-player/snice-video-player.js +1 -1
  551. package/dist/cdn/video-player/snice-video-player.min.js +1 -1
  552. package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +1 -1
  553. package/dist/cdn/virtual-scroller/snice-virtual-scroller.min.js +1 -1
  554. package/dist/cdn/waterfall/snice-waterfall.js +1 -1
  555. package/dist/cdn/waterfall/snice-waterfall.min.js +1 -1
  556. package/dist/cdn/weather/snice-weather.js +1 -1
  557. package/dist/cdn/weather/snice-weather.min.js +1 -1
  558. package/dist/cdn/work-order/snice-work-order.js +1 -1
  559. package/dist/cdn/work-order/snice-work-order.min.js +1 -1
  560. package/dist/components/action-bar/snice-action-bar.js +1 -1
  561. package/dist/components/action-bar/snice-action-bar.js.map +1 -1
  562. package/dist/components/banner/snice-banner.js +1 -1
  563. package/dist/components/banner/snice-banner.js.map +1 -1
  564. package/dist/components/chat/snice-chat-message.d.ts +32 -0
  565. package/dist/components/chat/snice-chat-message.js +127 -0
  566. package/dist/components/chat/snice-chat-message.js.map +1 -0
  567. package/dist/components/chat/snice-chat-message.types.d.ts +30 -0
  568. package/dist/components/chat/snice-chat.d.ts +31 -4
  569. package/dist/components/chat/snice-chat.js +363 -90
  570. package/dist/components/chat/snice-chat.js.map +1 -1
  571. package/dist/components/chat/snice-chat.types.d.ts +34 -3
  572. package/dist/components/command-palette/snice-command-palette.js +1 -1
  573. package/dist/components/command-palette/snice-command-palette.js.map +1 -1
  574. package/dist/components/icons/index.d.ts +4 -0
  575. package/dist/components/icons/index.js +5 -1
  576. package/dist/components/icons/index.js.map +1 -1
  577. package/dist/components/invoice/snice-invoice.js +1 -1
  578. package/dist/components/invoice/snice-invoice.js.map +1 -1
  579. package/dist/components/login/snice-login.d.ts +2 -2
  580. package/dist/components/login/snice-login.js +2 -0
  581. package/dist/components/login/snice-login.js.map +1 -1
  582. package/dist/components/login/snice-login.types.d.ts +4 -0
  583. package/dist/components/markdown/snice-markdown.d.ts +1 -0
  584. package/dist/components/markdown/snice-markdown.js +15 -2
  585. package/dist/components/markdown/snice-markdown.js.map +1 -1
  586. package/dist/components/menu/snice-menu.js +1 -1
  587. package/dist/components/menu/snice-menu.js.map +1 -1
  588. package/dist/components/message-strip/snice-message-strip.d.ts +1 -1
  589. package/dist/components/message-strip/snice-message-strip.js +8 -8
  590. package/dist/components/message-strip/snice-message-strip.types.d.ts +1 -1
  591. package/dist/components/product-card/snice-product-card.js +2 -2
  592. package/dist/components/product-card/snice-product-card.js.map +1 -1
  593. package/dist/components/progress/snice-progress.js +1 -1
  594. package/dist/components/progress/snice-progress.js.map +1 -1
  595. package/dist/components/progress-ring/snice-progress-ring.js +1 -1
  596. package/dist/components/progress-ring/snice-progress-ring.js.map +1 -1
  597. package/dist/components/table/snice-table.js +13 -3
  598. package/dist/components/table/snice-table.js.map +1 -1
  599. package/dist/components/tag-input/snice-tag-input.js +1 -1
  600. package/dist/components/tag-input/snice-tag-input.js.map +1 -1
  601. package/dist/components/time-picker/snice-time-picker.js +17 -12
  602. package/dist/components/time-picker/snice-time-picker.js.map +1 -1
  603. package/dist/components/utils.d.ts +6 -0
  604. package/dist/components/utils.js +1 -1
  605. package/dist/components/utils.js.map +1 -1
  606. package/dist/element.d.ts +2 -1
  607. package/dist/events.d.ts +6 -0
  608. package/dist/index.cjs +641 -169
  609. package/dist/index.cjs.map +1 -1
  610. package/dist/index.esm.js +641 -169
  611. package/dist/index.esm.js.map +1 -1
  612. package/dist/index.iife.js +641 -169
  613. package/dist/index.iife.js.map +1 -1
  614. package/dist/parts.d.ts +1 -8
  615. package/dist/react/SniceProvider.js +1 -1
  616. package/dist/react/SniceRouter.js +1 -1
  617. package/dist/react/index.js +1 -1
  618. package/dist/react/matchRoute.d.ts +1 -1
  619. package/dist/react/matchRoute.js +38 -4
  620. package/dist/react/matchRoute.js.map +1 -1
  621. package/dist/react/route-specificity.d.ts +1 -0
  622. package/dist/react/useRequestHandler.js +1 -1
  623. package/dist/render.d.ts +7 -0
  624. package/dist/route-specificity.d.ts +1 -0
  625. package/dist/symbols.cjs +14 -1
  626. package/dist/symbols.cjs.map +1 -1
  627. package/dist/symbols.d.ts +3 -0
  628. package/dist/symbols.esm.js +12 -2
  629. package/dist/symbols.esm.js.map +1 -1
  630. package/dist/transitions.cjs +1 -1
  631. package/dist/transitions.esm.js +1 -1
  632. package/dist/types/context.d.ts +5 -2
  633. package/dist/types/dispatch-options.d.ts +7 -0
  634. package/dist/types/index.d.ts +1 -0
  635. package/dist/types/on-options.d.ts +14 -0
  636. package/dist/types/watch-options.d.ts +9 -0
  637. package/dist/utils.d.ts +26 -0
  638. package/docs/ai/DEVELOPMENT.md +1 -1
  639. package/docs/ai/README.md +11 -0
  640. package/docs/ai/api.md +24 -5
  641. package/docs/ai/cdn.md +31 -0
  642. package/docs/ai/components/chat.md +57 -1
  643. package/docs/ai/components/message-strip.md +3 -3
  644. package/docs/ai/decorators.md +44 -5
  645. package/docs/cdn.md +56 -0
  646. package/docs/components/chat.md +108 -2
  647. package/docs/components/message-strip.md +8 -8
  648. package/docs/elements.md +21 -0
  649. package/docs/events.md +88 -0
  650. package/docs/superpowers/specs/2026-05-26-chat-styling-design.md +155 -0
  651. package/docs/superpowers/specs/2026-05-27-chat-theming-REMAINING.md +83 -0
  652. package/docs/superpowers/specs/2026-06-09-chat-custom-content-design.md +233 -0
  653. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v5.2.1
2
+ * snice v5.2.3
3
3
  * A decorator-driven web component library with differential rendering, routing, controllers, and 130+ ready-made UI components. Use as much or as little as you want. Zero dependencies, works anywhere.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -36,6 +36,12 @@ var Snice = (function (exports) {
36
36
  // Internal element state symbols
37
37
  const READY_PROMISE = getSymbol('ready-promise');
38
38
  const READY_RESOLVE = getSymbol('ready-resolve');
39
+ // Set when a render is requested but dropped because the element was
40
+ // disconnected; consumed on reconnect to replay exactly the missed render.
41
+ const PENDING_RECONNECT_RENDER = getSymbol('pending-reconnect-render');
42
+ // Per-element synchronous render-nesting depth, used to cap runaway re-entry
43
+ // without a shared global counter that one element could exhaust for others.
44
+ const RENDER_DEPTH = getSymbol('render-depth');
39
45
  const CONTROLLER = getSymbol('controller');
40
46
  const INITIALIZED = getSymbol('initialized');
41
47
  // Controller symbols
@@ -107,6 +113,10 @@ var Snice = (function (exports) {
107
113
  // Controller attach-abort — set to an AbortController while attachController
108
114
  // is awaiting element.ready, so detachController can short-circuit the wait.
109
115
  const CONTROLLER_ABORT = getSymbol('controller-abort');
116
+ // Set on a controller instance once it has gotten past element.ready and is
117
+ // being attached, so detachController knows not to run detach() on a controller
118
+ // whose attach() was aborted before it ever ran.
119
+ const CONTROLLER_ATTACHED = getSymbol('controller-attached');
110
120
  const PROPERTY_DEFINERS = getSymbol('property-definers');
111
121
 
112
122
  /**
@@ -227,11 +237,23 @@ var Snice = (function (exports) {
227
237
  this[REGISTERED_ELEMENTS_SET].add(element);
228
238
  }
229
239
  /**
230
- * Unregister an element from receiving context updates
240
+ * Unregister an element from receiving context updates. With a methodName,
241
+ * removes only that one handler (so a `once` handler tearing itself down
242
+ * doesn't kill the element's other @context handlers); without one, removes
243
+ * the element entirely (used on disconnect).
231
244
  * @internal Used by @context decorator cleanup
232
245
  */
233
- [CONTEXT_UNREGISTER](element) {
234
- this[REGISTERED_ELEMENTS].delete(element);
246
+ [CONTEXT_UNREGISTER](element, methodName) {
247
+ const map = this[REGISTERED_ELEMENTS];
248
+ if (methodName !== undefined) {
249
+ const names = map.get(element);
250
+ if (!names)
251
+ return;
252
+ names.delete(methodName);
253
+ if (names.size > 0)
254
+ return; // other handlers remain — keep the element
255
+ }
256
+ map.delete(element);
235
257
  this[REGISTERED_ELEMENTS_SET].delete(element);
236
258
  }
237
259
  /**
@@ -396,11 +418,23 @@ var Snice = (function (exports) {
396
418
  * @param initialValue - The default value assigned to the property field (used for type detection)
397
419
  * @returns The string representation for the HTML attribute, or null if value should remove attribute
398
420
  */
421
+ /**
422
+ * Default property dirty-check: has the value changed? Like `!==`, but treats
423
+ * NaN → NaN as unchanged (NaN !== NaN would otherwise re-fire forever).
424
+ */
425
+ function notEqual(value, old) {
426
+ // Changed unless strictly equal, or both are NaN (each self-unequal).
427
+ return value !== old && (value === value || old === old);
428
+ }
399
429
  /**
400
430
  * Extract attribute name from property options
401
431
  */
402
432
  function getAttrName(opts, propName) {
403
- return typeof opts.attribute === 'string' ? opts.attribute : propName.toLowerCase();
433
+ // Lowercase always: the DOM stores attribute names lowercase, so an explicit
434
+ // camelCase `attribute:` would otherwise never match observedAttributes and
435
+ // attributeChangedCallback would never fire. get/setAttribute are already
436
+ // case-insensitive, so this is safe for every caller.
437
+ return (typeof opts.attribute === 'string' ? opts.attribute : propName).toLowerCase();
404
438
  }
405
439
  /**
406
440
  * Escape HTML text-node content. Use for text that will be interpolated
@@ -547,6 +581,42 @@ var Snice = (function (exports) {
547
581
  }
548
582
  }
549
583
  }
584
+ /**
585
+ * Fire the one-time initialization call for `@watch(..., { immediate: true })`
586
+ * handlers. Runs once per declared property after the element is initialized,
587
+ * passing the effective initial value with `oldValue` undefined. Non-immediate
588
+ * watchers are untouched — they only react to later changes.
589
+ */
590
+ function invokeImmediateWatchers(instance, constructor) {
591
+ const watchers = constructor[PROPERTY_WATCHERS];
592
+ const properties = constructor[PROPERTIES];
593
+ if (!watchers || !properties)
594
+ return;
595
+ for (const [propName, propOptions] of properties) {
596
+ // A watcher may be registered under the JS name, the explicit `attribute:`
597
+ // name, or the `*` wildcard; fire each immediate one exactly once, keyed to
598
+ // this property's current value.
599
+ const keys = [propName];
600
+ const attrName = propOptions && typeof propOptions.attribute === 'string' ? propOptions.attribute : null;
601
+ if (attrName && attrName !== propName)
602
+ keys.push(attrName);
603
+ keys.push('*');
604
+ for (const key of keys) {
605
+ if (!watchers.has(key))
606
+ continue;
607
+ for (const w of watchers.get(key)) {
608
+ if (!w.immediate)
609
+ continue;
610
+ try {
611
+ w.method.call(instance, undefined, instance[propName], propName);
612
+ }
613
+ catch (e) {
614
+ console.error(`Error in @watch('${key}') method ${w.methodName}:`, e);
615
+ }
616
+ }
617
+ }
618
+ }
619
+ }
550
620
  /**
551
621
  * Create a debounced version of a function
552
622
  */
@@ -580,6 +650,41 @@ var Snice = (function (exports) {
580
650
  }
581
651
  };
582
652
  }
653
+ /**
654
+ * Resolve a scope option to a concrete EventTarget.
655
+ *
656
+ * - undefined → host element (default behavior)
657
+ * - 'global' → `document`
658
+ * - string → `host.closest(selector)` (nearest matching ancestor)
659
+ * - EventTarget instance → that target directly
660
+ * - resolver function → called with host as `this`, returns target or null
661
+ *
662
+ * Returns null when the scope cannot be resolved (e.g. selector matches no
663
+ * ancestor, resolver returns null). Caller decides how to handle null —
664
+ * typically warn in dev and skip listener attachment.
665
+ */
666
+ function resolveScope(host, scope) {
667
+ if (scope === undefined || scope === null)
668
+ return host;
669
+ if (scope === 'global')
670
+ return typeof document !== 'undefined' ? document : null;
671
+ if (typeof scope === 'function') {
672
+ try {
673
+ return scope.call(host) || null;
674
+ }
675
+ catch {
676
+ return null;
677
+ }
678
+ }
679
+ if (typeof scope === 'string') {
680
+ return host.closest?.(scope) || null;
681
+ }
682
+ // Direct EventTarget instance.
683
+ if (scope && typeof scope.addEventListener === 'function') {
684
+ return scope;
685
+ }
686
+ return null;
687
+ }
583
688
  function valueToAttribute(value, propertyOptions, initialValue) {
584
689
  // Handle null/undefined/false values and empty arrays
585
690
  if (value === null || value === undefined || value === false ||
@@ -997,13 +1102,11 @@ var Snice = (function (exports) {
997
1102
  return finalValue;
998
1103
  }
999
1104
  catch (error) {
1000
- // Send error to generator
1001
- try {
1002
- await generator.throw(error);
1003
- }
1004
- catch (generatorError) {
1005
- throw generatorError;
1006
- }
1105
+ // Drive the generator's own catch block and return whatever it recovers
1106
+ // with (e.g. a cached fallback). If the generator re-throws instead,
1107
+ // generator.throw rejects and the error propagates to the caller.
1108
+ const { value: recovered } = await generator.throw(error);
1109
+ return recovered;
1007
1110
  }
1008
1111
  }; // Close actualRequest function
1009
1112
  // Debounce
@@ -1027,9 +1130,21 @@ var Snice = (function (exports) {
1027
1130
  const now = Date.now();
1028
1131
  const remaining = options.throttle - (now - throttleLastCall);
1029
1132
  if (remaining <= 0) {
1133
+ // Cancelling the scheduled trailing call must not orphan the promises
1134
+ // queued for it — settle them with this fresh result. Also reset
1135
+ // throttleTimeout so later within-window calls don't attach to a dead
1136
+ // timer and hang.
1030
1137
  clearTimeout(throttleTimeout);
1138
+ throttleTimeout = null;
1031
1139
  throttleLastCall = now;
1032
- return actualRequest();
1140
+ const orphaned = throttleTrailingResolvers.splice(0);
1141
+ const p = actualRequest();
1142
+ if (orphaned.length) {
1143
+ p.then((r) => { for (const x of orphaned)
1144
+ x.resolve(r); }, (e) => { for (const x of orphaned)
1145
+ x.reject(e); });
1146
+ }
1147
+ return p;
1033
1148
  }
1034
1149
  // Throttle: already has pending trailing call — attach to it
1035
1150
  if (throttleTimeout) {
@@ -1130,10 +1245,20 @@ var Snice = (function (exports) {
1130
1245
  const now = Date.now();
1131
1246
  const remaining = handler.options.throttle - (now - throttleLastCall);
1132
1247
  if (remaining <= 0) {
1248
+ // Same as the @request side: settle any queued trailing callers with
1249
+ // this fresh result rather than orphaning their promises.
1133
1250
  clearTimeout(throttleTimeout);
1134
1251
  throttleTimeout = null;
1135
1252
  throttleLastCall = now;
1136
- return originalMethod(...args);
1253
+ const orphaned = throttleTrailingResolvers.splice(0);
1254
+ throttleLatestArgs = null;
1255
+ const result = originalMethod(...args);
1256
+ if (orphaned.length) {
1257
+ Promise.resolve(result).then((r) => { for (const x of orphaned)
1258
+ x.resolve(r); }, (e) => { for (const x of orphaned)
1259
+ x.reject(e); });
1260
+ }
1261
+ return result;
1137
1262
  }
1138
1263
  // Remember the LATEST args so the trailing call uses fresh input,
1139
1264
  // and queue every suppressed caller's resolver so they all receive
@@ -1240,7 +1365,10 @@ var Snice = (function (exports) {
1240
1365
  // Combine strings and values into final CSS text
1241
1366
  let cssText = strings[0];
1242
1367
  for (let i = 0; i < values.length; i++) {
1243
- cssText += String(values[i]) + strings[i + 1];
1368
+ // Inline a nested css`` result by its text so style fragments compose;
1369
+ // String() on a CSSResult object would yield "[object Object]".
1370
+ const value = values[i];
1371
+ cssText += (isCSSResult(value) ? value.cssText : String(value)) + strings[i + 1];
1244
1372
  }
1245
1373
  const result = {
1246
1374
  _$litType$: CSS_RESULT,
@@ -1391,6 +1519,20 @@ var Snice = (function (exports) {
1391
1519
  const attrStrings = value.split(marker);
1392
1520
  // Number of expressions = number of markers = attrStrings.length - 1
1393
1521
  const expressionCount = attrStrings.length - 1;
1522
+ // Property/boolean/event bindings take a SINGLE expression — they
1523
+ // can't concatenate static text (a `.prop` is an object, not a
1524
+ // string). They consume the first expression; any static text or
1525
+ // extra expressions are ignored. But the value index must still
1526
+ // advance by the true marker count, or every later binding shifts.
1527
+ const isSingleExpressionBinding = /^@@?|^[.?]/.test(originalName);
1528
+ if (isSingleExpressionBinding) {
1529
+ const hasStaticText = attrStrings.some((s) => s !== '');
1530
+ if (expressionCount > 1 || hasStaticText) {
1531
+ console.warn(`snice: binding "${originalName}" takes a single expression; ` +
1532
+ `static text and extra interpolations are ignored. ` +
1533
+ `Use a plain attribute for string interpolation.`);
1534
+ }
1535
+ }
1394
1536
  if (originalName.startsWith('@@')) {
1395
1537
  // Escaped event binding for events with @ in the name (e.g., @@snice/event -> @snice/event)
1396
1538
  this.parts.push({
@@ -1399,7 +1541,7 @@ var Snice = (function (exports) {
1399
1541
  name: originalName.slice(1), // Keep the @ in the event name
1400
1542
  element
1401
1543
  });
1402
- partIndex += 1;
1544
+ partIndex += expressionCount;
1403
1545
  }
1404
1546
  else if (originalName.startsWith('@')) {
1405
1547
  // Event binding (single value only)
@@ -1409,7 +1551,7 @@ var Snice = (function (exports) {
1409
1551
  name: originalName.slice(1),
1410
1552
  element
1411
1553
  });
1412
- partIndex += 1;
1554
+ partIndex += expressionCount;
1413
1555
  }
1414
1556
  else if (originalName.startsWith('.')) {
1415
1557
  // Property binding (single value only)
@@ -1419,7 +1561,7 @@ var Snice = (function (exports) {
1419
1561
  name: originalName.slice(1),
1420
1562
  element
1421
1563
  });
1422
- partIndex += 1;
1564
+ partIndex += expressionCount;
1423
1565
  }
1424
1566
  else if (originalName.startsWith('?')) {
1425
1567
  // Boolean attribute (single value only)
@@ -1429,7 +1571,7 @@ var Snice = (function (exports) {
1429
1571
  name: originalName.slice(1),
1430
1572
  element
1431
1573
  });
1432
- partIndex += 1;
1574
+ partIndex += expressionCount;
1433
1575
  }
1434
1576
  else {
1435
1577
  // Regular attribute - supports multiple interpolations
@@ -1904,7 +2046,13 @@ var Snice = (function (exports) {
1904
2046
  * Commit unsafe HTML string
1905
2047
  */
1906
2048
  _commitUnsafeHTML(value) {
1907
- // Always recreate for unsafe HTML (can't diff arbitrary HTML)
2049
+ // Can't diff arbitrary HTML, but when the STRING is unchanged the DOM is
2050
+ // already correct — skip the clear+reparse so live state (typed input
2051
+ // values, focus, scroll) inside the block survives unrelated re-renders.
2052
+ const prev = this._committedValue;
2053
+ if (isUnsafeHTML(prev) && prev.html === value.html) {
2054
+ return;
2055
+ }
1908
2056
  this._clear();
1909
2057
  const temp = document.createElement('template');
1910
2058
  temp.innerHTML = value.html;
@@ -2089,7 +2237,9 @@ var Snice = (function (exports) {
2089
2237
  // Supports both dot notation (@keydown.enter) and colon notation (@keydown:Enter) to match @on decorator
2090
2238
  // Only parse colons for keyboard events, not custom events
2091
2239
  const isKeyboardEvent = ['keydown', 'keyup', 'keypress'].includes(eventName.split('.')[0].split(':')[0]);
2092
- const dotIndex = eventName.indexOf('.');
2240
+ // Only keyboard events split on `.`/`:` into a key filter — a custom event
2241
+ // name may legitimately contain a dot (e.g. `app.ready`) and must be kept whole.
2242
+ const dotIndex = isKeyboardEvent ? eventName.indexOf('.') : -1;
2093
2243
  const colonIndex = isKeyboardEvent ? eventName.indexOf(':') : -1;
2094
2244
  // Use whichever delimiter comes first (dot or colon)
2095
2245
  const delimiterIndex = dotIndex > 0 && colonIndex > 0
@@ -2103,6 +2253,7 @@ var Snice = (function (exports) {
2103
2253
  }
2104
2254
  else {
2105
2255
  this.eventName = eventName;
2256
+ warnIfModifierMisuse(eventName);
2106
2257
  }
2107
2258
  }
2108
2259
  commit(value) {
@@ -2128,17 +2279,19 @@ var Snice = (function (exports) {
2128
2279
  }
2129
2280
  if (typeof value !== 'function')
2130
2281
  return;
2131
- // Auto-bind to host element (the custom element with shadow root)
2132
- if (!this.host) {
2133
- const rootNode = this.element.getRootNode();
2134
- this.host = rootNode.host || null;
2135
- }
2136
- const context = this.host || null;
2137
2282
  const keyFilter = this.keyFilter;
2138
2283
  this.listener = ((event) => {
2139
2284
  if (keyFilter && !matchesKeyboardFilter(event, keyFilter))
2140
2285
  return;
2141
- value.call(context, event);
2286
+ // Resolve the host (the custom element owning the shadow root) at dispatch
2287
+ // time, not bind time: a binding inside an initially-hidden <if>/<case>
2288
+ // branch commits while off-DOM, where getRootNode() has no host — caching
2289
+ // that null permanently would call the handler with `this = null` once the
2290
+ // branch is shown. By the time an event fires, the element is in the DOM.
2291
+ if (!this.host) {
2292
+ this.host = this.element.getRootNode().host || null;
2293
+ }
2294
+ value.call(this.host || null, event);
2142
2295
  });
2143
2296
  this.element.addEventListener(this.eventName, this.listener);
2144
2297
  }
@@ -2157,6 +2310,25 @@ var Snice = (function (exports) {
2157
2310
  * "ctrl+shift+s" -> { key: "s", ctrl: true, shift: true }
2158
2311
  * "~enter" -> { key: "Enter", anyModifiers: true }
2159
2312
  */
2313
+ // once/preventDefault/stopPropagation/capture/passive are @on() OPTIONS, not
2314
+ // template modifiers. A dotted non-keyboard event whose suffix is one of these
2315
+ // is almost certainly a mistake (e.g. `@click.once`) — it binds a listener for
2316
+ // an event type the DOM never dispatches. Warn instead of failing silently. A
2317
+ // legitimate custom event name like `app.ready` won't match and stays quiet.
2318
+ const MODIFIER_WORDS = new Set([
2319
+ 'once', 'prevent', 'preventdefault', 'stop', 'stoppropagation', 'capture', 'passive',
2320
+ ]);
2321
+ function warnIfModifierMisuse(eventName) {
2322
+ const dot = eventName.lastIndexOf('.');
2323
+ if (dot < 0)
2324
+ return;
2325
+ const suffix = eventName.slice(dot + 1).toLowerCase();
2326
+ if (!MODIFIER_WORDS.has(suffix))
2327
+ return;
2328
+ console.warn(`snice: "@${eventName}" is not a valid modifier — once/preventDefault/` +
2329
+ `stopPropagation/capture are @on() options, not template modifiers. ` +
2330
+ `This registers a listener for event "${eventName}", which never fires.`);
2331
+ }
2160
2332
  function parseKeyboardFilter(spec) {
2161
2333
  // Handle ~ prefix for matching regardless of modifiers
2162
2334
  const anyModifiers = spec.startsWith('~');
@@ -2259,9 +2431,14 @@ var Snice = (function (exports) {
2259
2431
  // Insert markers where <if> is
2260
2432
  parent.insertBefore(this.startNode, ifElement);
2261
2433
  parent.insertBefore(this.endNode, ifElement.nextSibling);
2262
- // Move <if>'s children between the markers
2434
+ // Park <if>'s children in an off-DOM fragment immediately, rather than
2435
+ // leaving them in the live template fragment. Otherwise a branch that is
2436
+ // hidden on first render would still connect its children (when the
2437
+ // template mounts) and then disconnect them (on the first commit) — firing
2438
+ // mount side effects spuriously. commit(true) inserts them when shown.
2439
+ this.childFragment = document.createDocumentFragment();
2263
2440
  while (ifElement.firstChild) {
2264
- parent.insertBefore(ifElement.firstChild, this.endNode);
2441
+ this.childFragment.appendChild(ifElement.firstChild);
2265
2442
  }
2266
2443
  // Remove the <if> element from DOM
2267
2444
  parent.removeChild(ifElement);
@@ -2342,16 +2519,24 @@ var Snice = (function (exports) {
2342
2519
  parent.removeChild(caseElement);
2343
2520
  }
2344
2521
  commit(value) {
2345
- if (this.value === value)
2522
+ // Dirty-check on the SELECTED BRANCH, not the raw value. A value whose type
2523
+ // changed but stringifies the same (number 1 vs string '1' — e.g. a numeric
2524
+ // default replaced by a router param, which is always a string) selects the
2525
+ // same branch, so tearing it down and re-inserting it would needlessly
2526
+ // restart media/animations and lose focus inside it.
2527
+ const valueStr = String(value);
2528
+ const hasBranch = this.branches.has(valueStr);
2529
+ const nextKey = hasBranch ? valueStr : (this.defaultBranch ? '__default__' : null);
2530
+ if (this.currentKey === nextKey) {
2531
+ this.value = value;
2346
2532
  return;
2533
+ }
2347
2534
  this.value = value;
2348
- const valueStr = String(value);
2349
2535
  // Move current content back to its fragment
2350
2536
  this._collectCurrent();
2351
2537
  // Insert matching branch
2352
- const hasBranch = this.branches.has(valueStr);
2353
2538
  const matchingFragment = hasBranch ? this.branches.get(valueStr) : this.defaultBranch;
2354
- this.currentKey = hasBranch ? valueStr : (this.defaultBranch ? '__default__' : null);
2539
+ this.currentKey = nextKey;
2355
2540
  if (matchingFragment && matchingFragment.hasChildNodes()) {
2356
2541
  this.startNode.parentNode.insertBefore(matchingFragment, this.endNode);
2357
2542
  }
@@ -2529,7 +2714,9 @@ var Snice = (function (exports) {
2529
2714
  // Supports both dot notation (@keydown.enter) and colon notation (@keydown:Enter)
2530
2715
  // Only parse colons for keyboard events, not custom events
2531
2716
  const isKeyboardEvent = ['keydown', 'keyup', 'keypress'].includes(handler.eventName.split('.')[0].split(':')[0]);
2532
- const dotIndex = handler.eventName.indexOf('.');
2717
+ // Only keyboard events split on `.`/`:` into a key filter — a custom event
2718
+ // name may legitimately contain a dot (e.g. `app.ready`) and must be kept whole.
2719
+ const dotIndex = isKeyboardEvent ? handler.eventName.indexOf('.') : -1;
2533
2720
  const colonIndex = isKeyboardEvent ? handler.eventName.indexOf(':') : -1;
2534
2721
  const delimiterIndex = dotIndex > 0 && colonIndex > 0
2535
2722
  ? Math.min(dotIndex, colonIndex)
@@ -2540,6 +2727,8 @@ var Snice = (function (exports) {
2540
2727
  const keyModifier = delimiterIndex > 0
2541
2728
  ? handler.eventName.substring(delimiterIndex + 1)
2542
2729
  : null;
2730
+ if (delimiterIndex <= 0)
2731
+ warnIfModifierMisuse(handler.eventName);
2543
2732
  // Apply debounce (takes precedence over throttle)
2544
2733
  if (handlerOptions.debounce && handlerOptions.debounce > 0) {
2545
2734
  boundMethod = createDebounced(boundMethod, handlerOptions.debounce);
@@ -2566,10 +2755,27 @@ var Snice = (function (exports) {
2566
2755
  };
2567
2756
  // Apply key modifier wrapper
2568
2757
  const keyModifierMethod = createKeyModifierHandler(boundMethod);
2758
+ // Resolve scope: explicit `scope` option redirects the listener target
2759
+ // to document / ancestor / arbitrary EventTarget. Default = host element.
2760
+ const hasExplicitScope = handlerOptions.scope !== undefined;
2761
+ let scopedTarget = null;
2762
+ if (hasExplicitScope) {
2763
+ scopedTarget = resolveScope(targetElement, handlerOptions.scope);
2764
+ if (!scopedTarget) {
2765
+ // Dev warning — listener silently dropped when scope cannot resolve.
2766
+ // Skip attachment so we don't bind to the wrong target.
2767
+ console.warn(`[snice/@on] scope did not resolve for "${handler.eventName}" on ${handler.methodName} — listener skipped.`);
2768
+ continue;
2769
+ }
2770
+ }
2569
2771
  // Main event handler with error handling and event delegation
2570
2772
  if (handler.selector) {
2571
- // Delegated event handling - use shadow root if available
2572
- const eventRoot = targetElement.shadowRoot || targetElement;
2773
+ // Delegated event handling.
2774
+ // - Default eventRoot: shadow root if present, else host element.
2775
+ // - With explicit scope: attach to the scoped target instead.
2776
+ const eventRoot = hasExplicitScope
2777
+ ? scopedTarget
2778
+ : (targetElement.shadowRoot || targetElement);
2573
2779
  const delegatedHandler = (event) => {
2574
2780
  const target = event.target;
2575
2781
  const matchingElement = (target.matches && target.matches(handler.selector) && target) ||
@@ -2609,10 +2815,11 @@ var Snice = (function (exports) {
2609
2815
  });
2610
2816
  }
2611
2817
  else {
2612
- // Direct event handling - on the element itself
2613
- // If element has shadow root, listen on both shadow root AND host element
2614
- // to catch events from inside shadow DOM (with correct target) and on host itself.
2615
- const shadowRoot = targetElement.shadowRoot;
2818
+ // Direct event handling.
2819
+ // - Default: shadow root + host element (so events inside shadow and on
2820
+ // the host itself both fire). A per-handler Symbol dedupes.
2821
+ // - With explicit scope: attach to the scoped target ONLY (no duplication).
2822
+ const shadowRoot = hasExplicitScope ? null : targetElement.shadowRoot;
2616
2823
  // Per-handler private Symbol so dedup is scoped to THIS handler's two
2617
2824
  // listeners (shadowRoot + host). Using Symbol.for() with the method name
2618
2825
  // would collide across components that share a method name (e.g. a parent
@@ -2640,6 +2847,16 @@ var Snice = (function (exports) {
2640
2847
  once: handlerOptions.once || false,
2641
2848
  passive: handlerOptions.passive || false,
2642
2849
  };
2850
+ if (hasExplicitScope) {
2851
+ scopedTarget.addEventListener(baseEventName, wrappedMethod, listenerOptions);
2852
+ instance[CLEANUP].events.push({
2853
+ target: scopedTarget,
2854
+ eventName: baseEventName,
2855
+ handler: wrappedMethod,
2856
+ options: listenerOptions,
2857
+ });
2858
+ continue;
2859
+ }
2643
2860
  if (shadowRoot) {
2644
2861
  // Listen on shadow root for events inside shadow DOM
2645
2862
  shadowRoot.addEventListener(baseEventName, wrappedMethod, listenerOptions);
@@ -2802,11 +3019,25 @@ var Snice = (function (exports) {
2802
3019
  }),
2803
3020
  ]);
2804
3021
  }
3022
+ catch (error) {
3023
+ // Attach never got past `ready` (aborted or deadline). Roll back the stored
3024
+ // references so nothing is left pointing at a controller that never
3025
+ // attached — otherwise a later attach short-circuits as "already attached"
3026
+ // and a detach would run detach() on it.
3027
+ if (element[CONTROLLER_KEY] === controllerInstance) {
3028
+ delete element[CONTROLLER_KEY];
3029
+ delete element[CONTROLLER_NAME_KEY];
3030
+ delete element[CONTROLLER_OPERATIONS];
3031
+ }
3032
+ throw error;
3033
+ }
2805
3034
  finally {
2806
3035
  if (element[CONTROLLER_ABORT] === abort) {
2807
3036
  delete element[CONTROLLER_ABORT];
2808
3037
  }
2809
3038
  }
3039
+ // Past `ready` — mark the controller so detach knows attach was actually run.
3040
+ controllerInstance[CONTROLLER_ATTACHED] = true;
2810
3041
  // Run attach in the controller's scope
2811
3042
  await scope.runOperation(async () => {
2812
3043
  await controllerInstance.attach(element);
@@ -2837,6 +3068,19 @@ var Snice = (function (exports) {
2837
3068
  if (!controllerInstance) {
2838
3069
  return;
2839
3070
  }
3071
+ // Claim the controller immediately — before any await — so a second detach
3072
+ // that overlaps this one (e.g. disconnect's fire-and-forget detach racing a
3073
+ // controller reassignment) finds no instance and returns, instead of running
3074
+ // detach() a second time on the same controller. The rest of this function
3075
+ // works off the locals captured above.
3076
+ delete element[CONTROLLER_KEY];
3077
+ delete element[CONTROLLER_NAME_KEY];
3078
+ delete element[CONTROLLER_OPERATIONS];
3079
+ // A controller whose attach() was aborted before it ever ran must not have
3080
+ // detach() (or the teardown/event) run on it.
3081
+ if (!controllerInstance[CONTROLLER_ATTACHED]) {
3082
+ return;
3083
+ }
2840
3084
  // Run detach in the controller's scope
2841
3085
  if (scope) {
2842
3086
  await scope.runOperation(async () => {
@@ -2859,9 +3103,6 @@ var Snice = (function (exports) {
2859
3103
  }
2860
3104
  // Clean up router context reference
2861
3105
  delete controllerInstance[ROUTER_CONTEXT];
2862
- delete element[CONTROLLER_KEY];
2863
- delete element[CONTROLLER_NAME_KEY];
2864
- delete element[CONTROLLER_OPERATIONS];
2865
3106
  element.dispatchEvent(new CustomEvent('controller-detached', {
2866
3107
  detail: { name: controllerName, controller: controllerInstance }
2867
3108
  }));
@@ -3024,19 +3265,23 @@ var Snice = (function (exports) {
3024
3265
  const wrappedMethodName = `__wrapped_${methodName}`;
3025
3266
  // Create wrapped method with timing controls
3026
3267
  element[wrappedMethodName] = function (context) {
3027
- // Skip if already called once
3028
- if (options.once && element[CONTEXT_CALLED]) {
3268
+ // `once` is tracked PER method (not a single element-level flag) so
3269
+ // multiple once handlers on the same element each fire.
3270
+ const called = element[CONTEXT_CALLED] ||
3271
+ (element[CONTEXT_CALLED] = new Set());
3272
+ if (options.once && called.has(methodName)) {
3029
3273
  return;
3030
3274
  }
3031
3275
  const callMethod = () => {
3032
3276
  method.call(element, context);
3033
3277
  // Handle once option
3034
3278
  if (options.once) {
3035
- element[CONTEXT_CALLED] = true;
3036
- // Unregister after first call
3279
+ called.add(methodName);
3280
+ // Unregister ONLY this handler, not the whole element — otherwise the
3281
+ // element's other @context handlers would stop receiving updates.
3037
3282
  const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
3038
3283
  if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
3039
- ctx[CONTEXT_UNREGISTER](element);
3284
+ ctx[CONTEXT_UNREGISTER](element, wrappedMethodName);
3040
3285
  }
3041
3286
  }
3042
3287
  };
@@ -3139,9 +3384,14 @@ var Snice = (function (exports) {
3139
3384
  this.pending.clear();
3140
3385
  this.scheduled = false;
3141
3386
  for (const element of elements) {
3142
- // Skip elements that were disconnected between scheduling and flush.
3143
- if (!element.isConnected)
3387
+ // Skip elements that were disconnected between scheduling and flush, but
3388
+ // remember the dropped render so reconnect can replay it — otherwise the
3389
+ // shadow DOM stays stale for a property changed while detached. The flag
3390
+ // lives on the element, so it's GC'd with it (no leak if never reattached).
3391
+ if (!element.isConnected) {
3392
+ element[PENDING_RECONNECT_RENDER] = true;
3144
3393
  continue;
3394
+ }
3145
3395
  const options = element[RENDER_OPTIONS] || {};
3146
3396
  performRender(element, options);
3147
3397
  }
@@ -3160,6 +3410,19 @@ var Snice = (function (exports) {
3160
3410
  element[RENDER_CALLBACKS] = [];
3161
3411
  cbs.forEach(cb => cb());
3162
3412
  }
3413
+ /**
3414
+ * Backstop against a render() that mutates an observed property, which makes
3415
+ * the property setter request another render from inside the current one. In
3416
+ * `sync` mode that re-enters performRender synchronously and, unchecked,
3417
+ * recurses to a stack overflow. The counter caps synchronous nesting and turns
3418
+ * a silent crash into an actionable error. Normal renders never nest (child
3419
+ * renders defer to a microtask), so this stays at depth 1 in the common case.
3420
+ *
3421
+ * The depth is tracked PER ELEMENT (on RENDER_DEPTH), not globally: a runaway
3422
+ * component must not consume a shared budget and reject an unrelated component
3423
+ * that happens to render inside its call stack.
3424
+ */
3425
+ const MAX_RENDER_DEPTH = 50;
3163
3426
  /**
3164
3427
  * Perform the actual render of an element
3165
3428
  */
@@ -3171,6 +3434,14 @@ var Snice = (function (exports) {
3171
3434
  if (options.once && element[RENDER_INSTANCE]) {
3172
3435
  return;
3173
3436
  }
3437
+ const depth = (element[RENDER_DEPTH] ?? 0);
3438
+ if (depth >= MAX_RENDER_DEPTH) {
3439
+ const tag = element.tagName ? element.tagName.toLowerCase() : 'element';
3440
+ console.error(`snice: maximum render depth (${MAX_RENDER_DEPTH}) exceeded for <${tag}>. ` +
3441
+ `render() is mutating an observed property, causing an infinite render loop.`);
3442
+ return;
3443
+ }
3444
+ element[RENDER_DEPTH] = depth + 1;
3174
3445
  try {
3175
3446
  const result = precomputedResult !== undefined ? precomputedResult : renderMethod.call(element);
3176
3447
  if (!element.shadowRoot)
@@ -3196,14 +3467,18 @@ var Snice = (function (exports) {
3196
3467
  flushRenderCallbacks(element);
3197
3468
  return;
3198
3469
  }
3199
- // Different template or first render. Remove only non-<style> children so
3200
- // the fallback @styles path (Safari <=16 / jsdom / SSR — no
3201
- // adoptedStyleSheets) doesn't lose its style tags on template switch.
3470
+ // Different template or first render. Keep only the framework's own
3471
+ // fallback <style> tags (marked data-snice-style) so the @styles path
3472
+ // (Safari <=16 / jsdom / SSR no adoptedStyleSheets) survives a template
3473
+ // switch; template-emitted <style> tags are removed and re-created by the
3474
+ // new instance, so they don't accumulate.
3202
3475
  if (instance) {
3203
3476
  const root = element.shadowRoot;
3204
3477
  const toRemove = [];
3205
3478
  for (const child of Array.from(root.childNodes)) {
3206
- if (child.nodeType === 1 && child.tagName === 'STYLE')
3479
+ if (child.nodeType === 1 &&
3480
+ child.tagName === 'STYLE' &&
3481
+ child.hasAttribute('data-snice-style'))
3207
3482
  continue;
3208
3483
  toRemove.push(child);
3209
3484
  }
@@ -3219,6 +3494,9 @@ var Snice = (function (exports) {
3219
3494
  catch (error) {
3220
3495
  console.error('Error rendering element:', error);
3221
3496
  }
3497
+ finally {
3498
+ element[RENDER_DEPTH] = depth;
3499
+ }
3222
3500
  }
3223
3501
  /**
3224
3502
  * Request a render for an element
@@ -3272,6 +3550,33 @@ var Snice = (function (exports) {
3272
3550
  // Normal rendering (with microtask batching unless sync)
3273
3551
  renderScheduler.schedule(element, options);
3274
3552
  }
3553
+ /**
3554
+ * Clear any pending debounce/throttle render timers on an element (e.g. on
3555
+ * disconnect, so they don't fire on a dead element and retain it until they
3556
+ * expire). Returns true if a render was actually pending, so the caller can
3557
+ * arrange a replay on reconnect via PENDING_RECONNECT_RENDER.
3558
+ */
3559
+ function clearRenderTimers(element) {
3560
+ const timers = element[RENDER_TIMERS];
3561
+ if (!timers)
3562
+ return false;
3563
+ let hadPending = false;
3564
+ if (timers.debounce) {
3565
+ clearTimeout(timers.debounce);
3566
+ timers.debounce = null;
3567
+ hadPending = true;
3568
+ }
3569
+ if (timers.throttleTimer) {
3570
+ clearTimeout(timers.throttleTimer);
3571
+ timers.throttleTimer = null;
3572
+ hadPending = true;
3573
+ }
3574
+ // Deliberately do NOT reset timers.lastThrottle: a plain DOM move is a
3575
+ // disconnect+reconnect, and zeroing the cooldown would let the next render
3576
+ // fire inside the throttle window. Preserving it keeps throttle honest
3577
+ // across moves; on a true disconnect the element is discarded anyway.
3578
+ return hadPending;
3579
+ }
3275
3580
  /**
3276
3581
  * @render decorator for component rendering
3277
3582
  *
@@ -3394,9 +3699,12 @@ var Snice = (function (exports) {
3394
3699
  element.shadowRoot.adoptedStyleSheets = allResults.map(r => r.styleSheet);
3395
3700
  return;
3396
3701
  }
3397
- // Fallback — one <style> tag per stylesheet, preserving cascade order
3702
+ // Fallback — one <style> tag per stylesheet, preserving cascade order.
3703
+ // Marked so the template-switch cleanup keeps these (framework styles) but
3704
+ // still removes template-emitted <style> tags (which would otherwise pile up).
3398
3705
  for (const r of allResults) {
3399
3706
  const style = document.createElement('style');
3707
+ style.setAttribute('data-snice-style', '');
3400
3708
  style.textContent = r.cssText;
3401
3709
  element.shadowRoot.appendChild(style);
3402
3710
  }
@@ -3406,6 +3714,125 @@ var Snice = (function (exports) {
3406
3714
  }
3407
3715
  }
3408
3716
 
3717
+ // @dispatch decorator - auto-dispatches custom events from method return values
3718
+ /**
3719
+ * Decorator that automatically dispatches a custom event after a method is called.
3720
+ * The return value of the method becomes the event detail.
3721
+ *
3722
+ * @param eventName The name of the event to dispatch
3723
+ * @param options Optional configuration extending EventInit
3724
+ */
3725
+ function dispatch(eventName, options) {
3726
+ return function (originalMethod, _context) {
3727
+ return function (...args) {
3728
+ // Create timing wrappers for dispatch (per-instance)
3729
+ if (!this[DISPATCH_TIMERS]) {
3730
+ this[DISPATCH_TIMERS] = new Map();
3731
+ }
3732
+ const timerKey = `${eventName}_${_context.name}`;
3733
+ if (!this[DISPATCH_TIMERS].has(timerKey)) {
3734
+ this[DISPATCH_TIMERS].set(timerKey, {
3735
+ debounceTimeout: null,
3736
+ throttleLastCall: 0,
3737
+ throttleTimeout: null
3738
+ });
3739
+ }
3740
+ const timers = this[DISPATCH_TIMERS].get(timerKey);
3741
+ // Call the original method with preserved this context
3742
+ const result = originalMethod.apply(this, args);
3743
+ // Helper to dispatch the event
3744
+ const doDispatch = (detail) => {
3745
+ // Skip dispatch if result is undefined and dispatchOnUndefined is false
3746
+ if (detail === undefined && options?.dispatchOnUndefined === false) {
3747
+ return;
3748
+ }
3749
+ // Create event with spread operator for options
3750
+ const event = new CustomEvent(eventName, {
3751
+ bubbles: true, // Default to true for component events
3752
+ composed: true, // Allow crossing shadow DOM boundaries
3753
+ ...options, // Spread all EventInit options
3754
+ detail
3755
+ });
3756
+ // Resolve scope target. Default is `this` (host element).
3757
+ // Explicit scope dispatches the event on another target — useful for
3758
+ // 'global' bus events on document, or routing through an ancestor.
3759
+ if (options?.scope !== undefined) {
3760
+ const target = resolveScope(this, options.scope);
3761
+ if (!target) {
3762
+ console.warn(`[snice/@dispatch] scope did not resolve for "${eventName}" — event not dispatched.`);
3763
+ return;
3764
+ }
3765
+ target.dispatchEvent(event);
3766
+ return;
3767
+ }
3768
+ this.dispatchEvent(event);
3769
+ };
3770
+ // Helper to handle timed dispatch
3771
+ const timedDispatch = (detail) => {
3772
+ if (options?.debounce) {
3773
+ clearTimeout(timers.debounceTimeout);
3774
+ timers.debounceTimeout = setTimeout(() => doDispatch(detail), options.debounce);
3775
+ return;
3776
+ }
3777
+ if (!options?.throttle) {
3778
+ doDispatch(detail);
3779
+ return;
3780
+ }
3781
+ const now = Date.now();
3782
+ const remaining = options.throttle - (now - timers.throttleLastCall);
3783
+ if (remaining <= 0) {
3784
+ clearTimeout(timers.throttleTimeout);
3785
+ timers.throttleLastCall = now;
3786
+ doDispatch(detail);
3787
+ return;
3788
+ }
3789
+ // Record the LATEST detail so the trailing dispatch carries fresh
3790
+ // data, not the first-suppressed-call detail captured by closure.
3791
+ timers.latestDetail = detail;
3792
+ if (!timers.throttleTimeout) {
3793
+ timers.throttleTimeout = setTimeout(() => {
3794
+ timers.throttleLastCall = Date.now();
3795
+ timers.throttleTimeout = null;
3796
+ const d = timers.latestDetail;
3797
+ timers.latestDetail = undefined;
3798
+ doDispatch(d);
3799
+ }, remaining);
3800
+ }
3801
+ };
3802
+ // Handle async methods
3803
+ if (result instanceof Promise) {
3804
+ return result.then((resolvedResult) => {
3805
+ timedDispatch(resolvedResult);
3806
+ return resolvedResult;
3807
+ });
3808
+ }
3809
+ // Sync method
3810
+ timedDispatch(result);
3811
+ return result;
3812
+ };
3813
+ };
3814
+ }
3815
+ /**
3816
+ * Clear any pending debounce/throttle dispatch timers on an instance (e.g. on
3817
+ * disconnect, so a queued event doesn't fire into a detached node). A dispatch
3818
+ * is a one-shot signal, so pending ones are dropped, not replayed.
3819
+ */
3820
+ function clearDispatchTimers(instance) {
3821
+ const timers = instance[DISPATCH_TIMERS];
3822
+ if (!timers)
3823
+ return;
3824
+ for (const t of timers.values()) {
3825
+ if (t.debounceTimeout)
3826
+ clearTimeout(t.debounceTimeout);
3827
+ if (t.throttleTimeout)
3828
+ clearTimeout(t.throttleTimeout);
3829
+ t.debounceTimeout = null;
3830
+ t.throttleTimeout = null;
3831
+ t.throttleLastCall = 0;
3832
+ t.latestDetail = undefined;
3833
+ }
3834
+ }
3835
+
3409
3836
  /**
3410
3837
  * Method decorators for common patterns
3411
3838
  * @debounce, @throttle, @once, @memoize
@@ -3832,6 +4259,14 @@ var Snice = (function (exports) {
3832
4259
  }
3833
4260
  }
3834
4261
  }
4262
+ // Replay a render that was dropped while disconnected. The scheduler
4263
+ // sets this flag when it skips a detached element, so a property changed
4264
+ // while detached still reaches the DOM on reattach. A plain move (no
4265
+ // pending render) leaves the flag unset, so we don't re-render needlessly.
4266
+ if (this[RENDER_METHOD] && this[PENDING_RECONNECT_RENDER]) {
4267
+ this[PENDING_RECONNECT_RENDER] = false;
4268
+ requestRender(this);
4269
+ }
3835
4270
  return;
3836
4271
  }
3837
4272
  // Mark that properties are being initialized from attributes
@@ -3879,6 +4314,9 @@ var Snice = (function (exports) {
3879
4314
  setupResponseHandlers(this, this);
3880
4315
  setupContextHandler(this);
3881
4316
  this[INITIALIZED] = true;
4317
+ // Now that the initial value is settled, give @watch immediate handlers
4318
+ // their one init call (before the first render, so derived state is ready).
4319
+ invokeImmediateWatchers(this, constructor);
3882
4320
  if (originalConnectedCallback) {
3883
4321
  originalConnectedCallback.call(this);
3884
4322
  }
@@ -3973,6 +4411,17 @@ var Snice = (function (exports) {
3973
4411
  // Cleanup pending @debounce / @throttle timers so they don't fire on a dead element
3974
4412
  clearDebounceTimers(this);
3975
4413
  clearThrottleTimers(this);
4414
+ // The render/dispatch debounce-throttle timers live in separate stores.
4415
+ // A pending render is replayed on reconnect (same flag the scheduler
4416
+ // uses); a pending dispatch is a one-shot signal, so it's dropped.
4417
+ if (clearRenderTimers(this)) {
4418
+ this[PENDING_RECONNECT_RENDER] = true;
4419
+ }
4420
+ clearDispatchTimers(this);
4421
+ // @moved / @adopted debounce-throttle timers, likewise dropped so they
4422
+ // don't fire on a dead element.
4423
+ clearLifecycleTimers(this, MOVED_TIMERS);
4424
+ clearLifecycleTimers(this, ADOPTED_TIMERS);
3976
4425
  };
3977
4426
  constructor.prototype.attributeChangedCallback = function (name, oldValue, newValue) {
3978
4427
  originalAttributeChangedCallback?.call(this, name, oldValue, newValue);
@@ -3988,13 +4437,27 @@ var Snice = (function (exports) {
3988
4437
  if (attributeName.toLowerCase() !== name.toLowerCase())
3989
4438
  continue;
3990
4439
  const currentValue = this[PROPERTY_VALUES]?.[propName];
3991
- const parsedValue = parseAttributeValue(newValue, propOptions, currentValue, undefined);
3992
- if (currentValue === parsedValue)
4440
+ // A removed attribute reverts the property to its field default. Read it
4441
+ // back through the getter (attribute is already gone at this point) so the
4442
+ // watcher's newValue and PROPERTY_VALUES match what this[propName] now
4443
+ // returns — parseAttributeValue(null) would diverge to null for String/Number.
4444
+ const parsedValue = newValue === null
4445
+ ? this[propName]
4446
+ : parseAttributeValue(newValue, propOptions, currentValue, undefined);
4447
+ const changed = propOptions?.hasChanged
4448
+ ? propOptions.hasChanged(parsedValue, currentValue)
4449
+ : notEqual(parsedValue, currentValue);
4450
+ if (!changed)
3993
4451
  break;
3994
4452
  ensureSet(this, EXPLICITLY_SET_PROPERTIES).add(propName);
3995
4453
  ensureObj(this, PROPERTY_VALUES)[propName] = parsedValue;
3996
4454
  if (!this[SETTING_FROM_PROPERTY]?.has(name.toLowerCase())) {
3997
- invokeWatchers(this, constructor, propName, currentValue, parsedValue);
4455
+ // Watchers react to changes only. During upgrade the initial value
4456
+ // is not a change, so suppress until INITIALIZED; @watch immediate
4457
+ // handlers get their one init call from invokeImmediateWatchers.
4458
+ if (this[INITIALIZED]) {
4459
+ invokeWatchers(this, constructor, propName, currentValue, parsedValue);
4460
+ }
3998
4461
  if (this[RENDER_METHOD] && this[INITIALIZED]) {
3999
4462
  requestRender(this);
4000
4463
  }
@@ -4004,12 +4467,14 @@ var Snice = (function (exports) {
4004
4467
  };
4005
4468
  // Add connectedMoveCallback for handling DOM moves
4006
4469
  constructor.prototype.connectedMoveCallback = async function () {
4007
- // Call @moved handlers
4470
+ // Call @moved handlers via the prototype method (the debounce/throttle
4471
+ // wrapper), not the raw handler — otherwise @moved({debounce}) options
4472
+ // are ignored for real moves and only work when called directly.
4008
4473
  const movedHandlers = constructor[MOVED_HANDLERS];
4009
4474
  if (movedHandlers) {
4010
4475
  for (const handler of movedHandlers) {
4011
4476
  try {
4012
- await handler.method.call(this);
4477
+ await this[handler.methodName]();
4013
4478
  }
4014
4479
  catch (error) {
4015
4480
  console.error(`Error in @moved handler ${handler.methodName}:`, error);
@@ -4019,12 +4484,13 @@ var Snice = (function (exports) {
4019
4484
  };
4020
4485
  // Add adoptedCallback for handling document adoption
4021
4486
  constructor.prototype.adoptedCallback = async function () {
4022
- // Call @adopted handlers
4487
+ // Call @adopted handlers via the prototype method (the debounce/throttle
4488
+ // wrapper), not the raw handler — same reason as connectedMoveCallback.
4023
4489
  const adoptedHandlers = constructor[ADOPTED_HANDLERS];
4024
4490
  if (adoptedHandlers) {
4025
4491
  for (const handler of adoptedHandlers) {
4026
4492
  try {
4027
- await handler.method.call(this);
4493
+ await this[handler.methodName]();
4028
4494
  }
4029
4495
  catch (error) {
4030
4496
  console.error(`Error in @adopted handler ${handler.methodName}:`, error);
@@ -4189,7 +4655,12 @@ var Snice = (function (exports) {
4189
4655
  },
4190
4656
  set(newValue) {
4191
4657
  const oldValue = this[propertyKey];
4192
- if (oldValue === newValue)
4658
+ // A custom hasChanged comparator overrides the default === check
4659
+ // (e.g. deep-equal objects, or forcing an update on a mutated ref).
4660
+ const changed = finalOptions?.hasChanged
4661
+ ? finalOptions.hasChanged(newValue, oldValue)
4662
+ : notEqual(newValue, oldValue);
4663
+ if (!changed)
4193
4664
  return;
4194
4665
  // Pre-init: store for later, don't reflect to DOM yet
4195
4666
  if (!this[PROPERTIES_INITIALIZED]) {
@@ -4206,18 +4677,27 @@ var Snice = (function (exports) {
4206
4677
  const attributeName = getAttrName(finalOptions, propertyKey);
4207
4678
  const attributeValue = valueToAttribute(newValue, finalOptions, initialValue);
4208
4679
  ensureSet(this, EXPLICITLY_SET_PROPERTIES).add(propertyKey);
4209
- ensureSet(this, SETTING_FROM_PROPERTY).add(attributeName.toLowerCase());
4680
+ const attrKey = attributeName.toLowerCase();
4681
+ ensureSet(this, SETTING_FROM_PROPERTY).add(attrKey);
4682
+ // attributeChangedCallback runs synchronously inside setAttribute /
4683
+ // removeAttribute (custom element reactions are synchronous), so the
4684
+ // guard only needs to bracket this one reflection. Clear it right
4685
+ // after — not on a microtask — otherwise an external attribute change
4686
+ // in the same tick is mistaken for our own echo and silently dropped.
4210
4687
  if (attributeValue === null) {
4211
4688
  this.removeAttribute?.(attributeName);
4212
4689
  }
4213
4690
  else {
4214
4691
  this.setAttribute?.(attributeName, attributeValue);
4215
4692
  }
4216
- queueMicrotask(() => {
4217
- this[SETTING_FROM_PROPERTY]?.delete(attributeName.toLowerCase());
4218
- });
4693
+ this[SETTING_FROM_PROPERTY].delete(attrKey);
4694
+ }
4695
+ // See attributeChangedCallback: the initial value is not a change,
4696
+ // so suppress watchers until INITIALIZED. @watch immediate handlers
4697
+ // get their one init call from invokeImmediateWatchers.
4698
+ if (this[INITIALIZED]) {
4699
+ invokeWatchers(this, this.constructor, propertyKey, oldValue, newValue);
4219
4700
  }
4220
- invokeWatchers(this, this.constructor, propertyKey, oldValue, newValue);
4221
4701
  if (this[RENDER_METHOD] && this[INITIALIZED]) {
4222
4702
  requestRender(this);
4223
4703
  }
@@ -4287,7 +4767,12 @@ var Snice = (function (exports) {
4287
4767
  };
4288
4768
  };
4289
4769
  }
4290
- function watch(...propertyNames) {
4770
+ function watch(...args) {
4771
+ // A trailing options object is separated from the watched property names.
4772
+ const propertyNames = args.filter((a) => typeof a === 'string');
4773
+ const options = args.find((a) => typeof a === 'object' && a !== null);
4774
+ // Fire on init by default; opt out of the init call with { immediate: false }.
4775
+ const immediate = options?.immediate !== false;
4291
4776
  return function (target, context) {
4292
4777
  const methodName = context.name;
4293
4778
  context.addInitializer(function () {
@@ -4327,7 +4812,8 @@ var Snice = (function (exports) {
4327
4812
  }
4328
4813
  constructor[PROPERTY_WATCHERS].get(propertyName).push({
4329
4814
  methodName,
4330
- method: target
4815
+ method: target,
4816
+ immediate
4331
4817
  });
4332
4818
  }
4333
4819
  });
@@ -4436,6 +4922,24 @@ var Snice = (function (exports) {
4436
4922
  registerHandler(RECONNECT_HANDLERS, RECONNECT_METHODS, target, context);
4437
4923
  };
4438
4924
  }
4925
+ /**
4926
+ * Clear any pending @moved / @adopted debounce/throttle timers on an instance
4927
+ * (e.g. on disconnect), so they don't fire on a dead element.
4928
+ */
4929
+ function clearLifecycleTimers(instance, timersSymbol) {
4930
+ const map = instance[timersSymbol];
4931
+ if (!map)
4932
+ return;
4933
+ for (const t of map.values()) {
4934
+ if (t.debounceTimer)
4935
+ clearTimeout(t.debounceTimer);
4936
+ if (t.throttleTimer)
4937
+ clearTimeout(t.throttleTimer);
4938
+ t.debounceTimer = null;
4939
+ t.throttleTimer = null;
4940
+ t.lastThrottleCall = 0;
4941
+ }
4942
+ }
4439
4943
  function createLifecycleDecorator(handlersSymbol, timersSymbol, methodsSymbol) {
4440
4944
  return function (options = {}) {
4441
4945
  return function (originalMethod, context) {
@@ -4449,9 +4953,23 @@ var Snice = (function (exports) {
4449
4953
  }
4450
4954
  const timers = this[timersSymbol].get(methodName);
4451
4955
  const exec = (...a) => originalMethod.apply(this, a);
4956
+ // Deferred (debounce/throttle) calls run inside a setTimeout, outside
4957
+ // the caller's try/catch — so catch their sync throws and async
4958
+ // rejections here and log, rather than surface an uncaught error.
4959
+ const execDeferred = (...a) => {
4960
+ try {
4961
+ const result = originalMethod.apply(this, a);
4962
+ if (result && typeof result.then === 'function') {
4963
+ result.catch((error) => console.error(`Error in deferred lifecycle handler ${methodName}:`, error));
4964
+ }
4965
+ }
4966
+ catch (error) {
4967
+ console.error(`Error in deferred lifecycle handler ${methodName}:`, error);
4968
+ }
4969
+ };
4452
4970
  if (options.debounce > 0) {
4453
4971
  clearTimeout(timers.debounceTimer);
4454
- timers.debounceTimer = setTimeout(() => exec(...args), options.debounce);
4972
+ timers.debounceTimer = setTimeout(() => execDeferred(...args), options.debounce);
4455
4973
  return undefined;
4456
4974
  }
4457
4975
  if (options.throttle > 0) {
@@ -4465,7 +4983,7 @@ var Snice = (function (exports) {
4465
4983
  timers.throttleTimer = setTimeout(() => {
4466
4984
  timers.throttleTimer = null;
4467
4985
  timers.lastThrottleCall = Date.now();
4468
- exec(...args);
4986
+ execDeferred(...args);
4469
4987
  }, remaining);
4470
4988
  }
4471
4989
  return undefined;
@@ -5097,6 +5615,40 @@ var Snice = (function (exports) {
5097
5615
  }
5098
5616
  }
5099
5617
 
5618
+ /**
5619
+ * Score a route pattern by specificity so that more specific routes are tried
5620
+ * first. Scoring is per-segment by ROLE, not by string length: a static segment
5621
+ * outweighs a dynamic (`:param`) segment, which outweighs a wildcard (`*` /
5622
+ * `*name`). So `/post/new` beats `/post/:id`, and a splat always ranks last —
5623
+ * regardless of how long the pattern string happens to be. This mirrors the
5624
+ * segment-scoring model mainstream routers (React Router, Remix) use.
5625
+ *
5626
+ * Sort callers descending by this score (highest = most specific = first).
5627
+ *
5628
+ * NOTE: mirrored in `src/react/route-specificity.ts` for the isolated React
5629
+ * bundle — keep the two in sync.
5630
+ */
5631
+ const STATIC_SEGMENT = 10;
5632
+ const DYNAMIC_SEGMENT = 3;
5633
+ const WILDCARD_SEGMENT = -2;
5634
+ function routeSpecificity(spec) {
5635
+ const segments = spec.split('/').filter(Boolean);
5636
+ // Base on segment count so a deeper path outranks a shallower one on ties.
5637
+ let score = segments.length;
5638
+ for (const segment of segments) {
5639
+ if (segment.startsWith('*')) {
5640
+ score += WILDCARD_SEGMENT;
5641
+ }
5642
+ else if (segment.startsWith(':')) {
5643
+ score += DYNAMIC_SEGMENT;
5644
+ }
5645
+ else {
5646
+ score += STATIC_SEGMENT;
5647
+ }
5648
+ }
5649
+ return score;
5650
+ }
5651
+
5100
5652
  /**
5101
5653
  * Generic transition system for animating between elements
5102
5654
  */
@@ -5364,7 +5916,7 @@ var Snice = (function (exports) {
5364
5916
  }
5365
5917
  const needsSorting = !is_sorted;
5366
5918
  if (needsSorting) {
5367
- routes.sort((a, b) => b.route.spec.length - a.route.spec.length);
5919
+ routes.sort((a, b) => routeSpecificity(b.route.spec) - routeSpecificity(a.route.spec));
5368
5920
  is_sorted = true;
5369
5921
  }
5370
5922
  // Collect placards from registered routes
@@ -5435,7 +5987,7 @@ var Snice = (function (exports) {
5435
5987
  </div>`;
5436
5988
  return container;
5437
5989
  }
5438
- async function checkGuards(guards, params, target) {
5990
+ async function checkGuards(guards, params, target, stale) {
5439
5991
  const hasGuards = !!guards;
5440
5992
  if (!hasGuards) {
5441
5993
  return true;
@@ -5443,8 +5995,11 @@ var Snice = (function (exports) {
5443
5995
  const guardsArray = Array.isArray(guards) ? guards : [guards];
5444
5996
  let spinner = null;
5445
5997
  let spinnerTimer = null;
5446
- // Show spinner after a short delay (avoids flash for fast sync guards)
5998
+ // Show spinner after a short delay (avoids flash for fast sync guards).
5999
+ // Skip if a newer navigation has already superseded this one.
5447
6000
  spinnerTimer = setTimeout(() => {
6001
+ if (stale && stale())
6002
+ return;
5448
6003
  spinner = createLoadingSpinner();
5449
6004
  target.appendChild(spinner);
5450
6005
  }, 50);
@@ -5452,7 +6007,11 @@ var Snice = (function (exports) {
5452
6007
  for (const guard of guardsArray) {
5453
6008
  const allowed = await guard(context, params);
5454
6009
  if (!allowed) {
5455
- renderForbiddenPage(target);
6010
+ // Don't stomp the current page with a 403 if a newer navigation
6011
+ // already took over while this guard was resolving.
6012
+ if (!stale || !stale()) {
6013
+ renderForbiddenPage(target);
6014
+ }
5456
6015
  return false;
5457
6016
  }
5458
6017
  }
@@ -5488,14 +6047,14 @@ var Snice = (function (exports) {
5488
6047
  div.innerHTML = /*html*/ `<h1>404</h1><p>Page not found</p>`;
5489
6048
  return { element: div, transition: undefined, layout: undefined };
5490
6049
  }
5491
- async function resolveRoute(path, target) {
6050
+ async function resolveRoute(path, target, stale) {
5492
6051
  for (const route of routes) {
5493
6052
  const params = route.route.match(path);
5494
6053
  const isMatch = params !== false;
5495
6054
  if (!isMatch) {
5496
6055
  continue;
5497
6056
  }
5498
- const guardsAllowed = await checkGuards(route.guards, params, target);
6057
+ const guardsAllowed = await checkGuards(route.guards, params, target, stale);
5499
6058
  if (!guardsAllowed) {
5500
6059
  return { result: RouteResult.GUARDS_FAILED };
5501
6060
  }
@@ -5623,7 +6182,7 @@ var Snice = (function (exports) {
5623
6182
  const isHomePath = (path?.trim() === '' || path === '/') && !!home;
5624
6183
  if (isHomePath) {
5625
6184
  const homeRoute = routes.find(r => r.route.match('/'));
5626
- if (!(await checkGuards(homeRoute?.guards, {}, target)))
6185
+ if (!(await checkGuards(homeRoute?.guards, {}, target, stale)))
5627
6186
  return;
5628
6187
  if (stale())
5629
6188
  return;
@@ -5635,7 +6194,7 @@ var Snice = (function (exports) {
5635
6194
  if (!path)
5636
6195
  return;
5637
6196
  // Resolve route
5638
- const routeResult = await resolveRoute(path, target);
6197
+ const routeResult = await resolveRoute(path, target, stale);
5639
6198
  if (stale())
5640
6199
  return;
5641
6200
  // Guards failed (403 already rendered by checkGuards)
@@ -5649,7 +6208,7 @@ var Snice = (function (exports) {
5649
6208
  }
5650
6209
  // 404 fallthrough
5651
6210
  const { element, transition, layout } = create404Element();
5652
- await renderPage(target, element, transition, layout, path, {});
6211
+ await renderPage(target, element, transition, layout, path, {}, stale);
5653
6212
  }
5654
6213
  async function performTransition$1(container, oldElement, newElement, transition, atGen = navGeneration) {
5655
6214
  // `isStale` returns true when a newer navigate() has been kicked off.
@@ -5666,93 +6225,6 @@ var Snice = (function (exports) {
5666
6225
  };
5667
6226
  }
5668
6227
 
5669
- // @dispatch decorator - auto-dispatches custom events from method return values
5670
- /**
5671
- * Decorator that automatically dispatches a custom event after a method is called.
5672
- * The return value of the method becomes the event detail.
5673
- *
5674
- * @param eventName The name of the event to dispatch
5675
- * @param options Optional configuration extending EventInit
5676
- */
5677
- function dispatch(eventName, options) {
5678
- return function (originalMethod, _context) {
5679
- return function (...args) {
5680
- // Create timing wrappers for dispatch (per-instance)
5681
- if (!this[DISPATCH_TIMERS]) {
5682
- this[DISPATCH_TIMERS] = new Map();
5683
- }
5684
- const timerKey = `${eventName}_${_context.name}`;
5685
- if (!this[DISPATCH_TIMERS].has(timerKey)) {
5686
- this[DISPATCH_TIMERS].set(timerKey, {
5687
- debounceTimeout: null,
5688
- throttleLastCall: 0,
5689
- throttleTimeout: null
5690
- });
5691
- }
5692
- const timers = this[DISPATCH_TIMERS].get(timerKey);
5693
- // Call the original method with preserved this context
5694
- const result = originalMethod.apply(this, args);
5695
- // Helper to dispatch the event
5696
- const doDispatch = (detail) => {
5697
- // Skip dispatch if result is undefined and dispatchOnUndefined is false
5698
- if (detail === undefined && options?.dispatchOnUndefined === false) {
5699
- return;
5700
- }
5701
- // Create event with spread operator for options
5702
- const event = new CustomEvent(eventName, {
5703
- bubbles: true, // Default to true for component events
5704
- composed: true, // Allow crossing shadow DOM boundaries
5705
- ...options, // Spread all EventInit options
5706
- detail
5707
- });
5708
- this.dispatchEvent(event);
5709
- };
5710
- // Helper to handle timed dispatch
5711
- const timedDispatch = (detail) => {
5712
- if (options?.debounce) {
5713
- clearTimeout(timers.debounceTimeout);
5714
- timers.debounceTimeout = setTimeout(() => doDispatch(detail), options.debounce);
5715
- return;
5716
- }
5717
- if (!options?.throttle) {
5718
- doDispatch(detail);
5719
- return;
5720
- }
5721
- const now = Date.now();
5722
- const remaining = options.throttle - (now - timers.throttleLastCall);
5723
- if (remaining <= 0) {
5724
- clearTimeout(timers.throttleTimeout);
5725
- timers.throttleLastCall = now;
5726
- doDispatch(detail);
5727
- return;
5728
- }
5729
- // Record the LATEST detail so the trailing dispatch carries fresh
5730
- // data, not the first-suppressed-call detail captured by closure.
5731
- timers.latestDetail = detail;
5732
- if (!timers.throttleTimeout) {
5733
- timers.throttleTimeout = setTimeout(() => {
5734
- timers.throttleLastCall = Date.now();
5735
- timers.throttleTimeout = null;
5736
- const d = timers.latestDetail;
5737
- timers.latestDetail = undefined;
5738
- doDispatch(d);
5739
- }, remaining);
5740
- }
5741
- };
5742
- // Handle async methods
5743
- if (result instanceof Promise) {
5744
- return result.then((resolvedResult) => {
5745
- timedDispatch(resolvedResult);
5746
- return resolvedResult;
5747
- });
5748
- }
5749
- // Sync method
5750
- timedDispatch(result);
5751
- return result;
5752
- };
5753
- };
5754
- }
5755
-
5756
6228
  /**
5757
6229
  * createRequestHandler — vanilla JS utility for handling @request channels
5758
6230
  * outside of the Snice element/controller decorator system.