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.2
2
+ * snice v6.0.0
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.
@@ -8,7 +8,7 @@ var Snice = (function (exports) {
8
8
  'use strict';
9
9
 
10
10
  /*!
11
- * snice v5.2.1
11
+ * snice v5.2.3
12
12
  * 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.
13
13
  * (c) 2024
14
14
  * Released under the MIT License.
@@ -42,6 +42,12 @@ var Snice = (function (exports) {
42
42
  // Internal element state symbols
43
43
  const READY_PROMISE = getSymbol('ready-promise');
44
44
  const READY_RESOLVE = getSymbol('ready-resolve');
45
+ // Set when a render is requested but dropped because the element was
46
+ // disconnected; consumed on reconnect to replay exactly the missed render.
47
+ const PENDING_RECONNECT_RENDER = getSymbol('pending-reconnect-render');
48
+ // Per-element synchronous render-nesting depth, used to cap runaway re-entry
49
+ // without a shared global counter that one element could exhaust for others.
50
+ const RENDER_DEPTH = getSymbol('render-depth');
45
51
  const CONTROLLER = getSymbol('controller');
46
52
  const INITIALIZED = getSymbol('initialized');
47
53
  // Controller symbols
@@ -113,6 +119,10 @@ var Snice = (function (exports) {
113
119
  // Controller attach-abort — set to an AbortController while attachController
114
120
  // is awaiting element.ready, so detachController can short-circuit the wait.
115
121
  const CONTROLLER_ABORT = getSymbol('controller-abort');
122
+ // Set on a controller instance once it has gotten past element.ready and is
123
+ // being attached, so detachController knows not to run detach() on a controller
124
+ // whose attach() was aborted before it ever ran.
125
+ const CONTROLLER_ATTACHED = getSymbol('controller-attached');
116
126
  const PROPERTY_DEFINERS = getSymbol('property-definers');
117
127
 
118
128
  /**
@@ -233,11 +243,23 @@ var Snice = (function (exports) {
233
243
  this[REGISTERED_ELEMENTS_SET].add(element);
234
244
  }
235
245
  /**
236
- * Unregister an element from receiving context updates
246
+ * Unregister an element from receiving context updates. With a methodName,
247
+ * removes only that one handler (so a `once` handler tearing itself down
248
+ * doesn't kill the element's other @context handlers); without one, removes
249
+ * the element entirely (used on disconnect).
237
250
  * @internal Used by @context decorator cleanup
238
251
  */
239
- [CONTEXT_UNREGISTER](element) {
240
- this[REGISTERED_ELEMENTS].delete(element);
252
+ [CONTEXT_UNREGISTER](element, methodName) {
253
+ const map = this[REGISTERED_ELEMENTS];
254
+ if (methodName !== undefined) {
255
+ const names = map.get(element);
256
+ if (!names)
257
+ return;
258
+ names.delete(methodName);
259
+ if (names.size > 0)
260
+ return; // other handlers remain — keep the element
261
+ }
262
+ map.delete(element);
241
263
  this[REGISTERED_ELEMENTS_SET].delete(element);
242
264
  }
243
265
  /**
@@ -402,11 +424,23 @@ var Snice = (function (exports) {
402
424
  * @param initialValue - The default value assigned to the property field (used for type detection)
403
425
  * @returns The string representation for the HTML attribute, or null if value should remove attribute
404
426
  */
427
+ /**
428
+ * Default property dirty-check: has the value changed? Like `!==`, but treats
429
+ * NaN → NaN as unchanged (NaN !== NaN would otherwise re-fire forever).
430
+ */
431
+ function notEqual(value, old) {
432
+ // Changed unless strictly equal, or both are NaN (each self-unequal).
433
+ return value !== old && (value === value || old === old);
434
+ }
405
435
  /**
406
436
  * Extract attribute name from property options
407
437
  */
408
438
  function getAttrName(opts, propName) {
409
- return typeof opts.attribute === 'string' ? opts.attribute : propName.toLowerCase();
439
+ // Lowercase always: the DOM stores attribute names lowercase, so an explicit
440
+ // camelCase `attribute:` would otherwise never match observedAttributes and
441
+ // attributeChangedCallback would never fire. get/setAttribute are already
442
+ // case-insensitive, so this is safe for every caller.
443
+ return (typeof opts.attribute === 'string' ? opts.attribute : propName).toLowerCase();
410
444
  }
411
445
  /**
412
446
  * Escape HTML text-node content. Use for text that will be interpolated
@@ -553,6 +587,42 @@ var Snice = (function (exports) {
553
587
  }
554
588
  }
555
589
  }
590
+ /**
591
+ * Fire the one-time initialization call for `@watch(..., { immediate: true })`
592
+ * handlers. Runs once per declared property after the element is initialized,
593
+ * passing the effective initial value with `oldValue` undefined. Non-immediate
594
+ * watchers are untouched — they only react to later changes.
595
+ */
596
+ function invokeImmediateWatchers(instance, constructor) {
597
+ const watchers = constructor[PROPERTY_WATCHERS];
598
+ const properties = constructor[PROPERTIES];
599
+ if (!watchers || !properties)
600
+ return;
601
+ for (const [propName, propOptions] of properties) {
602
+ // A watcher may be registered under the JS name, the explicit `attribute:`
603
+ // name, or the `*` wildcard; fire each immediate one exactly once, keyed to
604
+ // this property's current value.
605
+ const keys = [propName];
606
+ const attrName = propOptions && typeof propOptions.attribute === 'string' ? propOptions.attribute : null;
607
+ if (attrName && attrName !== propName)
608
+ keys.push(attrName);
609
+ keys.push('*');
610
+ for (const key of keys) {
611
+ if (!watchers.has(key))
612
+ continue;
613
+ for (const w of watchers.get(key)) {
614
+ if (!w.immediate)
615
+ continue;
616
+ try {
617
+ w.method.call(instance, undefined, instance[propName], propName);
618
+ }
619
+ catch (e) {
620
+ console.error(`Error in @watch('${key}') method ${w.methodName}:`, e);
621
+ }
622
+ }
623
+ }
624
+ }
625
+ }
556
626
  /**
557
627
  * Create a debounced version of a function
558
628
  */
@@ -586,6 +656,41 @@ var Snice = (function (exports) {
586
656
  }
587
657
  };
588
658
  }
659
+ /**
660
+ * Resolve a scope option to a concrete EventTarget.
661
+ *
662
+ * - undefined → host element (default behavior)
663
+ * - 'global' → `document`
664
+ * - string → `host.closest(selector)` (nearest matching ancestor)
665
+ * - EventTarget instance → that target directly
666
+ * - resolver function → called with host as `this`, returns target or null
667
+ *
668
+ * Returns null when the scope cannot be resolved (e.g. selector matches no
669
+ * ancestor, resolver returns null). Caller decides how to handle null —
670
+ * typically warn in dev and skip listener attachment.
671
+ */
672
+ function resolveScope(host, scope) {
673
+ if (scope === undefined || scope === null)
674
+ return host;
675
+ if (scope === 'global')
676
+ return typeof document !== 'undefined' ? document : null;
677
+ if (typeof scope === 'function') {
678
+ try {
679
+ return scope.call(host) || null;
680
+ }
681
+ catch {
682
+ return null;
683
+ }
684
+ }
685
+ if (typeof scope === 'string') {
686
+ return host.closest?.(scope) || null;
687
+ }
688
+ // Direct EventTarget instance.
689
+ if (scope && typeof scope.addEventListener === 'function') {
690
+ return scope;
691
+ }
692
+ return null;
693
+ }
589
694
  function valueToAttribute(value, propertyOptions, initialValue) {
590
695
  // Handle null/undefined/false values and empty arrays
591
696
  if (value === null || value === undefined || value === false ||
@@ -1003,13 +1108,11 @@ var Snice = (function (exports) {
1003
1108
  return finalValue;
1004
1109
  }
1005
1110
  catch (error) {
1006
- // Send error to generator
1007
- try {
1008
- await generator.throw(error);
1009
- }
1010
- catch (generatorError) {
1011
- throw generatorError;
1012
- }
1111
+ // Drive the generator's own catch block and return whatever it recovers
1112
+ // with (e.g. a cached fallback). If the generator re-throws instead,
1113
+ // generator.throw rejects and the error propagates to the caller.
1114
+ const { value: recovered } = await generator.throw(error);
1115
+ return recovered;
1013
1116
  }
1014
1117
  }; // Close actualRequest function
1015
1118
  // Debounce
@@ -1033,9 +1136,21 @@ var Snice = (function (exports) {
1033
1136
  const now = Date.now();
1034
1137
  const remaining = options.throttle - (now - throttleLastCall);
1035
1138
  if (remaining <= 0) {
1139
+ // Cancelling the scheduled trailing call must not orphan the promises
1140
+ // queued for it — settle them with this fresh result. Also reset
1141
+ // throttleTimeout so later within-window calls don't attach to a dead
1142
+ // timer and hang.
1036
1143
  clearTimeout(throttleTimeout);
1144
+ throttleTimeout = null;
1037
1145
  throttleLastCall = now;
1038
- return actualRequest();
1146
+ const orphaned = throttleTrailingResolvers.splice(0);
1147
+ const p = actualRequest();
1148
+ if (orphaned.length) {
1149
+ p.then((r) => { for (const x of orphaned)
1150
+ x.resolve(r); }, (e) => { for (const x of orphaned)
1151
+ x.reject(e); });
1152
+ }
1153
+ return p;
1039
1154
  }
1040
1155
  // Throttle: already has pending trailing call — attach to it
1041
1156
  if (throttleTimeout) {
@@ -1136,10 +1251,20 @@ var Snice = (function (exports) {
1136
1251
  const now = Date.now();
1137
1252
  const remaining = handler.options.throttle - (now - throttleLastCall);
1138
1253
  if (remaining <= 0) {
1254
+ // Same as the @request side: settle any queued trailing callers with
1255
+ // this fresh result rather than orphaning their promises.
1139
1256
  clearTimeout(throttleTimeout);
1140
1257
  throttleTimeout = null;
1141
1258
  throttleLastCall = now;
1142
- return originalMethod(...args);
1259
+ const orphaned = throttleTrailingResolvers.splice(0);
1260
+ throttleLatestArgs = null;
1261
+ const result = originalMethod(...args);
1262
+ if (orphaned.length) {
1263
+ Promise.resolve(result).then((r) => { for (const x of orphaned)
1264
+ x.resolve(r); }, (e) => { for (const x of orphaned)
1265
+ x.reject(e); });
1266
+ }
1267
+ return result;
1143
1268
  }
1144
1269
  // Remember the LATEST args so the trailing call uses fresh input,
1145
1270
  // and queue every suppressed caller's resolver so they all receive
@@ -1246,7 +1371,10 @@ var Snice = (function (exports) {
1246
1371
  // Combine strings and values into final CSS text
1247
1372
  let cssText = strings[0];
1248
1373
  for (let i = 0; i < values.length; i++) {
1249
- cssText += String(values[i]) + strings[i + 1];
1374
+ // Inline a nested css`` result by its text so style fragments compose;
1375
+ // String() on a CSSResult object would yield "[object Object]".
1376
+ const value = values[i];
1377
+ cssText += (isCSSResult(value) ? value.cssText : String(value)) + strings[i + 1];
1250
1378
  }
1251
1379
  const result = {
1252
1380
  _$litType$: CSS_RESULT,
@@ -1397,6 +1525,20 @@ var Snice = (function (exports) {
1397
1525
  const attrStrings = value.split(marker);
1398
1526
  // Number of expressions = number of markers = attrStrings.length - 1
1399
1527
  const expressionCount = attrStrings.length - 1;
1528
+ // Property/boolean/event bindings take a SINGLE expression — they
1529
+ // can't concatenate static text (a `.prop` is an object, not a
1530
+ // string). They consume the first expression; any static text or
1531
+ // extra expressions are ignored. But the value index must still
1532
+ // advance by the true marker count, or every later binding shifts.
1533
+ const isSingleExpressionBinding = /^@@?|^[.?]/.test(originalName);
1534
+ if (isSingleExpressionBinding) {
1535
+ const hasStaticText = attrStrings.some((s) => s !== '');
1536
+ if (expressionCount > 1 || hasStaticText) {
1537
+ console.warn(`snice: binding "${originalName}" takes a single expression; ` +
1538
+ `static text and extra interpolations are ignored. ` +
1539
+ `Use a plain attribute for string interpolation.`);
1540
+ }
1541
+ }
1400
1542
  if (originalName.startsWith('@@')) {
1401
1543
  // Escaped event binding for events with @ in the name (e.g., @@snice/event -> @snice/event)
1402
1544
  this.parts.push({
@@ -1405,7 +1547,7 @@ var Snice = (function (exports) {
1405
1547
  name: originalName.slice(1), // Keep the @ in the event name
1406
1548
  element
1407
1549
  });
1408
- partIndex += 1;
1550
+ partIndex += expressionCount;
1409
1551
  }
1410
1552
  else if (originalName.startsWith('@')) {
1411
1553
  // Event binding (single value only)
@@ -1415,7 +1557,7 @@ var Snice = (function (exports) {
1415
1557
  name: originalName.slice(1),
1416
1558
  element
1417
1559
  });
1418
- partIndex += 1;
1560
+ partIndex += expressionCount;
1419
1561
  }
1420
1562
  else if (originalName.startsWith('.')) {
1421
1563
  // Property binding (single value only)
@@ -1425,7 +1567,7 @@ var Snice = (function (exports) {
1425
1567
  name: originalName.slice(1),
1426
1568
  element
1427
1569
  });
1428
- partIndex += 1;
1570
+ partIndex += expressionCount;
1429
1571
  }
1430
1572
  else if (originalName.startsWith('?')) {
1431
1573
  // Boolean attribute (single value only)
@@ -1435,7 +1577,7 @@ var Snice = (function (exports) {
1435
1577
  name: originalName.slice(1),
1436
1578
  element
1437
1579
  });
1438
- partIndex += 1;
1580
+ partIndex += expressionCount;
1439
1581
  }
1440
1582
  else {
1441
1583
  // Regular attribute - supports multiple interpolations
@@ -1910,7 +2052,13 @@ var Snice = (function (exports) {
1910
2052
  * Commit unsafe HTML string
1911
2053
  */
1912
2054
  _commitUnsafeHTML(value) {
1913
- // Always recreate for unsafe HTML (can't diff arbitrary HTML)
2055
+ // Can't diff arbitrary HTML, but when the STRING is unchanged the DOM is
2056
+ // already correct — skip the clear+reparse so live state (typed input
2057
+ // values, focus, scroll) inside the block survives unrelated re-renders.
2058
+ const prev = this._committedValue;
2059
+ if (isUnsafeHTML(prev) && prev.html === value.html) {
2060
+ return;
2061
+ }
1914
2062
  this._clear();
1915
2063
  const temp = document.createElement('template');
1916
2064
  temp.innerHTML = value.html;
@@ -2095,7 +2243,9 @@ var Snice = (function (exports) {
2095
2243
  // Supports both dot notation (@keydown.enter) and colon notation (@keydown:Enter) to match @on decorator
2096
2244
  // Only parse colons for keyboard events, not custom events
2097
2245
  const isKeyboardEvent = ['keydown', 'keyup', 'keypress'].includes(eventName.split('.')[0].split(':')[0]);
2098
- const dotIndex = eventName.indexOf('.');
2246
+ // Only keyboard events split on `.`/`:` into a key filter — a custom event
2247
+ // name may legitimately contain a dot (e.g. `app.ready`) and must be kept whole.
2248
+ const dotIndex = isKeyboardEvent ? eventName.indexOf('.') : -1;
2099
2249
  const colonIndex = isKeyboardEvent ? eventName.indexOf(':') : -1;
2100
2250
  // Use whichever delimiter comes first (dot or colon)
2101
2251
  const delimiterIndex = dotIndex > 0 && colonIndex > 0
@@ -2109,6 +2259,7 @@ var Snice = (function (exports) {
2109
2259
  }
2110
2260
  else {
2111
2261
  this.eventName = eventName;
2262
+ warnIfModifierMisuse(eventName);
2112
2263
  }
2113
2264
  }
2114
2265
  commit(value) {
@@ -2134,17 +2285,19 @@ var Snice = (function (exports) {
2134
2285
  }
2135
2286
  if (typeof value !== 'function')
2136
2287
  return;
2137
- // Auto-bind to host element (the custom element with shadow root)
2138
- if (!this.host) {
2139
- const rootNode = this.element.getRootNode();
2140
- this.host = rootNode.host || null;
2141
- }
2142
- const context = this.host || null;
2143
2288
  const keyFilter = this.keyFilter;
2144
2289
  this.listener = ((event) => {
2145
2290
  if (keyFilter && !matchesKeyboardFilter(event, keyFilter))
2146
2291
  return;
2147
- value.call(context, event);
2292
+ // Resolve the host (the custom element owning the shadow root) at dispatch
2293
+ // time, not bind time: a binding inside an initially-hidden <if>/<case>
2294
+ // branch commits while off-DOM, where getRootNode() has no host — caching
2295
+ // that null permanently would call the handler with `this = null` once the
2296
+ // branch is shown. By the time an event fires, the element is in the DOM.
2297
+ if (!this.host) {
2298
+ this.host = this.element.getRootNode().host || null;
2299
+ }
2300
+ value.call(this.host || null, event);
2148
2301
  });
2149
2302
  this.element.addEventListener(this.eventName, this.listener);
2150
2303
  }
@@ -2163,6 +2316,25 @@ var Snice = (function (exports) {
2163
2316
  * "ctrl+shift+s" -> { key: "s", ctrl: true, shift: true }
2164
2317
  * "~enter" -> { key: "Enter", anyModifiers: true }
2165
2318
  */
2319
+ // once/preventDefault/stopPropagation/capture/passive are @on() OPTIONS, not
2320
+ // template modifiers. A dotted non-keyboard event whose suffix is one of these
2321
+ // is almost certainly a mistake (e.g. `@click.once`) — it binds a listener for
2322
+ // an event type the DOM never dispatches. Warn instead of failing silently. A
2323
+ // legitimate custom event name like `app.ready` won't match and stays quiet.
2324
+ const MODIFIER_WORDS = new Set([
2325
+ 'once', 'prevent', 'preventdefault', 'stop', 'stoppropagation', 'capture', 'passive',
2326
+ ]);
2327
+ function warnIfModifierMisuse(eventName) {
2328
+ const dot = eventName.lastIndexOf('.');
2329
+ if (dot < 0)
2330
+ return;
2331
+ const suffix = eventName.slice(dot + 1).toLowerCase();
2332
+ if (!MODIFIER_WORDS.has(suffix))
2333
+ return;
2334
+ console.warn(`snice: "@${eventName}" is not a valid modifier — once/preventDefault/` +
2335
+ `stopPropagation/capture are @on() options, not template modifiers. ` +
2336
+ `This registers a listener for event "${eventName}", which never fires.`);
2337
+ }
2166
2338
  function parseKeyboardFilter(spec) {
2167
2339
  // Handle ~ prefix for matching regardless of modifiers
2168
2340
  const anyModifiers = spec.startsWith('~');
@@ -2265,9 +2437,14 @@ var Snice = (function (exports) {
2265
2437
  // Insert markers where <if> is
2266
2438
  parent.insertBefore(this.startNode, ifElement);
2267
2439
  parent.insertBefore(this.endNode, ifElement.nextSibling);
2268
- // Move <if>'s children between the markers
2440
+ // Park <if>'s children in an off-DOM fragment immediately, rather than
2441
+ // leaving them in the live template fragment. Otherwise a branch that is
2442
+ // hidden on first render would still connect its children (when the
2443
+ // template mounts) and then disconnect them (on the first commit) — firing
2444
+ // mount side effects spuriously. commit(true) inserts them when shown.
2445
+ this.childFragment = document.createDocumentFragment();
2269
2446
  while (ifElement.firstChild) {
2270
- parent.insertBefore(ifElement.firstChild, this.endNode);
2447
+ this.childFragment.appendChild(ifElement.firstChild);
2271
2448
  }
2272
2449
  // Remove the <if> element from DOM
2273
2450
  parent.removeChild(ifElement);
@@ -2348,16 +2525,24 @@ var Snice = (function (exports) {
2348
2525
  parent.removeChild(caseElement);
2349
2526
  }
2350
2527
  commit(value) {
2351
- if (this.value === value)
2528
+ // Dirty-check on the SELECTED BRANCH, not the raw value. A value whose type
2529
+ // changed but stringifies the same (number 1 vs string '1' — e.g. a numeric
2530
+ // default replaced by a router param, which is always a string) selects the
2531
+ // same branch, so tearing it down and re-inserting it would needlessly
2532
+ // restart media/animations and lose focus inside it.
2533
+ const valueStr = String(value);
2534
+ const hasBranch = this.branches.has(valueStr);
2535
+ const nextKey = hasBranch ? valueStr : (this.defaultBranch ? '__default__' : null);
2536
+ if (this.currentKey === nextKey) {
2537
+ this.value = value;
2352
2538
  return;
2539
+ }
2353
2540
  this.value = value;
2354
- const valueStr = String(value);
2355
2541
  // Move current content back to its fragment
2356
2542
  this._collectCurrent();
2357
2543
  // Insert matching branch
2358
- const hasBranch = this.branches.has(valueStr);
2359
2544
  const matchingFragment = hasBranch ? this.branches.get(valueStr) : this.defaultBranch;
2360
- this.currentKey = hasBranch ? valueStr : (this.defaultBranch ? '__default__' : null);
2545
+ this.currentKey = nextKey;
2361
2546
  if (matchingFragment && matchingFragment.hasChildNodes()) {
2362
2547
  this.startNode.parentNode.insertBefore(matchingFragment, this.endNode);
2363
2548
  }
@@ -2535,7 +2720,9 @@ var Snice = (function (exports) {
2535
2720
  // Supports both dot notation (@keydown.enter) and colon notation (@keydown:Enter)
2536
2721
  // Only parse colons for keyboard events, not custom events
2537
2722
  const isKeyboardEvent = ['keydown', 'keyup', 'keypress'].includes(handler.eventName.split('.')[0].split(':')[0]);
2538
- const dotIndex = handler.eventName.indexOf('.');
2723
+ // Only keyboard events split on `.`/`:` into a key filter — a custom event
2724
+ // name may legitimately contain a dot (e.g. `app.ready`) and must be kept whole.
2725
+ const dotIndex = isKeyboardEvent ? handler.eventName.indexOf('.') : -1;
2539
2726
  const colonIndex = isKeyboardEvent ? handler.eventName.indexOf(':') : -1;
2540
2727
  const delimiterIndex = dotIndex > 0 && colonIndex > 0
2541
2728
  ? Math.min(dotIndex, colonIndex)
@@ -2546,6 +2733,8 @@ var Snice = (function (exports) {
2546
2733
  const keyModifier = delimiterIndex > 0
2547
2734
  ? handler.eventName.substring(delimiterIndex + 1)
2548
2735
  : null;
2736
+ if (delimiterIndex <= 0)
2737
+ warnIfModifierMisuse(handler.eventName);
2549
2738
  // Apply debounce (takes precedence over throttle)
2550
2739
  if (handlerOptions.debounce && handlerOptions.debounce > 0) {
2551
2740
  boundMethod = createDebounced(boundMethod, handlerOptions.debounce);
@@ -2572,10 +2761,27 @@ var Snice = (function (exports) {
2572
2761
  };
2573
2762
  // Apply key modifier wrapper
2574
2763
  const keyModifierMethod = createKeyModifierHandler(boundMethod);
2764
+ // Resolve scope: explicit `scope` option redirects the listener target
2765
+ // to document / ancestor / arbitrary EventTarget. Default = host element.
2766
+ const hasExplicitScope = handlerOptions.scope !== undefined;
2767
+ let scopedTarget = null;
2768
+ if (hasExplicitScope) {
2769
+ scopedTarget = resolveScope(targetElement, handlerOptions.scope);
2770
+ if (!scopedTarget) {
2771
+ // Dev warning — listener silently dropped when scope cannot resolve.
2772
+ // Skip attachment so we don't bind to the wrong target.
2773
+ console.warn(`[snice/@on] scope did not resolve for "${handler.eventName}" on ${handler.methodName} — listener skipped.`);
2774
+ continue;
2775
+ }
2776
+ }
2575
2777
  // Main event handler with error handling and event delegation
2576
2778
  if (handler.selector) {
2577
- // Delegated event handling - use shadow root if available
2578
- const eventRoot = targetElement.shadowRoot || targetElement;
2779
+ // Delegated event handling.
2780
+ // - Default eventRoot: shadow root if present, else host element.
2781
+ // - With explicit scope: attach to the scoped target instead.
2782
+ const eventRoot = hasExplicitScope
2783
+ ? scopedTarget
2784
+ : (targetElement.shadowRoot || targetElement);
2579
2785
  const delegatedHandler = (event) => {
2580
2786
  const target = event.target;
2581
2787
  const matchingElement = (target.matches && target.matches(handler.selector) && target) ||
@@ -2615,10 +2821,11 @@ var Snice = (function (exports) {
2615
2821
  });
2616
2822
  }
2617
2823
  else {
2618
- // Direct event handling - on the element itself
2619
- // If element has shadow root, listen on both shadow root AND host element
2620
- // to catch events from inside shadow DOM (with correct target) and on host itself.
2621
- const shadowRoot = targetElement.shadowRoot;
2824
+ // Direct event handling.
2825
+ // - Default: shadow root + host element (so events inside shadow and on
2826
+ // the host itself both fire). A per-handler Symbol dedupes.
2827
+ // - With explicit scope: attach to the scoped target ONLY (no duplication).
2828
+ const shadowRoot = hasExplicitScope ? null : targetElement.shadowRoot;
2622
2829
  // Per-handler private Symbol so dedup is scoped to THIS handler's two
2623
2830
  // listeners (shadowRoot + host). Using Symbol.for() with the method name
2624
2831
  // would collide across components that share a method name (e.g. a parent
@@ -2646,6 +2853,16 @@ var Snice = (function (exports) {
2646
2853
  once: handlerOptions.once || false,
2647
2854
  passive: handlerOptions.passive || false,
2648
2855
  };
2856
+ if (hasExplicitScope) {
2857
+ scopedTarget.addEventListener(baseEventName, wrappedMethod, listenerOptions);
2858
+ instance[CLEANUP].events.push({
2859
+ target: scopedTarget,
2860
+ eventName: baseEventName,
2861
+ handler: wrappedMethod,
2862
+ options: listenerOptions,
2863
+ });
2864
+ continue;
2865
+ }
2649
2866
  if (shadowRoot) {
2650
2867
  // Listen on shadow root for events inside shadow DOM
2651
2868
  shadowRoot.addEventListener(baseEventName, wrappedMethod, listenerOptions);
@@ -2808,11 +3025,25 @@ var Snice = (function (exports) {
2808
3025
  }),
2809
3026
  ]);
2810
3027
  }
3028
+ catch (error) {
3029
+ // Attach never got past `ready` (aborted or deadline). Roll back the stored
3030
+ // references so nothing is left pointing at a controller that never
3031
+ // attached — otherwise a later attach short-circuits as "already attached"
3032
+ // and a detach would run detach() on it.
3033
+ if (element[CONTROLLER_KEY] === controllerInstance) {
3034
+ delete element[CONTROLLER_KEY];
3035
+ delete element[CONTROLLER_NAME_KEY];
3036
+ delete element[CONTROLLER_OPERATIONS];
3037
+ }
3038
+ throw error;
3039
+ }
2811
3040
  finally {
2812
3041
  if (element[CONTROLLER_ABORT] === abort) {
2813
3042
  delete element[CONTROLLER_ABORT];
2814
3043
  }
2815
3044
  }
3045
+ // Past `ready` — mark the controller so detach knows attach was actually run.
3046
+ controllerInstance[CONTROLLER_ATTACHED] = true;
2816
3047
  // Run attach in the controller's scope
2817
3048
  await scope.runOperation(async () => {
2818
3049
  await controllerInstance.attach(element);
@@ -2843,6 +3074,19 @@ var Snice = (function (exports) {
2843
3074
  if (!controllerInstance) {
2844
3075
  return;
2845
3076
  }
3077
+ // Claim the controller immediately — before any await — so a second detach
3078
+ // that overlaps this one (e.g. disconnect's fire-and-forget detach racing a
3079
+ // controller reassignment) finds no instance and returns, instead of running
3080
+ // detach() a second time on the same controller. The rest of this function
3081
+ // works off the locals captured above.
3082
+ delete element[CONTROLLER_KEY];
3083
+ delete element[CONTROLLER_NAME_KEY];
3084
+ delete element[CONTROLLER_OPERATIONS];
3085
+ // A controller whose attach() was aborted before it ever ran must not have
3086
+ // detach() (or the teardown/event) run on it.
3087
+ if (!controllerInstance[CONTROLLER_ATTACHED]) {
3088
+ return;
3089
+ }
2846
3090
  // Run detach in the controller's scope
2847
3091
  if (scope) {
2848
3092
  await scope.runOperation(async () => {
@@ -2865,9 +3109,6 @@ var Snice = (function (exports) {
2865
3109
  }
2866
3110
  // Clean up router context reference
2867
3111
  delete controllerInstance[ROUTER_CONTEXT];
2868
- delete element[CONTROLLER_KEY];
2869
- delete element[CONTROLLER_NAME_KEY];
2870
- delete element[CONTROLLER_OPERATIONS];
2871
3112
  element.dispatchEvent(new CustomEvent('controller-detached', {
2872
3113
  detail: { name: controllerName, controller: controllerInstance }
2873
3114
  }));
@@ -3030,19 +3271,23 @@ var Snice = (function (exports) {
3030
3271
  const wrappedMethodName = `__wrapped_${methodName}`;
3031
3272
  // Create wrapped method with timing controls
3032
3273
  element[wrappedMethodName] = function (context) {
3033
- // Skip if already called once
3034
- if (options.once && element[CONTEXT_CALLED]) {
3274
+ // `once` is tracked PER method (not a single element-level flag) so
3275
+ // multiple once handlers on the same element each fire.
3276
+ const called = element[CONTEXT_CALLED] ||
3277
+ (element[CONTEXT_CALLED] = new Set());
3278
+ if (options.once && called.has(methodName)) {
3035
3279
  return;
3036
3280
  }
3037
3281
  const callMethod = () => {
3038
3282
  method.call(element, context);
3039
3283
  // Handle once option
3040
3284
  if (options.once) {
3041
- element[CONTEXT_CALLED] = true;
3042
- // Unregister after first call
3285
+ called.add(methodName);
3286
+ // Unregister ONLY this handler, not the whole element — otherwise the
3287
+ // element's other @context handlers would stop receiving updates.
3043
3288
  const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
3044
3289
  if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
3045
- ctx[CONTEXT_UNREGISTER](element);
3290
+ ctx[CONTEXT_UNREGISTER](element, wrappedMethodName);
3046
3291
  }
3047
3292
  }
3048
3293
  };
@@ -3145,9 +3390,14 @@ var Snice = (function (exports) {
3145
3390
  this.pending.clear();
3146
3391
  this.scheduled = false;
3147
3392
  for (const element of elements) {
3148
- // Skip elements that were disconnected between scheduling and flush.
3149
- if (!element.isConnected)
3393
+ // Skip elements that were disconnected between scheduling and flush, but
3394
+ // remember the dropped render so reconnect can replay it — otherwise the
3395
+ // shadow DOM stays stale for a property changed while detached. The flag
3396
+ // lives on the element, so it's GC'd with it (no leak if never reattached).
3397
+ if (!element.isConnected) {
3398
+ element[PENDING_RECONNECT_RENDER] = true;
3150
3399
  continue;
3400
+ }
3151
3401
  const options = element[RENDER_OPTIONS] || {};
3152
3402
  performRender(element, options);
3153
3403
  }
@@ -3166,6 +3416,19 @@ var Snice = (function (exports) {
3166
3416
  element[RENDER_CALLBACKS] = [];
3167
3417
  cbs.forEach(cb => cb());
3168
3418
  }
3419
+ /**
3420
+ * Backstop against a render() that mutates an observed property, which makes
3421
+ * the property setter request another render from inside the current one. In
3422
+ * `sync` mode that re-enters performRender synchronously and, unchecked,
3423
+ * recurses to a stack overflow. The counter caps synchronous nesting and turns
3424
+ * a silent crash into an actionable error. Normal renders never nest (child
3425
+ * renders defer to a microtask), so this stays at depth 1 in the common case.
3426
+ *
3427
+ * The depth is tracked PER ELEMENT (on RENDER_DEPTH), not globally: a runaway
3428
+ * component must not consume a shared budget and reject an unrelated component
3429
+ * that happens to render inside its call stack.
3430
+ */
3431
+ const MAX_RENDER_DEPTH = 50;
3169
3432
  /**
3170
3433
  * Perform the actual render of an element
3171
3434
  */
@@ -3177,6 +3440,14 @@ var Snice = (function (exports) {
3177
3440
  if (options.once && element[RENDER_INSTANCE]) {
3178
3441
  return;
3179
3442
  }
3443
+ const depth = (element[RENDER_DEPTH] ?? 0);
3444
+ if (depth >= MAX_RENDER_DEPTH) {
3445
+ const tag = element.tagName ? element.tagName.toLowerCase() : 'element';
3446
+ console.error(`snice: maximum render depth (${MAX_RENDER_DEPTH}) exceeded for <${tag}>. ` +
3447
+ `render() is mutating an observed property, causing an infinite render loop.`);
3448
+ return;
3449
+ }
3450
+ element[RENDER_DEPTH] = depth + 1;
3180
3451
  try {
3181
3452
  const result = precomputedResult !== undefined ? precomputedResult : renderMethod.call(element);
3182
3453
  if (!element.shadowRoot)
@@ -3202,14 +3473,18 @@ var Snice = (function (exports) {
3202
3473
  flushRenderCallbacks(element);
3203
3474
  return;
3204
3475
  }
3205
- // Different template or first render. Remove only non-<style> children so
3206
- // the fallback @styles path (Safari <=16 / jsdom / SSR — no
3207
- // adoptedStyleSheets) doesn't lose its style tags on template switch.
3476
+ // Different template or first render. Keep only the framework's own
3477
+ // fallback <style> tags (marked data-snice-style) so the @styles path
3478
+ // (Safari <=16 / jsdom / SSR no adoptedStyleSheets) survives a template
3479
+ // switch; template-emitted <style> tags are removed and re-created by the
3480
+ // new instance, so they don't accumulate.
3208
3481
  if (instance) {
3209
3482
  const root = element.shadowRoot;
3210
3483
  const toRemove = [];
3211
3484
  for (const child of Array.from(root.childNodes)) {
3212
- if (child.nodeType === 1 && child.tagName === 'STYLE')
3485
+ if (child.nodeType === 1 &&
3486
+ child.tagName === 'STYLE' &&
3487
+ child.hasAttribute('data-snice-style'))
3213
3488
  continue;
3214
3489
  toRemove.push(child);
3215
3490
  }
@@ -3225,6 +3500,9 @@ var Snice = (function (exports) {
3225
3500
  catch (error) {
3226
3501
  console.error('Error rendering element:', error);
3227
3502
  }
3503
+ finally {
3504
+ element[RENDER_DEPTH] = depth;
3505
+ }
3228
3506
  }
3229
3507
  /**
3230
3508
  * Request a render for an element
@@ -3278,6 +3556,33 @@ var Snice = (function (exports) {
3278
3556
  // Normal rendering (with microtask batching unless sync)
3279
3557
  renderScheduler.schedule(element, options);
3280
3558
  }
3559
+ /**
3560
+ * Clear any pending debounce/throttle render timers on an element (e.g. on
3561
+ * disconnect, so they don't fire on a dead element and retain it until they
3562
+ * expire). Returns true if a render was actually pending, so the caller can
3563
+ * arrange a replay on reconnect via PENDING_RECONNECT_RENDER.
3564
+ */
3565
+ function clearRenderTimers(element) {
3566
+ const timers = element[RENDER_TIMERS];
3567
+ if (!timers)
3568
+ return false;
3569
+ let hadPending = false;
3570
+ if (timers.debounce) {
3571
+ clearTimeout(timers.debounce);
3572
+ timers.debounce = null;
3573
+ hadPending = true;
3574
+ }
3575
+ if (timers.throttleTimer) {
3576
+ clearTimeout(timers.throttleTimer);
3577
+ timers.throttleTimer = null;
3578
+ hadPending = true;
3579
+ }
3580
+ // Deliberately do NOT reset timers.lastThrottle: a plain DOM move is a
3581
+ // disconnect+reconnect, and zeroing the cooldown would let the next render
3582
+ // fire inside the throttle window. Preserving it keeps throttle honest
3583
+ // across moves; on a true disconnect the element is discarded anyway.
3584
+ return hadPending;
3585
+ }
3281
3586
  /**
3282
3587
  * @render decorator for component rendering
3283
3588
  *
@@ -3400,9 +3705,12 @@ var Snice = (function (exports) {
3400
3705
  element.shadowRoot.adoptedStyleSheets = allResults.map(r => r.styleSheet);
3401
3706
  return;
3402
3707
  }
3403
- // Fallback — one <style> tag per stylesheet, preserving cascade order
3708
+ // Fallback — one <style> tag per stylesheet, preserving cascade order.
3709
+ // Marked so the template-switch cleanup keeps these (framework styles) but
3710
+ // still removes template-emitted <style> tags (which would otherwise pile up).
3404
3711
  for (const r of allResults) {
3405
3712
  const style = document.createElement('style');
3713
+ style.setAttribute('data-snice-style', '');
3406
3714
  style.textContent = r.cssText;
3407
3715
  element.shadowRoot.appendChild(style);
3408
3716
  }
@@ -3412,6 +3720,125 @@ var Snice = (function (exports) {
3412
3720
  }
3413
3721
  }
3414
3722
 
3723
+ // @dispatch decorator - auto-dispatches custom events from method return values
3724
+ /**
3725
+ * Decorator that automatically dispatches a custom event after a method is called.
3726
+ * The return value of the method becomes the event detail.
3727
+ *
3728
+ * @param eventName The name of the event to dispatch
3729
+ * @param options Optional configuration extending EventInit
3730
+ */
3731
+ function dispatch(eventName, options) {
3732
+ return function (originalMethod, _context) {
3733
+ return function (...args) {
3734
+ // Create timing wrappers for dispatch (per-instance)
3735
+ if (!this[DISPATCH_TIMERS]) {
3736
+ this[DISPATCH_TIMERS] = new Map();
3737
+ }
3738
+ const timerKey = `${eventName}_${_context.name}`;
3739
+ if (!this[DISPATCH_TIMERS].has(timerKey)) {
3740
+ this[DISPATCH_TIMERS].set(timerKey, {
3741
+ debounceTimeout: null,
3742
+ throttleLastCall: 0,
3743
+ throttleTimeout: null
3744
+ });
3745
+ }
3746
+ const timers = this[DISPATCH_TIMERS].get(timerKey);
3747
+ // Call the original method with preserved this context
3748
+ const result = originalMethod.apply(this, args);
3749
+ // Helper to dispatch the event
3750
+ const doDispatch = (detail) => {
3751
+ // Skip dispatch if result is undefined and dispatchOnUndefined is false
3752
+ if (detail === undefined && options?.dispatchOnUndefined === false) {
3753
+ return;
3754
+ }
3755
+ // Create event with spread operator for options
3756
+ const event = new CustomEvent(eventName, {
3757
+ bubbles: true, // Default to true for component events
3758
+ composed: true, // Allow crossing shadow DOM boundaries
3759
+ ...options, // Spread all EventInit options
3760
+ detail
3761
+ });
3762
+ // Resolve scope target. Default is `this` (host element).
3763
+ // Explicit scope dispatches the event on another target — useful for
3764
+ // 'global' bus events on document, or routing through an ancestor.
3765
+ if (options?.scope !== undefined) {
3766
+ const target = resolveScope(this, options.scope);
3767
+ if (!target) {
3768
+ console.warn(`[snice/@dispatch] scope did not resolve for "${eventName}" — event not dispatched.`);
3769
+ return;
3770
+ }
3771
+ target.dispatchEvent(event);
3772
+ return;
3773
+ }
3774
+ this.dispatchEvent(event);
3775
+ };
3776
+ // Helper to handle timed dispatch
3777
+ const timedDispatch = (detail) => {
3778
+ if (options?.debounce) {
3779
+ clearTimeout(timers.debounceTimeout);
3780
+ timers.debounceTimeout = setTimeout(() => doDispatch(detail), options.debounce);
3781
+ return;
3782
+ }
3783
+ if (!options?.throttle) {
3784
+ doDispatch(detail);
3785
+ return;
3786
+ }
3787
+ const now = Date.now();
3788
+ const remaining = options.throttle - (now - timers.throttleLastCall);
3789
+ if (remaining <= 0) {
3790
+ clearTimeout(timers.throttleTimeout);
3791
+ timers.throttleLastCall = now;
3792
+ doDispatch(detail);
3793
+ return;
3794
+ }
3795
+ // Record the LATEST detail so the trailing dispatch carries fresh
3796
+ // data, not the first-suppressed-call detail captured by closure.
3797
+ timers.latestDetail = detail;
3798
+ if (!timers.throttleTimeout) {
3799
+ timers.throttleTimeout = setTimeout(() => {
3800
+ timers.throttleLastCall = Date.now();
3801
+ timers.throttleTimeout = null;
3802
+ const d = timers.latestDetail;
3803
+ timers.latestDetail = undefined;
3804
+ doDispatch(d);
3805
+ }, remaining);
3806
+ }
3807
+ };
3808
+ // Handle async methods
3809
+ if (result instanceof Promise) {
3810
+ return result.then((resolvedResult) => {
3811
+ timedDispatch(resolvedResult);
3812
+ return resolvedResult;
3813
+ });
3814
+ }
3815
+ // Sync method
3816
+ timedDispatch(result);
3817
+ return result;
3818
+ };
3819
+ };
3820
+ }
3821
+ /**
3822
+ * Clear any pending debounce/throttle dispatch timers on an instance (e.g. on
3823
+ * disconnect, so a queued event doesn't fire into a detached node). A dispatch
3824
+ * is a one-shot signal, so pending ones are dropped, not replayed.
3825
+ */
3826
+ function clearDispatchTimers(instance) {
3827
+ const timers = instance[DISPATCH_TIMERS];
3828
+ if (!timers)
3829
+ return;
3830
+ for (const t of timers.values()) {
3831
+ if (t.debounceTimeout)
3832
+ clearTimeout(t.debounceTimeout);
3833
+ if (t.throttleTimeout)
3834
+ clearTimeout(t.throttleTimeout);
3835
+ t.debounceTimeout = null;
3836
+ t.throttleTimeout = null;
3837
+ t.throttleLastCall = 0;
3838
+ t.latestDetail = undefined;
3839
+ }
3840
+ }
3841
+
3415
3842
  /**
3416
3843
  * Method decorators for common patterns
3417
3844
  * @debounce, @throttle, @once, @memoize
@@ -3838,6 +4265,14 @@ var Snice = (function (exports) {
3838
4265
  }
3839
4266
  }
3840
4267
  }
4268
+ // Replay a render that was dropped while disconnected. The scheduler
4269
+ // sets this flag when it skips a detached element, so a property changed
4270
+ // while detached still reaches the DOM on reattach. A plain move (no
4271
+ // pending render) leaves the flag unset, so we don't re-render needlessly.
4272
+ if (this[RENDER_METHOD] && this[PENDING_RECONNECT_RENDER]) {
4273
+ this[PENDING_RECONNECT_RENDER] = false;
4274
+ requestRender(this);
4275
+ }
3841
4276
  return;
3842
4277
  }
3843
4278
  // Mark that properties are being initialized from attributes
@@ -3885,6 +4320,9 @@ var Snice = (function (exports) {
3885
4320
  setupResponseHandlers(this, this);
3886
4321
  setupContextHandler(this);
3887
4322
  this[INITIALIZED] = true;
4323
+ // Now that the initial value is settled, give @watch immediate handlers
4324
+ // their one init call (before the first render, so derived state is ready).
4325
+ invokeImmediateWatchers(this, constructor);
3888
4326
  if (originalConnectedCallback) {
3889
4327
  originalConnectedCallback.call(this);
3890
4328
  }
@@ -3979,6 +4417,17 @@ var Snice = (function (exports) {
3979
4417
  // Cleanup pending @debounce / @throttle timers so they don't fire on a dead element
3980
4418
  clearDebounceTimers(this);
3981
4419
  clearThrottleTimers(this);
4420
+ // The render/dispatch debounce-throttle timers live in separate stores.
4421
+ // A pending render is replayed on reconnect (same flag the scheduler
4422
+ // uses); a pending dispatch is a one-shot signal, so it's dropped.
4423
+ if (clearRenderTimers(this)) {
4424
+ this[PENDING_RECONNECT_RENDER] = true;
4425
+ }
4426
+ clearDispatchTimers(this);
4427
+ // @moved / @adopted debounce-throttle timers, likewise dropped so they
4428
+ // don't fire on a dead element.
4429
+ clearLifecycleTimers(this, MOVED_TIMERS);
4430
+ clearLifecycleTimers(this, ADOPTED_TIMERS);
3982
4431
  };
3983
4432
  constructor.prototype.attributeChangedCallback = function (name, oldValue, newValue) {
3984
4433
  originalAttributeChangedCallback?.call(this, name, oldValue, newValue);
@@ -3994,13 +4443,27 @@ var Snice = (function (exports) {
3994
4443
  if (attributeName.toLowerCase() !== name.toLowerCase())
3995
4444
  continue;
3996
4445
  const currentValue = this[PROPERTY_VALUES]?.[propName];
3997
- const parsedValue = parseAttributeValue(newValue, propOptions, currentValue, undefined);
3998
- if (currentValue === parsedValue)
4446
+ // A removed attribute reverts the property to its field default. Read it
4447
+ // back through the getter (attribute is already gone at this point) so the
4448
+ // watcher's newValue and PROPERTY_VALUES match what this[propName] now
4449
+ // returns — parseAttributeValue(null) would diverge to null for String/Number.
4450
+ const parsedValue = newValue === null
4451
+ ? this[propName]
4452
+ : parseAttributeValue(newValue, propOptions, currentValue, undefined);
4453
+ const changed = propOptions?.hasChanged
4454
+ ? propOptions.hasChanged(parsedValue, currentValue)
4455
+ : notEqual(parsedValue, currentValue);
4456
+ if (!changed)
3999
4457
  break;
4000
4458
  ensureSet(this, EXPLICITLY_SET_PROPERTIES).add(propName);
4001
4459
  ensureObj(this, PROPERTY_VALUES)[propName] = parsedValue;
4002
4460
  if (!this[SETTING_FROM_PROPERTY]?.has(name.toLowerCase())) {
4003
- invokeWatchers(this, constructor, propName, currentValue, parsedValue);
4461
+ // Watchers react to changes only. During upgrade the initial value
4462
+ // is not a change, so suppress until INITIALIZED; @watch immediate
4463
+ // handlers get their one init call from invokeImmediateWatchers.
4464
+ if (this[INITIALIZED]) {
4465
+ invokeWatchers(this, constructor, propName, currentValue, parsedValue);
4466
+ }
4004
4467
  if (this[RENDER_METHOD] && this[INITIALIZED]) {
4005
4468
  requestRender(this);
4006
4469
  }
@@ -4010,12 +4473,14 @@ var Snice = (function (exports) {
4010
4473
  };
4011
4474
  // Add connectedMoveCallback for handling DOM moves
4012
4475
  constructor.prototype.connectedMoveCallback = async function () {
4013
- // Call @moved handlers
4476
+ // Call @moved handlers via the prototype method (the debounce/throttle
4477
+ // wrapper), not the raw handler — otherwise @moved({debounce}) options
4478
+ // are ignored for real moves and only work when called directly.
4014
4479
  const movedHandlers = constructor[MOVED_HANDLERS];
4015
4480
  if (movedHandlers) {
4016
4481
  for (const handler of movedHandlers) {
4017
4482
  try {
4018
- await handler.method.call(this);
4483
+ await this[handler.methodName]();
4019
4484
  }
4020
4485
  catch (error) {
4021
4486
  console.error(`Error in @moved handler ${handler.methodName}:`, error);
@@ -4025,12 +4490,13 @@ var Snice = (function (exports) {
4025
4490
  };
4026
4491
  // Add adoptedCallback for handling document adoption
4027
4492
  constructor.prototype.adoptedCallback = async function () {
4028
- // Call @adopted handlers
4493
+ // Call @adopted handlers via the prototype method (the debounce/throttle
4494
+ // wrapper), not the raw handler — same reason as connectedMoveCallback.
4029
4495
  const adoptedHandlers = constructor[ADOPTED_HANDLERS];
4030
4496
  if (adoptedHandlers) {
4031
4497
  for (const handler of adoptedHandlers) {
4032
4498
  try {
4033
- await handler.method.call(this);
4499
+ await this[handler.methodName]();
4034
4500
  }
4035
4501
  catch (error) {
4036
4502
  console.error(`Error in @adopted handler ${handler.methodName}:`, error);
@@ -4195,7 +4661,12 @@ var Snice = (function (exports) {
4195
4661
  },
4196
4662
  set(newValue) {
4197
4663
  const oldValue = this[propertyKey];
4198
- if (oldValue === newValue)
4664
+ // A custom hasChanged comparator overrides the default === check
4665
+ // (e.g. deep-equal objects, or forcing an update on a mutated ref).
4666
+ const changed = finalOptions?.hasChanged
4667
+ ? finalOptions.hasChanged(newValue, oldValue)
4668
+ : notEqual(newValue, oldValue);
4669
+ if (!changed)
4199
4670
  return;
4200
4671
  // Pre-init: store for later, don't reflect to DOM yet
4201
4672
  if (!this[PROPERTIES_INITIALIZED]) {
@@ -4212,18 +4683,27 @@ var Snice = (function (exports) {
4212
4683
  const attributeName = getAttrName(finalOptions, propertyKey);
4213
4684
  const attributeValue = valueToAttribute(newValue, finalOptions, initialValue);
4214
4685
  ensureSet(this, EXPLICITLY_SET_PROPERTIES).add(propertyKey);
4215
- ensureSet(this, SETTING_FROM_PROPERTY).add(attributeName.toLowerCase());
4686
+ const attrKey = attributeName.toLowerCase();
4687
+ ensureSet(this, SETTING_FROM_PROPERTY).add(attrKey);
4688
+ // attributeChangedCallback runs synchronously inside setAttribute /
4689
+ // removeAttribute (custom element reactions are synchronous), so the
4690
+ // guard only needs to bracket this one reflection. Clear it right
4691
+ // after — not on a microtask — otherwise an external attribute change
4692
+ // in the same tick is mistaken for our own echo and silently dropped.
4216
4693
  if (attributeValue === null) {
4217
4694
  this.removeAttribute?.(attributeName);
4218
4695
  }
4219
4696
  else {
4220
4697
  this.setAttribute?.(attributeName, attributeValue);
4221
4698
  }
4222
- queueMicrotask(() => {
4223
- this[SETTING_FROM_PROPERTY]?.delete(attributeName.toLowerCase());
4224
- });
4699
+ this[SETTING_FROM_PROPERTY].delete(attrKey);
4700
+ }
4701
+ // See attributeChangedCallback: the initial value is not a change,
4702
+ // so suppress watchers until INITIALIZED. @watch immediate handlers
4703
+ // get their one init call from invokeImmediateWatchers.
4704
+ if (this[INITIALIZED]) {
4705
+ invokeWatchers(this, this.constructor, propertyKey, oldValue, newValue);
4225
4706
  }
4226
- invokeWatchers(this, this.constructor, propertyKey, oldValue, newValue);
4227
4707
  if (this[RENDER_METHOD] && this[INITIALIZED]) {
4228
4708
  requestRender(this);
4229
4709
  }
@@ -4293,7 +4773,12 @@ var Snice = (function (exports) {
4293
4773
  };
4294
4774
  };
4295
4775
  }
4296
- function watch(...propertyNames) {
4776
+ function watch(...args) {
4777
+ // A trailing options object is separated from the watched property names.
4778
+ const propertyNames = args.filter((a) => typeof a === 'string');
4779
+ const options = args.find((a) => typeof a === 'object' && a !== null);
4780
+ // Fire on init by default; opt out of the init call with { immediate: false }.
4781
+ const immediate = options?.immediate !== false;
4297
4782
  return function (target, context) {
4298
4783
  const methodName = context.name;
4299
4784
  context.addInitializer(function () {
@@ -4333,7 +4818,8 @@ var Snice = (function (exports) {
4333
4818
  }
4334
4819
  constructor[PROPERTY_WATCHERS].get(propertyName).push({
4335
4820
  methodName,
4336
- method: target
4821
+ method: target,
4822
+ immediate
4337
4823
  });
4338
4824
  }
4339
4825
  });
@@ -4442,6 +4928,24 @@ var Snice = (function (exports) {
4442
4928
  registerHandler(RECONNECT_HANDLERS, RECONNECT_METHODS, target, context);
4443
4929
  };
4444
4930
  }
4931
+ /**
4932
+ * Clear any pending @moved / @adopted debounce/throttle timers on an instance
4933
+ * (e.g. on disconnect), so they don't fire on a dead element.
4934
+ */
4935
+ function clearLifecycleTimers(instance, timersSymbol) {
4936
+ const map = instance[timersSymbol];
4937
+ if (!map)
4938
+ return;
4939
+ for (const t of map.values()) {
4940
+ if (t.debounceTimer)
4941
+ clearTimeout(t.debounceTimer);
4942
+ if (t.throttleTimer)
4943
+ clearTimeout(t.throttleTimer);
4944
+ t.debounceTimer = null;
4945
+ t.throttleTimer = null;
4946
+ t.lastThrottleCall = 0;
4947
+ }
4948
+ }
4445
4949
  function createLifecycleDecorator(handlersSymbol, timersSymbol, methodsSymbol) {
4446
4950
  return function (options = {}) {
4447
4951
  return function (originalMethod, context) {
@@ -4455,9 +4959,23 @@ var Snice = (function (exports) {
4455
4959
  }
4456
4960
  const timers = this[timersSymbol].get(methodName);
4457
4961
  const exec = (...a) => originalMethod.apply(this, a);
4962
+ // Deferred (debounce/throttle) calls run inside a setTimeout, outside
4963
+ // the caller's try/catch — so catch their sync throws and async
4964
+ // rejections here and log, rather than surface an uncaught error.
4965
+ const execDeferred = (...a) => {
4966
+ try {
4967
+ const result = originalMethod.apply(this, a);
4968
+ if (result && typeof result.then === 'function') {
4969
+ result.catch((error) => console.error(`Error in deferred lifecycle handler ${methodName}:`, error));
4970
+ }
4971
+ }
4972
+ catch (error) {
4973
+ console.error(`Error in deferred lifecycle handler ${methodName}:`, error);
4974
+ }
4975
+ };
4458
4976
  if (options.debounce > 0) {
4459
4977
  clearTimeout(timers.debounceTimer);
4460
- timers.debounceTimer = setTimeout(() => exec(...args), options.debounce);
4978
+ timers.debounceTimer = setTimeout(() => execDeferred(...args), options.debounce);
4461
4979
  return undefined;
4462
4980
  }
4463
4981
  if (options.throttle > 0) {
@@ -4471,7 +4989,7 @@ var Snice = (function (exports) {
4471
4989
  timers.throttleTimer = setTimeout(() => {
4472
4990
  timers.throttleTimer = null;
4473
4991
  timers.lastThrottleCall = Date.now();
4474
- exec(...args);
4992
+ execDeferred(...args);
4475
4993
  }, remaining);
4476
4994
  }
4477
4995
  return undefined;
@@ -5103,6 +5621,40 @@ var Snice = (function (exports) {
5103
5621
  }
5104
5622
  }
5105
5623
 
5624
+ /**
5625
+ * Score a route pattern by specificity so that more specific routes are tried
5626
+ * first. Scoring is per-segment by ROLE, not by string length: a static segment
5627
+ * outweighs a dynamic (`:param`) segment, which outweighs a wildcard (`*` /
5628
+ * `*name`). So `/post/new` beats `/post/:id`, and a splat always ranks last —
5629
+ * regardless of how long the pattern string happens to be. This mirrors the
5630
+ * segment-scoring model mainstream routers (React Router, Remix) use.
5631
+ *
5632
+ * Sort callers descending by this score (highest = most specific = first).
5633
+ *
5634
+ * NOTE: mirrored in `src/react/route-specificity.ts` for the isolated React
5635
+ * bundle — keep the two in sync.
5636
+ */
5637
+ const STATIC_SEGMENT = 10;
5638
+ const DYNAMIC_SEGMENT = 3;
5639
+ const WILDCARD_SEGMENT = -2;
5640
+ function routeSpecificity(spec) {
5641
+ const segments = spec.split('/').filter(Boolean);
5642
+ // Base on segment count so a deeper path outranks a shallower one on ties.
5643
+ let score = segments.length;
5644
+ for (const segment of segments) {
5645
+ if (segment.startsWith('*')) {
5646
+ score += WILDCARD_SEGMENT;
5647
+ }
5648
+ else if (segment.startsWith(':')) {
5649
+ score += DYNAMIC_SEGMENT;
5650
+ }
5651
+ else {
5652
+ score += STATIC_SEGMENT;
5653
+ }
5654
+ }
5655
+ return score;
5656
+ }
5657
+
5106
5658
  /**
5107
5659
  * Generic transition system for animating between elements
5108
5660
  */
@@ -5370,7 +5922,7 @@ var Snice = (function (exports) {
5370
5922
  }
5371
5923
  const needsSorting = !is_sorted;
5372
5924
  if (needsSorting) {
5373
- routes.sort((a, b) => b.route.spec.length - a.route.spec.length);
5925
+ routes.sort((a, b) => routeSpecificity(b.route.spec) - routeSpecificity(a.route.spec));
5374
5926
  is_sorted = true;
5375
5927
  }
5376
5928
  // Collect placards from registered routes
@@ -5441,7 +5993,7 @@ var Snice = (function (exports) {
5441
5993
  </div>`;
5442
5994
  return container;
5443
5995
  }
5444
- async function checkGuards(guards, params, target) {
5996
+ async function checkGuards(guards, params, target, stale) {
5445
5997
  const hasGuards = !!guards;
5446
5998
  if (!hasGuards) {
5447
5999
  return true;
@@ -5449,8 +6001,11 @@ var Snice = (function (exports) {
5449
6001
  const guardsArray = Array.isArray(guards) ? guards : [guards];
5450
6002
  let spinner = null;
5451
6003
  let spinnerTimer = null;
5452
- // Show spinner after a short delay (avoids flash for fast sync guards)
6004
+ // Show spinner after a short delay (avoids flash for fast sync guards).
6005
+ // Skip if a newer navigation has already superseded this one.
5453
6006
  spinnerTimer = setTimeout(() => {
6007
+ if (stale && stale())
6008
+ return;
5454
6009
  spinner = createLoadingSpinner();
5455
6010
  target.appendChild(spinner);
5456
6011
  }, 50);
@@ -5458,7 +6013,11 @@ var Snice = (function (exports) {
5458
6013
  for (const guard of guardsArray) {
5459
6014
  const allowed = await guard(context, params);
5460
6015
  if (!allowed) {
5461
- renderForbiddenPage(target);
6016
+ // Don't stomp the current page with a 403 if a newer navigation
6017
+ // already took over while this guard was resolving.
6018
+ if (!stale || !stale()) {
6019
+ renderForbiddenPage(target);
6020
+ }
5462
6021
  return false;
5463
6022
  }
5464
6023
  }
@@ -5494,14 +6053,14 @@ var Snice = (function (exports) {
5494
6053
  div.innerHTML = /*html*/ `<h1>404</h1><p>Page not found</p>`;
5495
6054
  return { element: div, transition: undefined, layout: undefined };
5496
6055
  }
5497
- async function resolveRoute(path, target) {
6056
+ async function resolveRoute(path, target, stale) {
5498
6057
  for (const route of routes) {
5499
6058
  const params = route.route.match(path);
5500
6059
  const isMatch = params !== false;
5501
6060
  if (!isMatch) {
5502
6061
  continue;
5503
6062
  }
5504
- const guardsAllowed = await checkGuards(route.guards, params, target);
6063
+ const guardsAllowed = await checkGuards(route.guards, params, target, stale);
5505
6064
  if (!guardsAllowed) {
5506
6065
  return { result: RouteResult.GUARDS_FAILED };
5507
6066
  }
@@ -5629,7 +6188,7 @@ var Snice = (function (exports) {
5629
6188
  const isHomePath = (path?.trim() === '' || path === '/') && !!home;
5630
6189
  if (isHomePath) {
5631
6190
  const homeRoute = routes.find(r => r.route.match('/'));
5632
- if (!(await checkGuards(homeRoute?.guards, {}, target)))
6191
+ if (!(await checkGuards(homeRoute?.guards, {}, target, stale)))
5633
6192
  return;
5634
6193
  if (stale())
5635
6194
  return;
@@ -5641,7 +6200,7 @@ var Snice = (function (exports) {
5641
6200
  if (!path)
5642
6201
  return;
5643
6202
  // Resolve route
5644
- const routeResult = await resolveRoute(path, target);
6203
+ const routeResult = await resolveRoute(path, target, stale);
5645
6204
  if (stale())
5646
6205
  return;
5647
6206
  // Guards failed (403 already rendered by checkGuards)
@@ -5655,7 +6214,7 @@ var Snice = (function (exports) {
5655
6214
  }
5656
6215
  // 404 fallthrough
5657
6216
  const { element, transition, layout } = create404Element();
5658
- await renderPage(target, element, transition, layout, path, {});
6217
+ await renderPage(target, element, transition, layout, path, {}, stale);
5659
6218
  }
5660
6219
  async function performTransition$1(container, oldElement, newElement, transition, atGen = navGeneration) {
5661
6220
  // `isStale` returns true when a newer navigate() has been kicked off.
@@ -5672,93 +6231,6 @@ var Snice = (function (exports) {
5672
6231
  };
5673
6232
  }
5674
6233
 
5675
- // @dispatch decorator - auto-dispatches custom events from method return values
5676
- /**
5677
- * Decorator that automatically dispatches a custom event after a method is called.
5678
- * The return value of the method becomes the event detail.
5679
- *
5680
- * @param eventName The name of the event to dispatch
5681
- * @param options Optional configuration extending EventInit
5682
- */
5683
- function dispatch(eventName, options) {
5684
- return function (originalMethod, _context) {
5685
- return function (...args) {
5686
- // Create timing wrappers for dispatch (per-instance)
5687
- if (!this[DISPATCH_TIMERS]) {
5688
- this[DISPATCH_TIMERS] = new Map();
5689
- }
5690
- const timerKey = `${eventName}_${_context.name}`;
5691
- if (!this[DISPATCH_TIMERS].has(timerKey)) {
5692
- this[DISPATCH_TIMERS].set(timerKey, {
5693
- debounceTimeout: null,
5694
- throttleLastCall: 0,
5695
- throttleTimeout: null
5696
- });
5697
- }
5698
- const timers = this[DISPATCH_TIMERS].get(timerKey);
5699
- // Call the original method with preserved this context
5700
- const result = originalMethod.apply(this, args);
5701
- // Helper to dispatch the event
5702
- const doDispatch = (detail) => {
5703
- // Skip dispatch if result is undefined and dispatchOnUndefined is false
5704
- if (detail === undefined && options?.dispatchOnUndefined === false) {
5705
- return;
5706
- }
5707
- // Create event with spread operator for options
5708
- const event = new CustomEvent(eventName, {
5709
- bubbles: true, // Default to true for component events
5710
- composed: true, // Allow crossing shadow DOM boundaries
5711
- ...options, // Spread all EventInit options
5712
- detail
5713
- });
5714
- this.dispatchEvent(event);
5715
- };
5716
- // Helper to handle timed dispatch
5717
- const timedDispatch = (detail) => {
5718
- if (options?.debounce) {
5719
- clearTimeout(timers.debounceTimeout);
5720
- timers.debounceTimeout = setTimeout(() => doDispatch(detail), options.debounce);
5721
- return;
5722
- }
5723
- if (!options?.throttle) {
5724
- doDispatch(detail);
5725
- return;
5726
- }
5727
- const now = Date.now();
5728
- const remaining = options.throttle - (now - timers.throttleLastCall);
5729
- if (remaining <= 0) {
5730
- clearTimeout(timers.throttleTimeout);
5731
- timers.throttleLastCall = now;
5732
- doDispatch(detail);
5733
- return;
5734
- }
5735
- // Record the LATEST detail so the trailing dispatch carries fresh
5736
- // data, not the first-suppressed-call detail captured by closure.
5737
- timers.latestDetail = detail;
5738
- if (!timers.throttleTimeout) {
5739
- timers.throttleTimeout = setTimeout(() => {
5740
- timers.throttleLastCall = Date.now();
5741
- timers.throttleTimeout = null;
5742
- const d = timers.latestDetail;
5743
- timers.latestDetail = undefined;
5744
- doDispatch(d);
5745
- }, remaining);
5746
- }
5747
- };
5748
- // Handle async methods
5749
- if (result instanceof Promise) {
5750
- return result.then((resolvedResult) => {
5751
- timedDispatch(resolvedResult);
5752
- return resolvedResult;
5753
- });
5754
- }
5755
- // Sync method
5756
- timedDispatch(result);
5757
- return result;
5758
- };
5759
- };
5760
- }
5761
-
5762
6234
  /**
5763
6235
  * createRequestHandler — vanilla JS utility for handling @request channels
5764
6236
  * outside of the Snice element/controller decorator system.
@@ -6104,7 +6576,7 @@ var Snice = (function (exports) {
6104
6576
  }
6105
6577
 
6106
6578
  /*!
6107
- * snice v5.2.1
6579
+ * snice v5.2.3
6108
6580
  * 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.
6109
6581
  * (c) 2024
6110
6582
  * Released under the MIT License.
@@ -6123,7 +6595,7 @@ var Snice = (function (exports) {
6123
6595
  }
6124
6596
 
6125
6597
  /*!
6126
- * snice v5.2.1
6598
+ * snice v5.2.3
6127
6599
  * 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.
6128
6600
  * (c) 2024
6129
6601
  * Released under the MIT License.