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