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
package/dist/index.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v5.2.1
2
+ * snice v5.2.3
3
3
  * A decorator-driven web component library with differential rendering, routing, controllers, and 130+ ready-made UI components. Use as much or as little as you want. Zero dependencies, works anywhere.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -33,6 +33,12 @@ const CHANNEL_HANDLERS = getSymbol('channel-handlers');
33
33
  // Internal element state symbols
34
34
  const READY_PROMISE = getSymbol('ready-promise');
35
35
  const READY_RESOLVE = getSymbol('ready-resolve');
36
+ // Set when a render is requested but dropped because the element was
37
+ // disconnected; consumed on reconnect to replay exactly the missed render.
38
+ const PENDING_RECONNECT_RENDER = getSymbol('pending-reconnect-render');
39
+ // Per-element synchronous render-nesting depth, used to cap runaway re-entry
40
+ // without a shared global counter that one element could exhaust for others.
41
+ const RENDER_DEPTH = getSymbol('render-depth');
36
42
  const CONTROLLER = getSymbol('controller');
37
43
  const INITIALIZED = getSymbol('initialized');
38
44
  // Controller symbols
@@ -104,6 +110,10 @@ const ADOPTED_METHODS = getSymbol('adopted-methods');
104
110
  // Controller attach-abort — set to an AbortController while attachController
105
111
  // is awaiting element.ready, so detachController can short-circuit the wait.
106
112
  const CONTROLLER_ABORT = getSymbol('controller-abort');
113
+ // Set on a controller instance once it has gotten past element.ready and is
114
+ // being attached, so detachController knows not to run detach() on a controller
115
+ // whose attach() was aborted before it ever ran.
116
+ const CONTROLLER_ATTACHED = getSymbol('controller-attached');
107
117
  const PROPERTY_DEFINERS = getSymbol('property-definers');
108
118
 
109
119
  /**
@@ -224,11 +234,23 @@ class Context {
224
234
  this[REGISTERED_ELEMENTS_SET].add(element);
225
235
  }
226
236
  /**
227
- * Unregister an element from receiving context updates
237
+ * Unregister an element from receiving context updates. With a methodName,
238
+ * removes only that one handler (so a `once` handler tearing itself down
239
+ * doesn't kill the element's other @context handlers); without one, removes
240
+ * the element entirely (used on disconnect).
228
241
  * @internal Used by @context decorator cleanup
229
242
  */
230
- [CONTEXT_UNREGISTER](element) {
231
- this[REGISTERED_ELEMENTS].delete(element);
243
+ [CONTEXT_UNREGISTER](element, methodName) {
244
+ const map = this[REGISTERED_ELEMENTS];
245
+ if (methodName !== undefined) {
246
+ const names = map.get(element);
247
+ if (!names)
248
+ return;
249
+ names.delete(methodName);
250
+ if (names.size > 0)
251
+ return; // other handlers remain — keep the element
252
+ }
253
+ map.delete(element);
232
254
  this[REGISTERED_ELEMENTS_SET].delete(element);
233
255
  }
234
256
  /**
@@ -393,11 +415,23 @@ function parseAttributeValue(attributeValue, propertyOptions, currentValue, init
393
415
  * @param initialValue - The default value assigned to the property field (used for type detection)
394
416
  * @returns The string representation for the HTML attribute, or null if value should remove attribute
395
417
  */
418
+ /**
419
+ * Default property dirty-check: has the value changed? Like `!==`, but treats
420
+ * NaN → NaN as unchanged (NaN !== NaN would otherwise re-fire forever).
421
+ */
422
+ function notEqual(value, old) {
423
+ // Changed unless strictly equal, or both are NaN (each self-unequal).
424
+ return value !== old && (value === value || old === old);
425
+ }
396
426
  /**
397
427
  * Extract attribute name from property options
398
428
  */
399
429
  function getAttrName(opts, propName) {
400
- return typeof opts.attribute === 'string' ? opts.attribute : propName.toLowerCase();
430
+ // Lowercase always: the DOM stores attribute names lowercase, so an explicit
431
+ // camelCase `attribute:` would otherwise never match observedAttributes and
432
+ // attributeChangedCallback would never fire. get/setAttribute are already
433
+ // case-insensitive, so this is safe for every caller.
434
+ return (typeof opts.attribute === 'string' ? opts.attribute : propName).toLowerCase();
401
435
  }
402
436
  /**
403
437
  * Escape HTML text-node content. Use for text that will be interpolated
@@ -544,6 +578,42 @@ function invokeWatchers(instance, constructor, propName, oldValue, newValue) {
544
578
  }
545
579
  }
546
580
  }
581
+ /**
582
+ * Fire the one-time initialization call for `@watch(..., { immediate: true })`
583
+ * handlers. Runs once per declared property after the element is initialized,
584
+ * passing the effective initial value with `oldValue` undefined. Non-immediate
585
+ * watchers are untouched — they only react to later changes.
586
+ */
587
+ function invokeImmediateWatchers(instance, constructor) {
588
+ const watchers = constructor[PROPERTY_WATCHERS];
589
+ const properties = constructor[PROPERTIES];
590
+ if (!watchers || !properties)
591
+ return;
592
+ for (const [propName, propOptions] of properties) {
593
+ // A watcher may be registered under the JS name, the explicit `attribute:`
594
+ // name, or the `*` wildcard; fire each immediate one exactly once, keyed to
595
+ // this property's current value.
596
+ const keys = [propName];
597
+ const attrName = propOptions && typeof propOptions.attribute === 'string' ? propOptions.attribute : null;
598
+ if (attrName && attrName !== propName)
599
+ keys.push(attrName);
600
+ keys.push('*');
601
+ for (const key of keys) {
602
+ if (!watchers.has(key))
603
+ continue;
604
+ for (const w of watchers.get(key)) {
605
+ if (!w.immediate)
606
+ continue;
607
+ try {
608
+ w.method.call(instance, undefined, instance[propName], propName);
609
+ }
610
+ catch (e) {
611
+ console.error(`Error in @watch('${key}') method ${w.methodName}:`, e);
612
+ }
613
+ }
614
+ }
615
+ }
616
+ }
547
617
  /**
548
618
  * Create a debounced version of a function
549
619
  */
@@ -577,6 +647,41 @@ function createThrottled(fn, delay) {
577
647
  }
578
648
  };
579
649
  }
650
+ /**
651
+ * Resolve a scope option to a concrete EventTarget.
652
+ *
653
+ * - undefined → host element (default behavior)
654
+ * - 'global' → `document`
655
+ * - string → `host.closest(selector)` (nearest matching ancestor)
656
+ * - EventTarget instance → that target directly
657
+ * - resolver function → called with host as `this`, returns target or null
658
+ *
659
+ * Returns null when the scope cannot be resolved (e.g. selector matches no
660
+ * ancestor, resolver returns null). Caller decides how to handle null —
661
+ * typically warn in dev and skip listener attachment.
662
+ */
663
+ function resolveScope(host, scope) {
664
+ if (scope === undefined || scope === null)
665
+ return host;
666
+ if (scope === 'global')
667
+ return typeof document !== 'undefined' ? document : null;
668
+ if (typeof scope === 'function') {
669
+ try {
670
+ return scope.call(host) || null;
671
+ }
672
+ catch {
673
+ return null;
674
+ }
675
+ }
676
+ if (typeof scope === 'string') {
677
+ return host.closest?.(scope) || null;
678
+ }
679
+ // Direct EventTarget instance.
680
+ if (scope && typeof scope.addEventListener === 'function') {
681
+ return scope;
682
+ }
683
+ return null;
684
+ }
580
685
  function valueToAttribute(value, propertyOptions, initialValue) {
581
686
  // Handle null/undefined/false values and empty arrays
582
687
  if (value === null || value === undefined || value === false ||
@@ -994,13 +1099,11 @@ function request(requestName, options) {
994
1099
  return finalValue;
995
1100
  }
996
1101
  catch (error) {
997
- // Send error to generator
998
- try {
999
- await generator.throw(error);
1000
- }
1001
- catch (generatorError) {
1002
- throw generatorError;
1003
- }
1102
+ // Drive the generator's own catch block and return whatever it recovers
1103
+ // with (e.g. a cached fallback). If the generator re-throws instead,
1104
+ // generator.throw rejects and the error propagates to the caller.
1105
+ const { value: recovered } = await generator.throw(error);
1106
+ return recovered;
1004
1107
  }
1005
1108
  }; // Close actualRequest function
1006
1109
  // Debounce
@@ -1024,9 +1127,21 @@ function request(requestName, options) {
1024
1127
  const now = Date.now();
1025
1128
  const remaining = options.throttle - (now - throttleLastCall);
1026
1129
  if (remaining <= 0) {
1130
+ // Cancelling the scheduled trailing call must not orphan the promises
1131
+ // queued for it — settle them with this fresh result. Also reset
1132
+ // throttleTimeout so later within-window calls don't attach to a dead
1133
+ // timer and hang.
1027
1134
  clearTimeout(throttleTimeout);
1135
+ throttleTimeout = null;
1028
1136
  throttleLastCall = now;
1029
- return actualRequest();
1137
+ const orphaned = throttleTrailingResolvers.splice(0);
1138
+ const p = actualRequest();
1139
+ if (orphaned.length) {
1140
+ p.then((r) => { for (const x of orphaned)
1141
+ x.resolve(r); }, (e) => { for (const x of orphaned)
1142
+ x.reject(e); });
1143
+ }
1144
+ return p;
1030
1145
  }
1031
1146
  // Throttle: already has pending trailing call — attach to it
1032
1147
  if (throttleTimeout) {
@@ -1127,10 +1242,20 @@ function setupResponseHandlers(instance, element) {
1127
1242
  const now = Date.now();
1128
1243
  const remaining = handler.options.throttle - (now - throttleLastCall);
1129
1244
  if (remaining <= 0) {
1245
+ // Same as the @request side: settle any queued trailing callers with
1246
+ // this fresh result rather than orphaning their promises.
1130
1247
  clearTimeout(throttleTimeout);
1131
1248
  throttleTimeout = null;
1132
1249
  throttleLastCall = now;
1133
- return originalMethod(...args);
1250
+ const orphaned = throttleTrailingResolvers.splice(0);
1251
+ throttleLatestArgs = null;
1252
+ const result = originalMethod(...args);
1253
+ if (orphaned.length) {
1254
+ Promise.resolve(result).then((r) => { for (const x of orphaned)
1255
+ x.resolve(r); }, (e) => { for (const x of orphaned)
1256
+ x.reject(e); });
1257
+ }
1258
+ return result;
1134
1259
  }
1135
1260
  // Remember the LATEST args so the trailing call uses fresh input,
1136
1261
  // and queue every suppressed caller's resolver so they all receive
@@ -1237,7 +1362,10 @@ function css(strings, ...values) {
1237
1362
  // Combine strings and values into final CSS text
1238
1363
  let cssText = strings[0];
1239
1364
  for (let i = 0; i < values.length; i++) {
1240
- cssText += String(values[i]) + strings[i + 1];
1365
+ // Inline a nested css`` result by its text so style fragments compose;
1366
+ // String() on a CSSResult object would yield "[object Object]".
1367
+ const value = values[i];
1368
+ cssText += (isCSSResult(value) ? value.cssText : String(value)) + strings[i + 1];
1241
1369
  }
1242
1370
  const result = {
1243
1371
  _$litType$: CSS_RESULT,
@@ -1388,6 +1516,20 @@ class Template {
1388
1516
  const attrStrings = value.split(marker);
1389
1517
  // Number of expressions = number of markers = attrStrings.length - 1
1390
1518
  const expressionCount = attrStrings.length - 1;
1519
+ // Property/boolean/event bindings take a SINGLE expression — they
1520
+ // can't concatenate static text (a `.prop` is an object, not a
1521
+ // string). They consume the first expression; any static text or
1522
+ // extra expressions are ignored. But the value index must still
1523
+ // advance by the true marker count, or every later binding shifts.
1524
+ const isSingleExpressionBinding = /^@@?|^[.?]/.test(originalName);
1525
+ if (isSingleExpressionBinding) {
1526
+ const hasStaticText = attrStrings.some((s) => s !== '');
1527
+ if (expressionCount > 1 || hasStaticText) {
1528
+ console.warn(`snice: binding "${originalName}" takes a single expression; ` +
1529
+ `static text and extra interpolations are ignored. ` +
1530
+ `Use a plain attribute for string interpolation.`);
1531
+ }
1532
+ }
1391
1533
  if (originalName.startsWith('@@')) {
1392
1534
  // Escaped event binding for events with @ in the name (e.g., @@snice/event -> @snice/event)
1393
1535
  this.parts.push({
@@ -1396,7 +1538,7 @@ class Template {
1396
1538
  name: originalName.slice(1), // Keep the @ in the event name
1397
1539
  element
1398
1540
  });
1399
- partIndex += 1;
1541
+ partIndex += expressionCount;
1400
1542
  }
1401
1543
  else if (originalName.startsWith('@')) {
1402
1544
  // Event binding (single value only)
@@ -1406,7 +1548,7 @@ class Template {
1406
1548
  name: originalName.slice(1),
1407
1549
  element
1408
1550
  });
1409
- partIndex += 1;
1551
+ partIndex += expressionCount;
1410
1552
  }
1411
1553
  else if (originalName.startsWith('.')) {
1412
1554
  // Property binding (single value only)
@@ -1416,7 +1558,7 @@ class Template {
1416
1558
  name: originalName.slice(1),
1417
1559
  element
1418
1560
  });
1419
- partIndex += 1;
1561
+ partIndex += expressionCount;
1420
1562
  }
1421
1563
  else if (originalName.startsWith('?')) {
1422
1564
  // Boolean attribute (single value only)
@@ -1426,7 +1568,7 @@ class Template {
1426
1568
  name: originalName.slice(1),
1427
1569
  element
1428
1570
  });
1429
- partIndex += 1;
1571
+ partIndex += expressionCount;
1430
1572
  }
1431
1573
  else {
1432
1574
  // Regular attribute - supports multiple interpolations
@@ -1901,7 +2043,13 @@ class NodePart extends Part {
1901
2043
  * Commit unsafe HTML string
1902
2044
  */
1903
2045
  _commitUnsafeHTML(value) {
1904
- // Always recreate for unsafe HTML (can't diff arbitrary HTML)
2046
+ // Can't diff arbitrary HTML, but when the STRING is unchanged the DOM is
2047
+ // already correct — skip the clear+reparse so live state (typed input
2048
+ // values, focus, scroll) inside the block survives unrelated re-renders.
2049
+ const prev = this._committedValue;
2050
+ if (isUnsafeHTML(prev) && prev.html === value.html) {
2051
+ return;
2052
+ }
1905
2053
  this._clear();
1906
2054
  const temp = document.createElement('template');
1907
2055
  temp.innerHTML = value.html;
@@ -2086,7 +2234,9 @@ class EventPart extends Part {
2086
2234
  // Supports both dot notation (@keydown.enter) and colon notation (@keydown:Enter) to match @on decorator
2087
2235
  // Only parse colons for keyboard events, not custom events
2088
2236
  const isKeyboardEvent = ['keydown', 'keyup', 'keypress'].includes(eventName.split('.')[0].split(':')[0]);
2089
- const dotIndex = eventName.indexOf('.');
2237
+ // Only keyboard events split on `.`/`:` into a key filter — a custom event
2238
+ // name may legitimately contain a dot (e.g. `app.ready`) and must be kept whole.
2239
+ const dotIndex = isKeyboardEvent ? eventName.indexOf('.') : -1;
2090
2240
  const colonIndex = isKeyboardEvent ? eventName.indexOf(':') : -1;
2091
2241
  // Use whichever delimiter comes first (dot or colon)
2092
2242
  const delimiterIndex = dotIndex > 0 && colonIndex > 0
@@ -2100,6 +2250,7 @@ class EventPart extends Part {
2100
2250
  }
2101
2251
  else {
2102
2252
  this.eventName = eventName;
2253
+ warnIfModifierMisuse(eventName);
2103
2254
  }
2104
2255
  }
2105
2256
  commit(value) {
@@ -2125,17 +2276,19 @@ class EventPart extends Part {
2125
2276
  }
2126
2277
  if (typeof value !== 'function')
2127
2278
  return;
2128
- // Auto-bind to host element (the custom element with shadow root)
2129
- if (!this.host) {
2130
- const rootNode = this.element.getRootNode();
2131
- this.host = rootNode.host || null;
2132
- }
2133
- const context = this.host || null;
2134
2279
  const keyFilter = this.keyFilter;
2135
2280
  this.listener = ((event) => {
2136
2281
  if (keyFilter && !matchesKeyboardFilter(event, keyFilter))
2137
2282
  return;
2138
- value.call(context, event);
2283
+ // Resolve the host (the custom element owning the shadow root) at dispatch
2284
+ // time, not bind time: a binding inside an initially-hidden <if>/<case>
2285
+ // branch commits while off-DOM, where getRootNode() has no host — caching
2286
+ // that null permanently would call the handler with `this = null` once the
2287
+ // branch is shown. By the time an event fires, the element is in the DOM.
2288
+ if (!this.host) {
2289
+ this.host = this.element.getRootNode().host || null;
2290
+ }
2291
+ value.call(this.host || null, event);
2139
2292
  });
2140
2293
  this.element.addEventListener(this.eventName, this.listener);
2141
2294
  }
@@ -2154,6 +2307,25 @@ class EventPart extends Part {
2154
2307
  * "ctrl+shift+s" -> { key: "s", ctrl: true, shift: true }
2155
2308
  * "~enter" -> { key: "Enter", anyModifiers: true }
2156
2309
  */
2310
+ // once/preventDefault/stopPropagation/capture/passive are @on() OPTIONS, not
2311
+ // template modifiers. A dotted non-keyboard event whose suffix is one of these
2312
+ // is almost certainly a mistake (e.g. `@click.once`) — it binds a listener for
2313
+ // an event type the DOM never dispatches. Warn instead of failing silently. A
2314
+ // legitimate custom event name like `app.ready` won't match and stays quiet.
2315
+ const MODIFIER_WORDS = new Set([
2316
+ 'once', 'prevent', 'preventdefault', 'stop', 'stoppropagation', 'capture', 'passive',
2317
+ ]);
2318
+ function warnIfModifierMisuse(eventName) {
2319
+ const dot = eventName.lastIndexOf('.');
2320
+ if (dot < 0)
2321
+ return;
2322
+ const suffix = eventName.slice(dot + 1).toLowerCase();
2323
+ if (!MODIFIER_WORDS.has(suffix))
2324
+ return;
2325
+ console.warn(`snice: "@${eventName}" is not a valid modifier — once/preventDefault/` +
2326
+ `stopPropagation/capture are @on() options, not template modifiers. ` +
2327
+ `This registers a listener for event "${eventName}", which never fires.`);
2328
+ }
2157
2329
  function parseKeyboardFilter(spec) {
2158
2330
  // Handle ~ prefix for matching regardless of modifiers
2159
2331
  const anyModifiers = spec.startsWith('~');
@@ -2256,9 +2428,14 @@ class ConditionalIfPart extends Part {
2256
2428
  // Insert markers where <if> is
2257
2429
  parent.insertBefore(this.startNode, ifElement);
2258
2430
  parent.insertBefore(this.endNode, ifElement.nextSibling);
2259
- // Move <if>'s children between the markers
2431
+ // Park <if>'s children in an off-DOM fragment immediately, rather than
2432
+ // leaving them in the live template fragment. Otherwise a branch that is
2433
+ // hidden on first render would still connect its children (when the
2434
+ // template mounts) and then disconnect them (on the first commit) — firing
2435
+ // mount side effects spuriously. commit(true) inserts them when shown.
2436
+ this.childFragment = document.createDocumentFragment();
2260
2437
  while (ifElement.firstChild) {
2261
- parent.insertBefore(ifElement.firstChild, this.endNode);
2438
+ this.childFragment.appendChild(ifElement.firstChild);
2262
2439
  }
2263
2440
  // Remove the <if> element from DOM
2264
2441
  parent.removeChild(ifElement);
@@ -2339,16 +2516,24 @@ class ConditionalCasePart extends Part {
2339
2516
  parent.removeChild(caseElement);
2340
2517
  }
2341
2518
  commit(value) {
2342
- if (this.value === value)
2519
+ // Dirty-check on the SELECTED BRANCH, not the raw value. A value whose type
2520
+ // changed but stringifies the same (number 1 vs string '1' — e.g. a numeric
2521
+ // default replaced by a router param, which is always a string) selects the
2522
+ // same branch, so tearing it down and re-inserting it would needlessly
2523
+ // restart media/animations and lose focus inside it.
2524
+ const valueStr = String(value);
2525
+ const hasBranch = this.branches.has(valueStr);
2526
+ const nextKey = hasBranch ? valueStr : (this.defaultBranch ? '__default__' : null);
2527
+ if (this.currentKey === nextKey) {
2528
+ this.value = value;
2343
2529
  return;
2530
+ }
2344
2531
  this.value = value;
2345
- const valueStr = String(value);
2346
2532
  // Move current content back to its fragment
2347
2533
  this._collectCurrent();
2348
2534
  // Insert matching branch
2349
- const hasBranch = this.branches.has(valueStr);
2350
2535
  const matchingFragment = hasBranch ? this.branches.get(valueStr) : this.defaultBranch;
2351
- this.currentKey = hasBranch ? valueStr : (this.defaultBranch ? '__default__' : null);
2536
+ this.currentKey = nextKey;
2352
2537
  if (matchingFragment && matchingFragment.hasChildNodes()) {
2353
2538
  this.startNode.parentNode.insertBefore(matchingFragment, this.endNode);
2354
2539
  }
@@ -2526,7 +2711,9 @@ function setupEventHandlers(instance, targetElement) {
2526
2711
  // Supports both dot notation (@keydown.enter) and colon notation (@keydown:Enter)
2527
2712
  // Only parse colons for keyboard events, not custom events
2528
2713
  const isKeyboardEvent = ['keydown', 'keyup', 'keypress'].includes(handler.eventName.split('.')[0].split(':')[0]);
2529
- const dotIndex = handler.eventName.indexOf('.');
2714
+ // Only keyboard events split on `.`/`:` into a key filter — a custom event
2715
+ // name may legitimately contain a dot (e.g. `app.ready`) and must be kept whole.
2716
+ const dotIndex = isKeyboardEvent ? handler.eventName.indexOf('.') : -1;
2530
2717
  const colonIndex = isKeyboardEvent ? handler.eventName.indexOf(':') : -1;
2531
2718
  const delimiterIndex = dotIndex > 0 && colonIndex > 0
2532
2719
  ? Math.min(dotIndex, colonIndex)
@@ -2537,6 +2724,8 @@ function setupEventHandlers(instance, targetElement) {
2537
2724
  const keyModifier = delimiterIndex > 0
2538
2725
  ? handler.eventName.substring(delimiterIndex + 1)
2539
2726
  : null;
2727
+ if (delimiterIndex <= 0)
2728
+ warnIfModifierMisuse(handler.eventName);
2540
2729
  // Apply debounce (takes precedence over throttle)
2541
2730
  if (handlerOptions.debounce && handlerOptions.debounce > 0) {
2542
2731
  boundMethod = createDebounced(boundMethod, handlerOptions.debounce);
@@ -2563,10 +2752,27 @@ function setupEventHandlers(instance, targetElement) {
2563
2752
  };
2564
2753
  // Apply key modifier wrapper
2565
2754
  const keyModifierMethod = createKeyModifierHandler(boundMethod);
2755
+ // Resolve scope: explicit `scope` option redirects the listener target
2756
+ // to document / ancestor / arbitrary EventTarget. Default = host element.
2757
+ const hasExplicitScope = handlerOptions.scope !== undefined;
2758
+ let scopedTarget = null;
2759
+ if (hasExplicitScope) {
2760
+ scopedTarget = resolveScope(targetElement, handlerOptions.scope);
2761
+ if (!scopedTarget) {
2762
+ // Dev warning — listener silently dropped when scope cannot resolve.
2763
+ // Skip attachment so we don't bind to the wrong target.
2764
+ console.warn(`[snice/@on] scope did not resolve for "${handler.eventName}" on ${handler.methodName} — listener skipped.`);
2765
+ continue;
2766
+ }
2767
+ }
2566
2768
  // Main event handler with error handling and event delegation
2567
2769
  if (handler.selector) {
2568
- // Delegated event handling - use shadow root if available
2569
- const eventRoot = targetElement.shadowRoot || targetElement;
2770
+ // Delegated event handling.
2771
+ // - Default eventRoot: shadow root if present, else host element.
2772
+ // - With explicit scope: attach to the scoped target instead.
2773
+ const eventRoot = hasExplicitScope
2774
+ ? scopedTarget
2775
+ : (targetElement.shadowRoot || targetElement);
2570
2776
  const delegatedHandler = (event) => {
2571
2777
  const target = event.target;
2572
2778
  const matchingElement = (target.matches && target.matches(handler.selector) && target) ||
@@ -2606,10 +2812,11 @@ function setupEventHandlers(instance, targetElement) {
2606
2812
  });
2607
2813
  }
2608
2814
  else {
2609
- // Direct event handling - on the element itself
2610
- // If element has shadow root, listen on both shadow root AND host element
2611
- // to catch events from inside shadow DOM (with correct target) and on host itself.
2612
- const shadowRoot = targetElement.shadowRoot;
2815
+ // Direct event handling.
2816
+ // - Default: shadow root + host element (so events inside shadow and on
2817
+ // the host itself both fire). A per-handler Symbol dedupes.
2818
+ // - With explicit scope: attach to the scoped target ONLY (no duplication).
2819
+ const shadowRoot = hasExplicitScope ? null : targetElement.shadowRoot;
2613
2820
  // Per-handler private Symbol so dedup is scoped to THIS handler's two
2614
2821
  // listeners (shadowRoot + host). Using Symbol.for() with the method name
2615
2822
  // would collide across components that share a method name (e.g. a parent
@@ -2637,6 +2844,16 @@ function setupEventHandlers(instance, targetElement) {
2637
2844
  once: handlerOptions.once || false,
2638
2845
  passive: handlerOptions.passive || false,
2639
2846
  };
2847
+ if (hasExplicitScope) {
2848
+ scopedTarget.addEventListener(baseEventName, wrappedMethod, listenerOptions);
2849
+ instance[CLEANUP].events.push({
2850
+ target: scopedTarget,
2851
+ eventName: baseEventName,
2852
+ handler: wrappedMethod,
2853
+ options: listenerOptions,
2854
+ });
2855
+ continue;
2856
+ }
2640
2857
  if (shadowRoot) {
2641
2858
  // Listen on shadow root for events inside shadow DOM
2642
2859
  shadowRoot.addEventListener(baseEventName, wrappedMethod, listenerOptions);
@@ -2799,11 +3016,25 @@ async function attachController(element, controllerName) {
2799
3016
  }),
2800
3017
  ]);
2801
3018
  }
3019
+ catch (error) {
3020
+ // Attach never got past `ready` (aborted or deadline). Roll back the stored
3021
+ // references so nothing is left pointing at a controller that never
3022
+ // attached — otherwise a later attach short-circuits as "already attached"
3023
+ // and a detach would run detach() on it.
3024
+ if (element[CONTROLLER_KEY] === controllerInstance) {
3025
+ delete element[CONTROLLER_KEY];
3026
+ delete element[CONTROLLER_NAME_KEY];
3027
+ delete element[CONTROLLER_OPERATIONS];
3028
+ }
3029
+ throw error;
3030
+ }
2802
3031
  finally {
2803
3032
  if (element[CONTROLLER_ABORT] === abort) {
2804
3033
  delete element[CONTROLLER_ABORT];
2805
3034
  }
2806
3035
  }
3036
+ // Past `ready` — mark the controller so detach knows attach was actually run.
3037
+ controllerInstance[CONTROLLER_ATTACHED] = true;
2807
3038
  // Run attach in the controller's scope
2808
3039
  await scope.runOperation(async () => {
2809
3040
  await controllerInstance.attach(element);
@@ -2834,6 +3065,19 @@ async function detachController(element) {
2834
3065
  if (!controllerInstance) {
2835
3066
  return;
2836
3067
  }
3068
+ // Claim the controller immediately — before any await — so a second detach
3069
+ // that overlaps this one (e.g. disconnect's fire-and-forget detach racing a
3070
+ // controller reassignment) finds no instance and returns, instead of running
3071
+ // detach() a second time on the same controller. The rest of this function
3072
+ // works off the locals captured above.
3073
+ delete element[CONTROLLER_KEY];
3074
+ delete element[CONTROLLER_NAME_KEY];
3075
+ delete element[CONTROLLER_OPERATIONS];
3076
+ // A controller whose attach() was aborted before it ever ran must not have
3077
+ // detach() (or the teardown/event) run on it.
3078
+ if (!controllerInstance[CONTROLLER_ATTACHED]) {
3079
+ return;
3080
+ }
2837
3081
  // Run detach in the controller's scope
2838
3082
  if (scope) {
2839
3083
  await scope.runOperation(async () => {
@@ -2856,9 +3100,6 @@ async function detachController(element) {
2856
3100
  }
2857
3101
  // Clean up router context reference
2858
3102
  delete controllerInstance[ROUTER_CONTEXT];
2859
- delete element[CONTROLLER_KEY];
2860
- delete element[CONTROLLER_NAME_KEY];
2861
- delete element[CONTROLLER_OPERATIONS];
2862
3103
  element.dispatchEvent(new CustomEvent('controller-detached', {
2863
3104
  detail: { name: controllerName, controller: controllerInstance }
2864
3105
  }));
@@ -3021,19 +3262,23 @@ function setupContextHandler(element) {
3021
3262
  const wrappedMethodName = `__wrapped_${methodName}`;
3022
3263
  // Create wrapped method with timing controls
3023
3264
  element[wrappedMethodName] = function (context) {
3024
- // Skip if already called once
3025
- if (options.once && element[CONTEXT_CALLED]) {
3265
+ // `once` is tracked PER method (not a single element-level flag) so
3266
+ // multiple once handlers on the same element each fire.
3267
+ const called = element[CONTEXT_CALLED] ||
3268
+ (element[CONTEXT_CALLED] = new Set());
3269
+ if (options.once && called.has(methodName)) {
3026
3270
  return;
3027
3271
  }
3028
3272
  const callMethod = () => {
3029
3273
  method.call(element, context);
3030
3274
  // Handle once option
3031
3275
  if (options.once) {
3032
- element[CONTEXT_CALLED] = true;
3033
- // Unregister after first call
3276
+ called.add(methodName);
3277
+ // Unregister ONLY this handler, not the whole element — otherwise the
3278
+ // element's other @context handlers would stop receiving updates.
3034
3279
  const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
3035
3280
  if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
3036
- ctx[CONTEXT_UNREGISTER](element);
3281
+ ctx[CONTEXT_UNREGISTER](element, wrappedMethodName);
3037
3282
  }
3038
3283
  }
3039
3284
  };
@@ -3136,9 +3381,14 @@ class RenderScheduler {
3136
3381
  this.pending.clear();
3137
3382
  this.scheduled = false;
3138
3383
  for (const element of elements) {
3139
- // Skip elements that were disconnected between scheduling and flush.
3140
- if (!element.isConnected)
3384
+ // Skip elements that were disconnected between scheduling and flush, but
3385
+ // remember the dropped render so reconnect can replay it — otherwise the
3386
+ // shadow DOM stays stale for a property changed while detached. The flag
3387
+ // lives on the element, so it's GC'd with it (no leak if never reattached).
3388
+ if (!element.isConnected) {
3389
+ element[PENDING_RECONNECT_RENDER] = true;
3141
3390
  continue;
3391
+ }
3142
3392
  const options = element[RENDER_OPTIONS] || {};
3143
3393
  performRender(element, options);
3144
3394
  }
@@ -3157,6 +3407,19 @@ function flushRenderCallbacks(element) {
3157
3407
  element[RENDER_CALLBACKS] = [];
3158
3408
  cbs.forEach(cb => cb());
3159
3409
  }
3410
+ /**
3411
+ * Backstop against a render() that mutates an observed property, which makes
3412
+ * the property setter request another render from inside the current one. In
3413
+ * `sync` mode that re-enters performRender synchronously and, unchecked,
3414
+ * recurses to a stack overflow. The counter caps synchronous nesting and turns
3415
+ * a silent crash into an actionable error. Normal renders never nest (child
3416
+ * renders defer to a microtask), so this stays at depth 1 in the common case.
3417
+ *
3418
+ * The depth is tracked PER ELEMENT (on RENDER_DEPTH), not globally: a runaway
3419
+ * component must not consume a shared budget and reject an unrelated component
3420
+ * that happens to render inside its call stack.
3421
+ */
3422
+ const MAX_RENDER_DEPTH = 50;
3160
3423
  /**
3161
3424
  * Perform the actual render of an element
3162
3425
  */
@@ -3168,6 +3431,14 @@ function performRender(element, options, precomputedResult) {
3168
3431
  if (options.once && element[RENDER_INSTANCE]) {
3169
3432
  return;
3170
3433
  }
3434
+ const depth = (element[RENDER_DEPTH] ?? 0);
3435
+ if (depth >= MAX_RENDER_DEPTH) {
3436
+ const tag = element.tagName ? element.tagName.toLowerCase() : 'element';
3437
+ console.error(`snice: maximum render depth (${MAX_RENDER_DEPTH}) exceeded for <${tag}>. ` +
3438
+ `render() is mutating an observed property, causing an infinite render loop.`);
3439
+ return;
3440
+ }
3441
+ element[RENDER_DEPTH] = depth + 1;
3171
3442
  try {
3172
3443
  const result = precomputedResult !== undefined ? precomputedResult : renderMethod.call(element);
3173
3444
  if (!element.shadowRoot)
@@ -3193,14 +3464,18 @@ function performRender(element, options, precomputedResult) {
3193
3464
  flushRenderCallbacks(element);
3194
3465
  return;
3195
3466
  }
3196
- // Different template or first render. Remove only non-<style> children so
3197
- // the fallback @styles path (Safari <=16 / jsdom / SSR — no
3198
- // adoptedStyleSheets) doesn't lose its style tags on template switch.
3467
+ // Different template or first render. Keep only the framework's own
3468
+ // fallback <style> tags (marked data-snice-style) so the @styles path
3469
+ // (Safari <=16 / jsdom / SSR no adoptedStyleSheets) survives a template
3470
+ // switch; template-emitted <style> tags are removed and re-created by the
3471
+ // new instance, so they don't accumulate.
3199
3472
  if (instance) {
3200
3473
  const root = element.shadowRoot;
3201
3474
  const toRemove = [];
3202
3475
  for (const child of Array.from(root.childNodes)) {
3203
- if (child.nodeType === 1 && child.tagName === 'STYLE')
3476
+ if (child.nodeType === 1 &&
3477
+ child.tagName === 'STYLE' &&
3478
+ child.hasAttribute('data-snice-style'))
3204
3479
  continue;
3205
3480
  toRemove.push(child);
3206
3481
  }
@@ -3216,6 +3491,9 @@ function performRender(element, options, precomputedResult) {
3216
3491
  catch (error) {
3217
3492
  console.error('Error rendering element:', error);
3218
3493
  }
3494
+ finally {
3495
+ element[RENDER_DEPTH] = depth;
3496
+ }
3219
3497
  }
3220
3498
  /**
3221
3499
  * Request a render for an element
@@ -3269,6 +3547,33 @@ function requestRender(element, immediate = false) {
3269
3547
  // Normal rendering (with microtask batching unless sync)
3270
3548
  renderScheduler.schedule(element, options);
3271
3549
  }
3550
+ /**
3551
+ * Clear any pending debounce/throttle render timers on an element (e.g. on
3552
+ * disconnect, so they don't fire on a dead element and retain it until they
3553
+ * expire). Returns true if a render was actually pending, so the caller can
3554
+ * arrange a replay on reconnect via PENDING_RECONNECT_RENDER.
3555
+ */
3556
+ function clearRenderTimers(element) {
3557
+ const timers = element[RENDER_TIMERS];
3558
+ if (!timers)
3559
+ return false;
3560
+ let hadPending = false;
3561
+ if (timers.debounce) {
3562
+ clearTimeout(timers.debounce);
3563
+ timers.debounce = null;
3564
+ hadPending = true;
3565
+ }
3566
+ if (timers.throttleTimer) {
3567
+ clearTimeout(timers.throttleTimer);
3568
+ timers.throttleTimer = null;
3569
+ hadPending = true;
3570
+ }
3571
+ // Deliberately do NOT reset timers.lastThrottle: a plain DOM move is a
3572
+ // disconnect+reconnect, and zeroing the cooldown would let the next render
3573
+ // fire inside the throttle window. Preserving it keeps throttle honest
3574
+ // across moves; on a true disconnect the element is discarded anyway.
3575
+ return hadPending;
3576
+ }
3272
3577
  /**
3273
3578
  * @render decorator for component rendering
3274
3579
  *
@@ -3391,9 +3696,12 @@ function applyStyles(element) {
3391
3696
  element.shadowRoot.adoptedStyleSheets = allResults.map(r => r.styleSheet);
3392
3697
  return;
3393
3698
  }
3394
- // Fallback — one <style> tag per stylesheet, preserving cascade order
3699
+ // Fallback — one <style> tag per stylesheet, preserving cascade order.
3700
+ // Marked so the template-switch cleanup keeps these (framework styles) but
3701
+ // still removes template-emitted <style> tags (which would otherwise pile up).
3395
3702
  for (const r of allResults) {
3396
3703
  const style = document.createElement('style');
3704
+ style.setAttribute('data-snice-style', '');
3397
3705
  style.textContent = r.cssText;
3398
3706
  element.shadowRoot.appendChild(style);
3399
3707
  }
@@ -3403,6 +3711,125 @@ function applyStyles(element) {
3403
3711
  }
3404
3712
  }
3405
3713
 
3714
+ // @dispatch decorator - auto-dispatches custom events from method return values
3715
+ /**
3716
+ * Decorator that automatically dispatches a custom event after a method is called.
3717
+ * The return value of the method becomes the event detail.
3718
+ *
3719
+ * @param eventName The name of the event to dispatch
3720
+ * @param options Optional configuration extending EventInit
3721
+ */
3722
+ function dispatch(eventName, options) {
3723
+ return function (originalMethod, _context) {
3724
+ return function (...args) {
3725
+ // Create timing wrappers for dispatch (per-instance)
3726
+ if (!this[DISPATCH_TIMERS]) {
3727
+ this[DISPATCH_TIMERS] = new Map();
3728
+ }
3729
+ const timerKey = `${eventName}_${_context.name}`;
3730
+ if (!this[DISPATCH_TIMERS].has(timerKey)) {
3731
+ this[DISPATCH_TIMERS].set(timerKey, {
3732
+ debounceTimeout: null,
3733
+ throttleLastCall: 0,
3734
+ throttleTimeout: null
3735
+ });
3736
+ }
3737
+ const timers = this[DISPATCH_TIMERS].get(timerKey);
3738
+ // Call the original method with preserved this context
3739
+ const result = originalMethod.apply(this, args);
3740
+ // Helper to dispatch the event
3741
+ const doDispatch = (detail) => {
3742
+ // Skip dispatch if result is undefined and dispatchOnUndefined is false
3743
+ if (detail === undefined && options?.dispatchOnUndefined === false) {
3744
+ return;
3745
+ }
3746
+ // Create event with spread operator for options
3747
+ const event = new CustomEvent(eventName, {
3748
+ bubbles: true, // Default to true for component events
3749
+ composed: true, // Allow crossing shadow DOM boundaries
3750
+ ...options, // Spread all EventInit options
3751
+ detail
3752
+ });
3753
+ // Resolve scope target. Default is `this` (host element).
3754
+ // Explicit scope dispatches the event on another target — useful for
3755
+ // 'global' bus events on document, or routing through an ancestor.
3756
+ if (options?.scope !== undefined) {
3757
+ const target = resolveScope(this, options.scope);
3758
+ if (!target) {
3759
+ console.warn(`[snice/@dispatch] scope did not resolve for "${eventName}" — event not dispatched.`);
3760
+ return;
3761
+ }
3762
+ target.dispatchEvent(event);
3763
+ return;
3764
+ }
3765
+ this.dispatchEvent(event);
3766
+ };
3767
+ // Helper to handle timed dispatch
3768
+ const timedDispatch = (detail) => {
3769
+ if (options?.debounce) {
3770
+ clearTimeout(timers.debounceTimeout);
3771
+ timers.debounceTimeout = setTimeout(() => doDispatch(detail), options.debounce);
3772
+ return;
3773
+ }
3774
+ if (!options?.throttle) {
3775
+ doDispatch(detail);
3776
+ return;
3777
+ }
3778
+ const now = Date.now();
3779
+ const remaining = options.throttle - (now - timers.throttleLastCall);
3780
+ if (remaining <= 0) {
3781
+ clearTimeout(timers.throttleTimeout);
3782
+ timers.throttleLastCall = now;
3783
+ doDispatch(detail);
3784
+ return;
3785
+ }
3786
+ // Record the LATEST detail so the trailing dispatch carries fresh
3787
+ // data, not the first-suppressed-call detail captured by closure.
3788
+ timers.latestDetail = detail;
3789
+ if (!timers.throttleTimeout) {
3790
+ timers.throttleTimeout = setTimeout(() => {
3791
+ timers.throttleLastCall = Date.now();
3792
+ timers.throttleTimeout = null;
3793
+ const d = timers.latestDetail;
3794
+ timers.latestDetail = undefined;
3795
+ doDispatch(d);
3796
+ }, remaining);
3797
+ }
3798
+ };
3799
+ // Handle async methods
3800
+ if (result instanceof Promise) {
3801
+ return result.then((resolvedResult) => {
3802
+ timedDispatch(resolvedResult);
3803
+ return resolvedResult;
3804
+ });
3805
+ }
3806
+ // Sync method
3807
+ timedDispatch(result);
3808
+ return result;
3809
+ };
3810
+ };
3811
+ }
3812
+ /**
3813
+ * Clear any pending debounce/throttle dispatch timers on an instance (e.g. on
3814
+ * disconnect, so a queued event doesn't fire into a detached node). A dispatch
3815
+ * is a one-shot signal, so pending ones are dropped, not replayed.
3816
+ */
3817
+ function clearDispatchTimers(instance) {
3818
+ const timers = instance[DISPATCH_TIMERS];
3819
+ if (!timers)
3820
+ return;
3821
+ for (const t of timers.values()) {
3822
+ if (t.debounceTimeout)
3823
+ clearTimeout(t.debounceTimeout);
3824
+ if (t.throttleTimeout)
3825
+ clearTimeout(t.throttleTimeout);
3826
+ t.debounceTimeout = null;
3827
+ t.throttleTimeout = null;
3828
+ t.throttleLastCall = 0;
3829
+ t.latestDetail = undefined;
3830
+ }
3831
+ }
3832
+
3406
3833
  /**
3407
3834
  * Method decorators for common patterns
3408
3835
  * @debounce, @throttle, @once, @memoize
@@ -3829,6 +4256,14 @@ function applyElementFunctionality(constructor) {
3829
4256
  }
3830
4257
  }
3831
4258
  }
4259
+ // Replay a render that was dropped while disconnected. The scheduler
4260
+ // sets this flag when it skips a detached element, so a property changed
4261
+ // while detached still reaches the DOM on reattach. A plain move (no
4262
+ // pending render) leaves the flag unset, so we don't re-render needlessly.
4263
+ if (this[RENDER_METHOD] && this[PENDING_RECONNECT_RENDER]) {
4264
+ this[PENDING_RECONNECT_RENDER] = false;
4265
+ requestRender(this);
4266
+ }
3832
4267
  return;
3833
4268
  }
3834
4269
  // Mark that properties are being initialized from attributes
@@ -3876,6 +4311,9 @@ function applyElementFunctionality(constructor) {
3876
4311
  setupResponseHandlers(this, this);
3877
4312
  setupContextHandler(this);
3878
4313
  this[INITIALIZED] = true;
4314
+ // Now that the initial value is settled, give @watch immediate handlers
4315
+ // their one init call (before the first render, so derived state is ready).
4316
+ invokeImmediateWatchers(this, constructor);
3879
4317
  if (originalConnectedCallback) {
3880
4318
  originalConnectedCallback.call(this);
3881
4319
  }
@@ -3970,6 +4408,17 @@ function applyElementFunctionality(constructor) {
3970
4408
  // Cleanup pending @debounce / @throttle timers so they don't fire on a dead element
3971
4409
  clearDebounceTimers(this);
3972
4410
  clearThrottleTimers(this);
4411
+ // The render/dispatch debounce-throttle timers live in separate stores.
4412
+ // A pending render is replayed on reconnect (same flag the scheduler
4413
+ // uses); a pending dispatch is a one-shot signal, so it's dropped.
4414
+ if (clearRenderTimers(this)) {
4415
+ this[PENDING_RECONNECT_RENDER] = true;
4416
+ }
4417
+ clearDispatchTimers(this);
4418
+ // @moved / @adopted debounce-throttle timers, likewise dropped so they
4419
+ // don't fire on a dead element.
4420
+ clearLifecycleTimers(this, MOVED_TIMERS);
4421
+ clearLifecycleTimers(this, ADOPTED_TIMERS);
3973
4422
  };
3974
4423
  constructor.prototype.attributeChangedCallback = function (name, oldValue, newValue) {
3975
4424
  originalAttributeChangedCallback?.call(this, name, oldValue, newValue);
@@ -3985,13 +4434,27 @@ function applyElementFunctionality(constructor) {
3985
4434
  if (attributeName.toLowerCase() !== name.toLowerCase())
3986
4435
  continue;
3987
4436
  const currentValue = this[PROPERTY_VALUES]?.[propName];
3988
- const parsedValue = parseAttributeValue(newValue, propOptions, currentValue, undefined);
3989
- if (currentValue === parsedValue)
4437
+ // A removed attribute reverts the property to its field default. Read it
4438
+ // back through the getter (attribute is already gone at this point) so the
4439
+ // watcher's newValue and PROPERTY_VALUES match what this[propName] now
4440
+ // returns — parseAttributeValue(null) would diverge to null for String/Number.
4441
+ const parsedValue = newValue === null
4442
+ ? this[propName]
4443
+ : parseAttributeValue(newValue, propOptions, currentValue, undefined);
4444
+ const changed = propOptions?.hasChanged
4445
+ ? propOptions.hasChanged(parsedValue, currentValue)
4446
+ : notEqual(parsedValue, currentValue);
4447
+ if (!changed)
3990
4448
  break;
3991
4449
  ensureSet(this, EXPLICITLY_SET_PROPERTIES).add(propName);
3992
4450
  ensureObj(this, PROPERTY_VALUES)[propName] = parsedValue;
3993
4451
  if (!this[SETTING_FROM_PROPERTY]?.has(name.toLowerCase())) {
3994
- invokeWatchers(this, constructor, propName, currentValue, parsedValue);
4452
+ // Watchers react to changes only. During upgrade the initial value
4453
+ // is not a change, so suppress until INITIALIZED; @watch immediate
4454
+ // handlers get their one init call from invokeImmediateWatchers.
4455
+ if (this[INITIALIZED]) {
4456
+ invokeWatchers(this, constructor, propName, currentValue, parsedValue);
4457
+ }
3995
4458
  if (this[RENDER_METHOD] && this[INITIALIZED]) {
3996
4459
  requestRender(this);
3997
4460
  }
@@ -4001,12 +4464,14 @@ function applyElementFunctionality(constructor) {
4001
4464
  };
4002
4465
  // Add connectedMoveCallback for handling DOM moves
4003
4466
  constructor.prototype.connectedMoveCallback = async function () {
4004
- // Call @moved handlers
4467
+ // Call @moved handlers via the prototype method (the debounce/throttle
4468
+ // wrapper), not the raw handler — otherwise @moved({debounce}) options
4469
+ // are ignored for real moves and only work when called directly.
4005
4470
  const movedHandlers = constructor[MOVED_HANDLERS];
4006
4471
  if (movedHandlers) {
4007
4472
  for (const handler of movedHandlers) {
4008
4473
  try {
4009
- await handler.method.call(this);
4474
+ await this[handler.methodName]();
4010
4475
  }
4011
4476
  catch (error) {
4012
4477
  console.error(`Error in @moved handler ${handler.methodName}:`, error);
@@ -4016,12 +4481,13 @@ function applyElementFunctionality(constructor) {
4016
4481
  };
4017
4482
  // Add adoptedCallback for handling document adoption
4018
4483
  constructor.prototype.adoptedCallback = async function () {
4019
- // Call @adopted handlers
4484
+ // Call @adopted handlers via the prototype method (the debounce/throttle
4485
+ // wrapper), not the raw handler — same reason as connectedMoveCallback.
4020
4486
  const adoptedHandlers = constructor[ADOPTED_HANDLERS];
4021
4487
  if (adoptedHandlers) {
4022
4488
  for (const handler of adoptedHandlers) {
4023
4489
  try {
4024
- await handler.method.call(this);
4490
+ await this[handler.methodName]();
4025
4491
  }
4026
4492
  catch (error) {
4027
4493
  console.error(`Error in @adopted handler ${handler.methodName}:`, error);
@@ -4186,7 +4652,12 @@ function property(options) {
4186
4652
  },
4187
4653
  set(newValue) {
4188
4654
  const oldValue = this[propertyKey];
4189
- if (oldValue === newValue)
4655
+ // A custom hasChanged comparator overrides the default === check
4656
+ // (e.g. deep-equal objects, or forcing an update on a mutated ref).
4657
+ const changed = finalOptions?.hasChanged
4658
+ ? finalOptions.hasChanged(newValue, oldValue)
4659
+ : notEqual(newValue, oldValue);
4660
+ if (!changed)
4190
4661
  return;
4191
4662
  // Pre-init: store for later, don't reflect to DOM yet
4192
4663
  if (!this[PROPERTIES_INITIALIZED]) {
@@ -4203,18 +4674,27 @@ function property(options) {
4203
4674
  const attributeName = getAttrName(finalOptions, propertyKey);
4204
4675
  const attributeValue = valueToAttribute(newValue, finalOptions, initialValue);
4205
4676
  ensureSet(this, EXPLICITLY_SET_PROPERTIES).add(propertyKey);
4206
- ensureSet(this, SETTING_FROM_PROPERTY).add(attributeName.toLowerCase());
4677
+ const attrKey = attributeName.toLowerCase();
4678
+ ensureSet(this, SETTING_FROM_PROPERTY).add(attrKey);
4679
+ // attributeChangedCallback runs synchronously inside setAttribute /
4680
+ // removeAttribute (custom element reactions are synchronous), so the
4681
+ // guard only needs to bracket this one reflection. Clear it right
4682
+ // after — not on a microtask — otherwise an external attribute change
4683
+ // in the same tick is mistaken for our own echo and silently dropped.
4207
4684
  if (attributeValue === null) {
4208
4685
  this.removeAttribute?.(attributeName);
4209
4686
  }
4210
4687
  else {
4211
4688
  this.setAttribute?.(attributeName, attributeValue);
4212
4689
  }
4213
- queueMicrotask(() => {
4214
- this[SETTING_FROM_PROPERTY]?.delete(attributeName.toLowerCase());
4215
- });
4690
+ this[SETTING_FROM_PROPERTY].delete(attrKey);
4691
+ }
4692
+ // See attributeChangedCallback: the initial value is not a change,
4693
+ // so suppress watchers until INITIALIZED. @watch immediate handlers
4694
+ // get their one init call from invokeImmediateWatchers.
4695
+ if (this[INITIALIZED]) {
4696
+ invokeWatchers(this, this.constructor, propertyKey, oldValue, newValue);
4216
4697
  }
4217
- invokeWatchers(this, this.constructor, propertyKey, oldValue, newValue);
4218
4698
  if (this[RENDER_METHOD] && this[INITIALIZED]) {
4219
4699
  requestRender(this);
4220
4700
  }
@@ -4284,7 +4764,12 @@ function queryAll(selector, options = {}) {
4284
4764
  };
4285
4765
  };
4286
4766
  }
4287
- function watch(...propertyNames) {
4767
+ function watch(...args) {
4768
+ // A trailing options object is separated from the watched property names.
4769
+ const propertyNames = args.filter((a) => typeof a === 'string');
4770
+ const options = args.find((a) => typeof a === 'object' && a !== null);
4771
+ // Fire on init by default; opt out of the init call with { immediate: false }.
4772
+ const immediate = options?.immediate !== false;
4288
4773
  return function (target, context) {
4289
4774
  const methodName = context.name;
4290
4775
  context.addInitializer(function () {
@@ -4324,7 +4809,8 @@ function watch(...propertyNames) {
4324
4809
  }
4325
4810
  constructor[PROPERTY_WATCHERS].get(propertyName).push({
4326
4811
  methodName,
4327
- method: target
4812
+ method: target,
4813
+ immediate
4328
4814
  });
4329
4815
  }
4330
4816
  });
@@ -4433,6 +4919,24 @@ function reconnect() {
4433
4919
  registerHandler(RECONNECT_HANDLERS, RECONNECT_METHODS, target, context);
4434
4920
  };
4435
4921
  }
4922
+ /**
4923
+ * Clear any pending @moved / @adopted debounce/throttle timers on an instance
4924
+ * (e.g. on disconnect), so they don't fire on a dead element.
4925
+ */
4926
+ function clearLifecycleTimers(instance, timersSymbol) {
4927
+ const map = instance[timersSymbol];
4928
+ if (!map)
4929
+ return;
4930
+ for (const t of map.values()) {
4931
+ if (t.debounceTimer)
4932
+ clearTimeout(t.debounceTimer);
4933
+ if (t.throttleTimer)
4934
+ clearTimeout(t.throttleTimer);
4935
+ t.debounceTimer = null;
4936
+ t.throttleTimer = null;
4937
+ t.lastThrottleCall = 0;
4938
+ }
4939
+ }
4436
4940
  function createLifecycleDecorator(handlersSymbol, timersSymbol, methodsSymbol) {
4437
4941
  return function (options = {}) {
4438
4942
  return function (originalMethod, context) {
@@ -4446,9 +4950,23 @@ function createLifecycleDecorator(handlersSymbol, timersSymbol, methodsSymbol) {
4446
4950
  }
4447
4951
  const timers = this[timersSymbol].get(methodName);
4448
4952
  const exec = (...a) => originalMethod.apply(this, a);
4953
+ // Deferred (debounce/throttle) calls run inside a setTimeout, outside
4954
+ // the caller's try/catch — so catch their sync throws and async
4955
+ // rejections here and log, rather than surface an uncaught error.
4956
+ const execDeferred = (...a) => {
4957
+ try {
4958
+ const result = originalMethod.apply(this, a);
4959
+ if (result && typeof result.then === 'function') {
4960
+ result.catch((error) => console.error(`Error in deferred lifecycle handler ${methodName}:`, error));
4961
+ }
4962
+ }
4963
+ catch (error) {
4964
+ console.error(`Error in deferred lifecycle handler ${methodName}:`, error);
4965
+ }
4966
+ };
4449
4967
  if (options.debounce > 0) {
4450
4968
  clearTimeout(timers.debounceTimer);
4451
- timers.debounceTimer = setTimeout(() => exec(...args), options.debounce);
4969
+ timers.debounceTimer = setTimeout(() => execDeferred(...args), options.debounce);
4452
4970
  return undefined;
4453
4971
  }
4454
4972
  if (options.throttle > 0) {
@@ -4462,7 +4980,7 @@ function createLifecycleDecorator(handlersSymbol, timersSymbol, methodsSymbol) {
4462
4980
  timers.throttleTimer = setTimeout(() => {
4463
4981
  timers.throttleTimer = null;
4464
4982
  timers.lastThrottleCall = Date.now();
4465
- exec(...args);
4983
+ execDeferred(...args);
4466
4984
  }, remaining);
4467
4985
  }
4468
4986
  return undefined;
@@ -5094,6 +5612,40 @@ class Route {
5094
5612
  }
5095
5613
  }
5096
5614
 
5615
+ /**
5616
+ * Score a route pattern by specificity so that more specific routes are tried
5617
+ * first. Scoring is per-segment by ROLE, not by string length: a static segment
5618
+ * outweighs a dynamic (`:param`) segment, which outweighs a wildcard (`*` /
5619
+ * `*name`). So `/post/new` beats `/post/:id`, and a splat always ranks last —
5620
+ * regardless of how long the pattern string happens to be. This mirrors the
5621
+ * segment-scoring model mainstream routers (React Router, Remix) use.
5622
+ *
5623
+ * Sort callers descending by this score (highest = most specific = first).
5624
+ *
5625
+ * NOTE: mirrored in `src/react/route-specificity.ts` for the isolated React
5626
+ * bundle — keep the two in sync.
5627
+ */
5628
+ const STATIC_SEGMENT = 10;
5629
+ const DYNAMIC_SEGMENT = 3;
5630
+ const WILDCARD_SEGMENT = -2;
5631
+ function routeSpecificity(spec) {
5632
+ const segments = spec.split('/').filter(Boolean);
5633
+ // Base on segment count so a deeper path outranks a shallower one on ties.
5634
+ let score = segments.length;
5635
+ for (const segment of segments) {
5636
+ if (segment.startsWith('*')) {
5637
+ score += WILDCARD_SEGMENT;
5638
+ }
5639
+ else if (segment.startsWith(':')) {
5640
+ score += DYNAMIC_SEGMENT;
5641
+ }
5642
+ else {
5643
+ score += STATIC_SEGMENT;
5644
+ }
5645
+ }
5646
+ return score;
5647
+ }
5648
+
5097
5649
  /**
5098
5650
  * Generic transition system for animating between elements
5099
5651
  */
@@ -5361,7 +5913,7 @@ function Router(options) {
5361
5913
  }
5362
5914
  const needsSorting = !is_sorted;
5363
5915
  if (needsSorting) {
5364
- routes.sort((a, b) => b.route.spec.length - a.route.spec.length);
5916
+ routes.sort((a, b) => routeSpecificity(b.route.spec) - routeSpecificity(a.route.spec));
5365
5917
  is_sorted = true;
5366
5918
  }
5367
5919
  // Collect placards from registered routes
@@ -5432,7 +5984,7 @@ function Router(options) {
5432
5984
  </div>`;
5433
5985
  return container;
5434
5986
  }
5435
- async function checkGuards(guards, params, target) {
5987
+ async function checkGuards(guards, params, target, stale) {
5436
5988
  const hasGuards = !!guards;
5437
5989
  if (!hasGuards) {
5438
5990
  return true;
@@ -5440,8 +5992,11 @@ function Router(options) {
5440
5992
  const guardsArray = Array.isArray(guards) ? guards : [guards];
5441
5993
  let spinner = null;
5442
5994
  let spinnerTimer = null;
5443
- // Show spinner after a short delay (avoids flash for fast sync guards)
5995
+ // Show spinner after a short delay (avoids flash for fast sync guards).
5996
+ // Skip if a newer navigation has already superseded this one.
5444
5997
  spinnerTimer = setTimeout(() => {
5998
+ if (stale && stale())
5999
+ return;
5445
6000
  spinner = createLoadingSpinner();
5446
6001
  target.appendChild(spinner);
5447
6002
  }, 50);
@@ -5449,7 +6004,11 @@ function Router(options) {
5449
6004
  for (const guard of guardsArray) {
5450
6005
  const allowed = await guard(context, params);
5451
6006
  if (!allowed) {
5452
- renderForbiddenPage(target);
6007
+ // Don't stomp the current page with a 403 if a newer navigation
6008
+ // already took over while this guard was resolving.
6009
+ if (!stale || !stale()) {
6010
+ renderForbiddenPage(target);
6011
+ }
5453
6012
  return false;
5454
6013
  }
5455
6014
  }
@@ -5485,14 +6044,14 @@ function Router(options) {
5485
6044
  div.innerHTML = /*html*/ `<h1>404</h1><p>Page not found</p>`;
5486
6045
  return { element: div, transition: undefined, layout: undefined };
5487
6046
  }
5488
- async function resolveRoute(path, target) {
6047
+ async function resolveRoute(path, target, stale) {
5489
6048
  for (const route of routes) {
5490
6049
  const params = route.route.match(path);
5491
6050
  const isMatch = params !== false;
5492
6051
  if (!isMatch) {
5493
6052
  continue;
5494
6053
  }
5495
- const guardsAllowed = await checkGuards(route.guards, params, target);
6054
+ const guardsAllowed = await checkGuards(route.guards, params, target, stale);
5496
6055
  if (!guardsAllowed) {
5497
6056
  return { result: RouteResult.GUARDS_FAILED };
5498
6057
  }
@@ -5620,7 +6179,7 @@ function Router(options) {
5620
6179
  const isHomePath = (path?.trim() === '' || path === '/') && !!home;
5621
6180
  if (isHomePath) {
5622
6181
  const homeRoute = routes.find(r => r.route.match('/'));
5623
- if (!(await checkGuards(homeRoute?.guards, {}, target)))
6182
+ if (!(await checkGuards(homeRoute?.guards, {}, target, stale)))
5624
6183
  return;
5625
6184
  if (stale())
5626
6185
  return;
@@ -5632,7 +6191,7 @@ function Router(options) {
5632
6191
  if (!path)
5633
6192
  return;
5634
6193
  // Resolve route
5635
- const routeResult = await resolveRoute(path, target);
6194
+ const routeResult = await resolveRoute(path, target, stale);
5636
6195
  if (stale())
5637
6196
  return;
5638
6197
  // Guards failed (403 already rendered by checkGuards)
@@ -5646,7 +6205,7 @@ function Router(options) {
5646
6205
  }
5647
6206
  // 404 fallthrough
5648
6207
  const { element, transition, layout } = create404Element();
5649
- await renderPage(target, element, transition, layout, path, {});
6208
+ await renderPage(target, element, transition, layout, path, {}, stale);
5650
6209
  }
5651
6210
  async function performTransition$1(container, oldElement, newElement, transition, atGen = navGeneration) {
5652
6211
  // `isStale` returns true when a newer navigate() has been kicked off.
@@ -5663,93 +6222,6 @@ function Router(options) {
5663
6222
  };
5664
6223
  }
5665
6224
 
5666
- // @dispatch decorator - auto-dispatches custom events from method return values
5667
- /**
5668
- * Decorator that automatically dispatches a custom event after a method is called.
5669
- * The return value of the method becomes the event detail.
5670
- *
5671
- * @param eventName The name of the event to dispatch
5672
- * @param options Optional configuration extending EventInit
5673
- */
5674
- function dispatch(eventName, options) {
5675
- return function (originalMethod, _context) {
5676
- return function (...args) {
5677
- // Create timing wrappers for dispatch (per-instance)
5678
- if (!this[DISPATCH_TIMERS]) {
5679
- this[DISPATCH_TIMERS] = new Map();
5680
- }
5681
- const timerKey = `${eventName}_${_context.name}`;
5682
- if (!this[DISPATCH_TIMERS].has(timerKey)) {
5683
- this[DISPATCH_TIMERS].set(timerKey, {
5684
- debounceTimeout: null,
5685
- throttleLastCall: 0,
5686
- throttleTimeout: null
5687
- });
5688
- }
5689
- const timers = this[DISPATCH_TIMERS].get(timerKey);
5690
- // Call the original method with preserved this context
5691
- const result = originalMethod.apply(this, args);
5692
- // Helper to dispatch the event
5693
- const doDispatch = (detail) => {
5694
- // Skip dispatch if result is undefined and dispatchOnUndefined is false
5695
- if (detail === undefined && options?.dispatchOnUndefined === false) {
5696
- return;
5697
- }
5698
- // Create event with spread operator for options
5699
- const event = new CustomEvent(eventName, {
5700
- bubbles: true, // Default to true for component events
5701
- composed: true, // Allow crossing shadow DOM boundaries
5702
- ...options, // Spread all EventInit options
5703
- detail
5704
- });
5705
- this.dispatchEvent(event);
5706
- };
5707
- // Helper to handle timed dispatch
5708
- const timedDispatch = (detail) => {
5709
- if (options?.debounce) {
5710
- clearTimeout(timers.debounceTimeout);
5711
- timers.debounceTimeout = setTimeout(() => doDispatch(detail), options.debounce);
5712
- return;
5713
- }
5714
- if (!options?.throttle) {
5715
- doDispatch(detail);
5716
- return;
5717
- }
5718
- const now = Date.now();
5719
- const remaining = options.throttle - (now - timers.throttleLastCall);
5720
- if (remaining <= 0) {
5721
- clearTimeout(timers.throttleTimeout);
5722
- timers.throttleLastCall = now;
5723
- doDispatch(detail);
5724
- return;
5725
- }
5726
- // Record the LATEST detail so the trailing dispatch carries fresh
5727
- // data, not the first-suppressed-call detail captured by closure.
5728
- timers.latestDetail = detail;
5729
- if (!timers.throttleTimeout) {
5730
- timers.throttleTimeout = setTimeout(() => {
5731
- timers.throttleLastCall = Date.now();
5732
- timers.throttleTimeout = null;
5733
- const d = timers.latestDetail;
5734
- timers.latestDetail = undefined;
5735
- doDispatch(d);
5736
- }, remaining);
5737
- }
5738
- };
5739
- // Handle async methods
5740
- if (result instanceof Promise) {
5741
- return result.then((resolvedResult) => {
5742
- timedDispatch(resolvedResult);
5743
- return resolvedResult;
5744
- });
5745
- }
5746
- // Sync method
5747
- timedDispatch(result);
5748
- return result;
5749
- };
5750
- };
5751
- }
5752
-
5753
6225
  /**
5754
6226
  * createRequestHandler — vanilla JS utility for handling @request channels
5755
6227
  * outside of the Snice element/controller decorator system.