snice 5.2.2 → 6.0.0

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