snice 2.5.4 → 3.2.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 (411) hide show
  1. package/README.md +537 -869
  2. package/bin/templates/base/src/components/counter-button.ts +13 -26
  3. package/bin/templates/base/src/controllers/counter-controller.ts +3 -3
  4. package/dist/components/accordion/snice-accordion-item.d.ts +4 -5
  5. package/dist/components/accordion/snice-accordion-item.js +37 -39
  6. package/dist/components/accordion/snice-accordion-item.js.map +1 -1
  7. package/dist/components/accordion/snice-accordion.d.ts +5 -11
  8. package/dist/components/accordion/snice-accordion.js +51 -52
  9. package/dist/components/accordion/snice-accordion.js.map +1 -1
  10. package/dist/components/alert/snice-alert.d.ts +2 -6
  11. package/dist/components/alert/snice-alert.js +41 -56
  12. package/dist/components/alert/snice-alert.js.map +1 -1
  13. package/dist/components/avatar/snice-avatar.d.ts +2 -6
  14. package/dist/components/avatar/snice-avatar.js +64 -71
  15. package/dist/components/avatar/snice-avatar.js.map +1 -1
  16. package/dist/components/badge/snice-badge.d.ts +2 -3
  17. package/dist/components/badge/snice-badge.js +22 -23
  18. package/dist/components/badge/snice-badge.js.map +1 -1
  19. package/dist/components/banner/snice-banner.d.ts +22 -0
  20. package/dist/components/banner/snice-banner.js +180 -0
  21. package/dist/components/banner/snice-banner.js.map +1 -0
  22. package/dist/components/banner/snice-banner.types.d.ts +14 -0
  23. package/dist/components/breadcrumbs/snice-breadcrumbs.d.ts +5 -12
  24. package/dist/components/breadcrumbs/snice-breadcrumbs.js +88 -89
  25. package/dist/components/breadcrumbs/snice-breadcrumbs.js.map +1 -1
  26. package/dist/components/button/snice-button.d.ts +3 -7
  27. package/dist/components/button/snice-button.js +37 -58
  28. package/dist/components/button/snice-button.js.map +1 -1
  29. package/dist/components/card/snice-card.d.ts +5 -8
  30. package/dist/components/card/snice-card.js +71 -56
  31. package/dist/components/card/snice-card.js.map +1 -1
  32. package/dist/components/checkbox/snice-checkbox.d.ts +4 -13
  33. package/dist/components/checkbox/snice-checkbox.js +66 -137
  34. package/dist/components/checkbox/snice-checkbox.js.map +1 -1
  35. package/dist/components/chip/snice-chip.d.ts +5 -11
  36. package/dist/components/chip/snice-chip.js +44 -47
  37. package/dist/components/chip/snice-chip.js.map +1 -1
  38. package/dist/components/color-display/snice-color-display.d.ts +14 -0
  39. package/dist/components/color-display/snice-color-display.js +151 -0
  40. package/dist/components/color-display/snice-color-display.js.map +1 -0
  41. package/dist/components/color-display/snice-color-display.types.d.ts +10 -0
  42. package/dist/components/color-picker/snice-color-picker.d.ts +50 -0
  43. package/dist/components/color-picker/snice-color-picker.js +489 -0
  44. package/dist/components/color-picker/snice-color-picker.js.map +1 -0
  45. package/dist/components/color-picker/snice-color-picker.types.d.ts +19 -0
  46. package/dist/components/date-picker/snice-date-picker.d.ts +11 -11
  47. package/dist/components/date-picker/snice-date-picker.js +134 -133
  48. package/dist/components/date-picker/snice-date-picker.js.map +1 -1
  49. package/dist/components/divider/snice-divider.d.ts +2 -4
  50. package/dist/components/divider/snice-divider.js +14 -22
  51. package/dist/components/divider/snice-divider.js.map +1 -1
  52. package/dist/components/drawer/snice-drawer.d.ts +4 -4
  53. package/dist/components/drawer/snice-drawer.js +25 -19
  54. package/dist/components/drawer/snice-drawer.js.map +1 -1
  55. package/dist/components/empty-state/snice-empty-state.d.ts +13 -0
  56. package/dist/components/empty-state/snice-empty-state.js +121 -0
  57. package/dist/components/empty-state/snice-empty-state.js.map +1 -0
  58. package/dist/components/empty-state/snice-empty-state.types.d.ts +9 -0
  59. package/dist/components/file-upload/snice-file-upload.d.ts +45 -0
  60. package/dist/components/file-upload/snice-file-upload.js +394 -0
  61. package/dist/components/file-upload/snice-file-upload.js.map +1 -0
  62. package/dist/components/file-upload/snice-file-upload.types.d.ts +22 -0
  63. package/dist/components/image/snice-image.d.ts +22 -0
  64. package/dist/components/image/snice-image.js +201 -0
  65. package/dist/components/image/snice-image.js.map +1 -0
  66. package/dist/components/image/snice-image.types.d.ts +17 -0
  67. package/dist/components/input/snice-input.d.ts +8 -6
  68. package/dist/components/input/snice-input.js +122 -105
  69. package/dist/components/input/snice-input.js.map +1 -1
  70. package/dist/components/kpi/snice-kpi.d.ts +16 -0
  71. package/dist/components/kpi/snice-kpi.js +162 -0
  72. package/dist/components/kpi/snice-kpi.js.map +1 -0
  73. package/dist/components/kpi/snice-kpi.types.d.ts +12 -0
  74. package/dist/components/layout/snice-layout-blog.d.ts +4 -4
  75. package/dist/components/layout/snice-layout-blog.js +21 -19
  76. package/dist/components/layout/snice-layout-blog.js.map +1 -1
  77. package/dist/components/layout/snice-layout-card.d.ts +2 -2
  78. package/dist/components/layout/snice-layout-card.js +16 -9
  79. package/dist/components/layout/snice-layout-card.js.map +1 -1
  80. package/dist/components/layout/snice-layout-centered.d.ts +2 -2
  81. package/dist/components/layout/snice-layout-centered.js +14 -7
  82. package/dist/components/layout/snice-layout-centered.js.map +1 -1
  83. package/dist/components/layout/snice-layout-dashboard.d.ts +5 -5
  84. package/dist/components/layout/snice-layout-dashboard.js +38 -30
  85. package/dist/components/layout/snice-layout-dashboard.js.map +1 -1
  86. package/dist/components/layout/snice-layout-fullscreen.d.ts +2 -2
  87. package/dist/components/layout/snice-layout-fullscreen.js +17 -10
  88. package/dist/components/layout/snice-layout-fullscreen.js.map +1 -1
  89. package/dist/components/layout/snice-layout-landing.d.ts +4 -4
  90. package/dist/components/layout/snice-layout-landing.js +21 -19
  91. package/dist/components/layout/snice-layout-landing.js.map +1 -1
  92. package/dist/components/layout/snice-layout-minimal.d.ts +2 -2
  93. package/dist/components/layout/snice-layout-minimal.js +17 -6
  94. package/dist/components/layout/snice-layout-minimal.js.map +1 -1
  95. package/dist/components/layout/snice-layout-sidebar.d.ts +5 -4
  96. package/dist/components/layout/snice-layout-sidebar.js +42 -20
  97. package/dist/components/layout/snice-layout-sidebar.js.map +1 -1
  98. package/dist/components/layout/snice-layout-split.d.ts +2 -2
  99. package/dist/components/layout/snice-layout-split.js +14 -7
  100. package/dist/components/layout/snice-layout-split.js.map +1 -1
  101. package/dist/components/layout/snice-layout.d.ts +4 -4
  102. package/dist/components/layout/snice-layout.js +16 -10
  103. package/dist/components/layout/snice-layout.js.map +1 -1
  104. package/dist/components/link/snice-link.d.ts +13 -0
  105. package/dist/components/link/snice-link.js +137 -0
  106. package/dist/components/link/snice-link.js.map +1 -0
  107. package/dist/components/link/snice-link.types.d.ts +11 -0
  108. package/dist/components/login/snice-login.d.ts +6 -11
  109. package/dist/components/login/snice-login.js +97 -71
  110. package/dist/components/login/snice-login.js.map +1 -1
  111. package/dist/components/modal/snice-modal.d.ts +5 -9
  112. package/dist/components/modal/snice-modal.js +47 -78
  113. package/dist/components/modal/snice-modal.js.map +1 -1
  114. package/dist/components/nav/snice-nav.d.ts +13 -7
  115. package/dist/components/nav/snice-nav.js +191 -100
  116. package/dist/components/nav/snice-nav.js.map +1 -1
  117. package/dist/components/nav/snice-nav.types.d.ts +3 -3
  118. package/dist/components/pagination/snice-pagination.d.ts +6 -7
  119. package/dist/components/pagination/snice-pagination.js +94 -81
  120. package/dist/components/pagination/snice-pagination.js.map +1 -1
  121. package/dist/components/progress/snice-progress.d.ts +2 -7
  122. package/dist/components/progress/snice-progress.js +41 -98
  123. package/dist/components/progress/snice-progress.js.map +1 -1
  124. package/dist/components/radio/snice-radio.d.ts +4 -4
  125. package/dist/components/radio/snice-radio.js +52 -44
  126. package/dist/components/radio/snice-radio.js.map +1 -1
  127. package/dist/components/select/snice-option.d.ts +2 -1
  128. package/dist/components/select/snice-option.js +12 -5
  129. package/dist/components/select/snice-option.js.map +1 -1
  130. package/dist/components/select/snice-select.d.ts +9 -21
  131. package/dist/components/select/snice-select.js +98 -170
  132. package/dist/components/select/snice-select.js.map +1 -1
  133. package/dist/components/skeleton/snice-skeleton.d.ts +2 -6
  134. package/dist/components/skeleton/snice-skeleton.js +18 -49
  135. package/dist/components/skeleton/snice-skeleton.js.map +1 -1
  136. package/dist/components/slider/snice-slider.d.ts +53 -0
  137. package/dist/components/slider/snice-slider.js +479 -0
  138. package/dist/components/slider/snice-slider.js.map +1 -0
  139. package/dist/components/slider/snice-slider.types.d.ts +26 -0
  140. package/dist/components/snice-cell-C0slgOpe.js +4 -0
  141. package/dist/components/snice-cell-C0slgOpe.js.map +1 -0
  142. package/dist/components/sparkline/snice-sparkline.d.ts +21 -0
  143. package/dist/components/sparkline/snice-sparkline.js +228 -0
  144. package/dist/components/sparkline/snice-sparkline.js.map +1 -0
  145. package/dist/components/sparkline/snice-sparkline.types.d.ts +16 -0
  146. package/dist/components/spinner/snice-spinner.d.ts +10 -0
  147. package/dist/components/spinner/snice-spinner.js +109 -0
  148. package/dist/components/spinner/snice-spinner.js.map +1 -0
  149. package/dist/components/spinner/snice-spinner.types.d.ts +8 -0
  150. package/dist/components/stepper/snice-stepper-panel.d.ts +8 -0
  151. package/dist/components/stepper/snice-stepper-panel.js +70 -0
  152. package/dist/components/stepper/snice-stepper-panel.js.map +1 -0
  153. package/dist/components/stepper/snice-stepper-panel.types.d.ts +4 -0
  154. package/dist/components/stepper/snice-stepper.d.ts +15 -0
  155. package/dist/components/stepper/snice-stepper.js +163 -0
  156. package/dist/components/stepper/snice-stepper.js.map +1 -0
  157. package/dist/components/stepper/snice-stepper.types.d.ts +13 -0
  158. package/dist/components/switch/snice-switch.d.ts +2 -2
  159. package/dist/components/switch/snice-switch.js +38 -26
  160. package/dist/components/switch/snice-switch.js.map +1 -1
  161. package/dist/components/table/snice-cell-actions.d.ts +24 -0
  162. package/dist/components/table/snice-cell-actions.js +149 -0
  163. package/dist/components/table/snice-cell-actions.js.map +1 -0
  164. package/dist/components/table/snice-cell-boolean.d.ts +2 -2
  165. package/dist/components/table/snice-cell-boolean.js +13 -7
  166. package/dist/components/table/snice-cell-boolean.js.map +1 -1
  167. package/dist/components/table/snice-cell-color.d.ts +18 -0
  168. package/dist/components/table/snice-cell-color.js +149 -0
  169. package/dist/components/table/snice-cell-color.js.map +1 -0
  170. package/dist/components/table/snice-cell-currency.d.ts +24 -0
  171. package/dist/components/table/snice-cell-currency.js +235 -0
  172. package/dist/components/table/snice-cell-currency.js.map +1 -0
  173. package/dist/components/table/snice-cell-date.d.ts +2 -2
  174. package/dist/components/table/snice-cell-date.js +14 -8
  175. package/dist/components/table/snice-cell-date.js.map +1 -1
  176. package/dist/components/table/snice-cell-duration.d.ts +2 -2
  177. package/dist/components/table/snice-cell-duration.js +12 -6
  178. package/dist/components/table/snice-cell-duration.js.map +1 -1
  179. package/dist/components/table/snice-cell-email.d.ts +15 -0
  180. package/dist/components/table/snice-cell-email.js +125 -0
  181. package/dist/components/table/snice-cell-email.js.map +1 -0
  182. package/dist/components/table/snice-cell-filesize.d.ts +2 -2
  183. package/dist/components/table/snice-cell-filesize.js +12 -6
  184. package/dist/components/table/snice-cell-filesize.js.map +1 -1
  185. package/dist/components/table/snice-cell-image.d.ts +20 -0
  186. package/dist/components/table/snice-cell-image.js +162 -0
  187. package/dist/components/table/snice-cell-image.js.map +1 -0
  188. package/dist/components/table/snice-cell-json.d.ts +20 -0
  189. package/dist/components/table/snice-cell-json.js +186 -0
  190. package/dist/components/table/snice-cell-json.js.map +1 -0
  191. package/dist/components/table/snice-cell-link.d.ts +17 -0
  192. package/dist/components/table/snice-cell-link.js +142 -0
  193. package/dist/components/table/snice-cell-link.js.map +1 -0
  194. package/dist/components/table/snice-cell-location.d.ts +19 -0
  195. package/dist/components/table/snice-cell-location.js +185 -0
  196. package/dist/components/table/snice-cell-location.js.map +1 -0
  197. package/dist/components/table/snice-cell-number.d.ts +2 -2
  198. package/dist/components/table/snice-cell-number.js +12 -6
  199. package/dist/components/table/snice-cell-number.js.map +1 -1
  200. package/dist/components/table/snice-cell-percentage.d.ts +22 -0
  201. package/dist/components/table/snice-cell-percentage.js +208 -0
  202. package/dist/components/table/snice-cell-percentage.js.map +1 -0
  203. package/dist/components/table/snice-cell-phone.d.ts +18 -0
  204. package/dist/components/table/snice-cell-phone.js +153 -0
  205. package/dist/components/table/snice-cell-phone.js.map +1 -0
  206. package/dist/components/table/snice-cell-progress.d.ts +2 -2
  207. package/dist/components/table/snice-cell-progress.js +12 -6
  208. package/dist/components/table/snice-cell-progress.js.map +1 -1
  209. package/dist/components/table/snice-cell-rating.d.ts +2 -2
  210. package/dist/components/table/snice-cell-rating.js +12 -6
  211. package/dist/components/table/snice-cell-rating.js.map +1 -1
  212. package/dist/components/table/snice-cell-sparkline.d.ts +2 -2
  213. package/dist/components/table/snice-cell-sparkline.js +13 -7
  214. package/dist/components/table/snice-cell-sparkline.js.map +1 -1
  215. package/dist/components/table/snice-cell-status.d.ts +17 -0
  216. package/dist/components/table/snice-cell-status.js +144 -0
  217. package/dist/components/table/snice-cell-status.js.map +1 -0
  218. package/dist/components/table/snice-cell-tag.d.ts +16 -0
  219. package/dist/components/table/snice-cell-tag.js +131 -0
  220. package/dist/components/table/snice-cell-tag.js.map +1 -0
  221. package/dist/components/table/snice-cell-text.d.ts +2 -2
  222. package/dist/components/table/snice-cell-text.js +14 -8
  223. package/dist/components/table/snice-cell-text.js.map +1 -1
  224. package/dist/components/table/snice-cell.d.ts +2 -2
  225. package/dist/components/table/snice-cell.js +12 -6
  226. package/dist/components/table/snice-cell.js.map +1 -1
  227. package/dist/components/table/snice-column.d.ts +1 -1
  228. package/dist/components/table/snice-column.js +6 -3
  229. package/dist/components/table/snice-column.js.map +1 -1
  230. package/dist/components/table/snice-header.d.ts +5 -5
  231. package/dist/components/table/snice-header.js +60 -50
  232. package/dist/components/table/snice-header.js.map +1 -1
  233. package/dist/components/table/snice-progress.d.ts +2 -2
  234. package/dist/components/table/snice-progress.js +18 -11
  235. package/dist/components/table/snice-progress.js.map +1 -1
  236. package/dist/components/table/snice-rating.d.ts +2 -2
  237. package/dist/components/table/snice-rating.js +15 -8
  238. package/dist/components/table/snice-rating.js.map +1 -1
  239. package/dist/components/table/snice-row.d.ts +17 -6
  240. package/dist/components/table/snice-row.js +95 -44
  241. package/dist/components/table/snice-row.js.map +1 -1
  242. package/dist/components/table/snice-table.d.ts +18 -10
  243. package/dist/components/table/snice-table.js +355 -173
  244. package/dist/components/table/snice-table.js.map +1 -1
  245. package/dist/components/table/snice-table.types.d.ts +101 -2
  246. package/dist/components/tabs/snice-tab-panel.d.ts +2 -2
  247. package/dist/components/tabs/snice-tab-panel.js +12 -6
  248. package/dist/components/tabs/snice-tab-panel.js.map +1 -1
  249. package/dist/components/tabs/snice-tab.d.ts +6 -5
  250. package/dist/components/tabs/snice-tab.js +36 -19
  251. package/dist/components/tabs/snice-tab.js.map +1 -1
  252. package/dist/components/tabs/snice-tabs.d.ts +5 -5
  253. package/dist/components/tabs/snice-tabs.js +38 -28
  254. package/dist/components/tabs/snice-tabs.js.map +1 -1
  255. package/dist/components/textarea/snice-textarea.d.ts +52 -0
  256. package/dist/components/textarea/snice-textarea.js +407 -0
  257. package/dist/components/textarea/snice-textarea.js.map +1 -0
  258. package/dist/components/textarea/snice-textarea.types.d.ts +30 -0
  259. package/dist/components/timeline/snice-timeline.d.ts +11 -0
  260. package/dist/components/timeline/snice-timeline.js +112 -0
  261. package/dist/components/timeline/snice-timeline.js.map +1 -0
  262. package/dist/components/timeline/snice-timeline.types.d.ts +16 -0
  263. package/dist/components/toast/snice-toast-container.d.ts +7 -7
  264. package/dist/components/toast/snice-toast-container.js +19 -12
  265. package/dist/components/toast/snice-toast-container.js.map +1 -1
  266. package/dist/components/toast/snice-toast.d.ts +3 -15
  267. package/dist/components/toast/snice-toast.js +49 -108
  268. package/dist/components/toast/snice-toast.js.map +1 -1
  269. package/dist/components/tooltip/snice-tooltip.d.ts +2 -2
  270. package/dist/components/tooltip/snice-tooltip.js +15 -8
  271. package/dist/components/tooltip/snice-tooltip.js.map +1 -1
  272. package/dist/context.d.ts +44 -0
  273. package/dist/element-ready.d.ts +40 -0
  274. package/dist/{types/element.d.ts → element.d.ts} +2 -8
  275. package/dist/{types/events.d.ts → events.d.ts} +0 -4
  276. package/dist/index.cjs +2556 -605
  277. package/dist/index.cjs.map +1 -1
  278. package/dist/index.d.ts +21 -0
  279. package/dist/index.esm.js +2535 -604
  280. package/dist/index.esm.js.map +1 -1
  281. package/dist/index.iife.js +2556 -605
  282. package/dist/index.iife.js.map +1 -1
  283. package/dist/method-decorators.d.ts +121 -0
  284. package/dist/on.d.ts +59 -0
  285. package/dist/parts.d.ts +156 -0
  286. package/dist/render-debug.d.ts +27 -0
  287. package/dist/render-tracker.d.ts +14 -0
  288. package/dist/render.d.ts +96 -0
  289. package/dist/symbols.cjs +163 -0
  290. package/dist/symbols.cjs.map +1 -1
  291. package/dist/{types/symbols.d.ts → symbols.d.ts} +22 -0
  292. package/dist/symbols.esm.js +27 -3
  293. package/dist/symbols.esm.js.map +1 -1
  294. package/dist/template.d.ts +99 -0
  295. package/dist/transitions.cjs +219 -0
  296. package/dist/transitions.esm.js +2 -2
  297. package/dist/types/context.d.ts +48 -0
  298. package/dist/types/element-options.d.ts +26 -0
  299. package/dist/types/index.d.ts +25 -9
  300. package/dist/types/nav-context.d.ts +19 -0
  301. package/dist/types/{types/on-options.d.ts → on-options.d.ts} +2 -0
  302. package/dist/types/{types/placard.d.ts → placard.d.ts} +0 -1
  303. package/docs/ai/README.md +26 -0
  304. package/docs/ai/api.md +175 -0
  305. package/docs/ai/architecture.md +160 -0
  306. package/docs/ai/components/accordion.md +174 -0
  307. package/docs/ai/components/alert.md +77 -0
  308. package/docs/ai/components/avatar.md +61 -0
  309. package/docs/ai/components/badge.md +69 -0
  310. package/docs/ai/components/banner.md +84 -0
  311. package/docs/ai/components/breadcrumbs.md +74 -0
  312. package/docs/ai/components/button.md +75 -0
  313. package/docs/ai/components/card.md +61 -0
  314. package/docs/ai/components/checkbox.md +74 -0
  315. package/docs/ai/components/chip.md +73 -0
  316. package/docs/ai/components/color-display.md +48 -0
  317. package/docs/ai/components/color-picker.md +75 -0
  318. package/docs/ai/components/date-picker.md +75 -0
  319. package/docs/ai/components/divider.md +66 -0
  320. package/docs/ai/components/drawer.md +80 -0
  321. package/docs/ai/components/empty-state.md +72 -0
  322. package/docs/ai/components/file-upload.md +93 -0
  323. package/docs/ai/components/image.md +60 -0
  324. package/docs/ai/components/input.md +111 -0
  325. package/docs/ai/components/kpi.md +158 -0
  326. package/docs/ai/components/link.md +77 -0
  327. package/docs/ai/components/login.md +109 -0
  328. package/docs/ai/components/modal.md +67 -0
  329. package/docs/ai/components/nav.md +76 -0
  330. package/docs/ai/components/pagination.md +55 -0
  331. package/docs/ai/components/progress.md +72 -0
  332. package/docs/ai/components/radio.md +79 -0
  333. package/docs/ai/components/select.md +92 -0
  334. package/docs/ai/components/skeleton.md +57 -0
  335. package/docs/ai/components/slider.md +87 -0
  336. package/docs/ai/components/sparkline.md +168 -0
  337. package/docs/ai/components/spinner.md +47 -0
  338. package/docs/ai/components/stepper.md +216 -0
  339. package/docs/ai/components/switch.md +53 -0
  340. package/docs/ai/components/table.md +227 -0
  341. package/docs/ai/components/tabs.md +83 -0
  342. package/docs/ai/components/textarea.md +87 -0
  343. package/docs/ai/components/timeline.md +77 -0
  344. package/docs/ai/components/toast.md +140 -0
  345. package/docs/ai/components/tooltip.md +146 -0
  346. package/docs/ai/patterns.md +244 -0
  347. package/docs/components/accordion.md +558 -0
  348. package/docs/components/banner.md +106 -0
  349. package/docs/components/color-display.md +96 -0
  350. package/docs/components/color-picker.md +81 -0
  351. package/docs/components/drawer.md +602 -0
  352. package/docs/components/empty-state.md +79 -0
  353. package/docs/components/file-upload.md +263 -0
  354. package/docs/components/image.md +110 -0
  355. package/docs/components/kpi.md +251 -0
  356. package/docs/components/link.md +229 -0
  357. package/docs/components/modal.md +558 -0
  358. package/docs/components/nav.md +239 -0
  359. package/docs/components/pagination.md +289 -0
  360. package/docs/components/select.md +599 -0
  361. package/docs/components/slider.md +297 -0
  362. package/docs/components/sparkline.md +293 -0
  363. package/docs/components/spinner.md +63 -0
  364. package/docs/components/stepper.md +410 -0
  365. package/docs/components/switch.md +354 -0
  366. package/docs/components/tabs.md +546 -0
  367. package/docs/components/textarea.md +235 -0
  368. package/docs/components/timeline.md +192 -0
  369. package/docs/components/toast.md +506 -0
  370. package/docs/components/tooltip.md +523 -0
  371. package/docs/controllers.md +744 -0
  372. package/docs/elements.md +855 -0
  373. package/docs/events.md +807 -0
  374. package/docs/migration-v2-to-v3.md +569 -0
  375. package/docs/observe.md +588 -0
  376. package/docs/placards.md +401 -0
  377. package/docs/request-response.md +852 -0
  378. package/docs/routing.md +1186 -0
  379. package/package.json +11 -11
  380. package/dist/components/snice-cell-C9N6yGxQ.js +0 -4
  381. package/dist/components/snice-cell-C9N6yGxQ.js.map +0 -1
  382. package/dist/types/types/index.d.ts +0 -23
  383. /package/dist/{types/controller.d.ts → controller.d.ts} +0 -0
  384. /package/dist/{types/global.d.ts → global.d.ts} +0 -0
  385. /package/dist/{types/observe.d.ts → observe.d.ts} +0 -0
  386. /package/dist/{types/request-response.d.ts → request-response.d.ts} +0 -0
  387. /package/dist/{types/router.d.ts → router.d.ts} +0 -0
  388. /package/dist/{types/testing.d.ts → testing.d.ts} +0 -0
  389. /package/dist/{types/transitions.d.ts → transitions.d.ts} +0 -0
  390. /package/dist/types/{types/adopted-options.d.ts → adopted-options.d.ts} +0 -0
  391. /package/dist/types/{types/app-context.d.ts → app-context.d.ts} +0 -0
  392. /package/dist/types/{types/dispatch-options.d.ts → dispatch-options.d.ts} +0 -0
  393. /package/dist/types/{types/guard.d.ts → guard.d.ts} +0 -0
  394. /package/dist/types/{types/i-controller.d.ts → i-controller.d.ts} +0 -0
  395. /package/dist/types/{types/moved-options.d.ts → moved-options.d.ts} +0 -0
  396. /package/dist/types/{types/observe-options.d.ts → observe-options.d.ts} +0 -0
  397. /package/dist/types/{types/page-options.d.ts → page-options.d.ts} +0 -0
  398. /package/dist/types/{types/part-options.d.ts → part-options.d.ts} +0 -0
  399. /package/dist/types/{types/property-converter.d.ts → property-converter.d.ts} +0 -0
  400. /package/dist/types/{types/property-options.d.ts → property-options.d.ts} +0 -0
  401. /package/dist/types/{types/query-options.d.ts → query-options.d.ts} +0 -0
  402. /package/dist/types/{types/request-options.d.ts → request-options.d.ts} +0 -0
  403. /package/dist/types/{types/respond-options.d.ts → respond-options.d.ts} +0 -0
  404. /package/dist/types/{types/route-params.d.ts → route-params.d.ts} +0 -0
  405. /package/dist/types/{types/router-instance.d.ts → router-instance.d.ts} +0 -0
  406. /package/dist/types/{types/router-options.d.ts → router-options.d.ts} +0 -0
  407. /package/dist/types/{types/simple-array.d.ts → simple-array.d.ts} +0 -0
  408. /package/dist/types/{types/snice-element.d.ts → snice-element.d.ts} +0 -0
  409. /package/dist/types/{types/snice-global.d.ts → snice-global.d.ts} +0 -0
  410. /package/dist/types/{types/transition.d.ts → transition.d.ts} +0 -0
  411. /package/dist/{types/utils.d.ts → utils.d.ts} +0 -0
package/dist/index.esm.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /*!
2
- * snice v2.5.3
3
- * Imperative TypeScript framework for building vanilla web components with decorators, routing, and controllers. No virtual DOM, no build complexity.
2
+ * snice v3.1.0
3
+ * Imperative TypeScript framework for building vanilla web components with decorators, differential rendering, routing, and controllers. No virtual DOM, no build complexity.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
6
6
  */
@@ -36,10 +36,12 @@ const CHANNEL_HANDLERS = getSymbol('channel-handlers');
36
36
  // Internal element state symbols
37
37
  const READY_PROMISE = getSymbol('ready-promise');
38
38
  const READY_RESOLVE = getSymbol('ready-resolve');
39
+ getSymbol('rendered-promise');
40
+ getSymbol('rendered-resolve');
39
41
  const CONTROLLER = getSymbol('controller');
40
42
  const INITIALIZED = getSymbol('initialized');
41
43
  // Event handler symbols
42
- const ON_HANDLERS = getSymbol('on-handlers');
44
+ getSymbol('on-handlers');
43
45
  // Controller symbols
44
46
  const CONTROLLER_KEY = getSymbol('controller-key');
45
47
  const CONTROLLER_NAME_KEY = getSymbol('controller-name');
@@ -51,13 +53,14 @@ const CLEANUP = getSymbol('cleanup');
51
53
  // Property symbols
52
54
  const PROPERTIES = getSymbol('properties');
53
55
  const PROPERTY_VALUES = getSymbol('property-values');
56
+ const PRE_INIT_PROPERTY_VALUES = getSymbol('pre-init-property-values');
54
57
  const PROPERTIES_INITIALIZED = getSymbol('properties-initialized');
55
58
  const PROPERTY_WATCHERS = getSymbol('property-watchers');
56
59
  const EXPLICITLY_SET_PROPERTIES = getSymbol('explicitly-set-properties');
57
60
  // Router context symbol
58
61
  const ROUTER_CONTEXT = getSymbol('router-context');
59
62
  getSymbol('current-page-marker');
60
- const CONTEXT_REQUEST_HANDLER = getSymbol('context-request-handler');
63
+ getSymbol('context-request-handler');
61
64
  const PAGE_TRANSITION = getSymbol('page-transition');
62
65
  const CREATED_AT = getSymbol('created-at');
63
66
  // Lifecycle symbols
@@ -68,313 +71,34 @@ const ADOPTED_HANDLERS = getSymbol('adopted-handlers');
68
71
  // Observer symbols
69
72
  const OBSERVERS = getSymbol('observers');
70
73
  // Part symbols
71
- const PARTS = getSymbol('parts');
72
- const PART_TIMERS = getSymbol('part-timers');
74
+ getSymbol('parts');
75
+ getSymbol('part-timers');
73
76
  // Lifecycle callback timers
74
77
  const MOVED_TIMERS = getSymbol('moved-timers');
75
78
  const ADOPTED_TIMERS = getSymbol('adopted-timers');
76
79
  // Dispatch timing symbols
77
80
  const DISPATCH_TIMERS = getSymbol('dispatch-timers');
78
-
79
- function on(eventName, selectorOrOptions, options) {
80
- // Handle overloaded parameters
81
- let selector;
82
- let opts;
83
- if (typeof selectorOrOptions === 'string') {
84
- selector = selectorOrOptions;
85
- opts = options;
86
- }
87
- else {
88
- selector = undefined;
89
- opts = selectorOrOptions;
90
- }
91
- return function (target, context) {
92
- const propertyKey = context.name;
93
- context.addInitializer(function () {
94
- const constructor = this.constructor;
95
- // Store event handler metadata
96
- if (!constructor.prototype[ON_HANDLERS]) {
97
- constructor.prototype[ON_HANDLERS] = [];
98
- }
99
- // Normalize to array and expand at decoration time
100
- const eventNames = Array.isArray(eventName) ? eventName : [eventName];
101
- // Create a handler entry for each event
102
- for (const event of eventNames) {
103
- constructor.prototype[ON_HANDLERS].push({
104
- eventName: event,
105
- selector,
106
- methodName: propertyKey,
107
- method: target,
108
- options: opts
109
- });
110
- }
111
- });
112
- };
113
- }
114
- // Helper to setup event handlers for elements
115
- function setupEventHandlers(instance, element) {
116
- const handlers = instance.constructor.prototype[ON_HANDLERS];
117
- if (!handlers)
118
- return;
119
- // Initialize cleanup object if needed
120
- if (!instance[CLEANUP]) {
121
- instance[CLEANUP] = { events: [], channels: [] };
122
- }
123
- for (const handler of handlers) {
124
- // Get the current method from the instance (preserves decorator stacking)
125
- const currentMethod = instance[handler.method.name];
126
- const originalMethod = currentMethod ? currentMethod.bind(instance) : handler.method.bind(instance);
127
- const handlerOptions = handler.options || {};
128
- // Parse event name for key modifiers
129
- const [baseEventName, keyModifier] = handler.eventName.split(':');
130
- // Create debounced/throttled wrapper if needed
131
- const createTimedWrapper = (method) => {
132
- if (handlerOptions.debounce) {
133
- let timeoutId;
134
- return function (...args) {
135
- clearTimeout(timeoutId);
136
- timeoutId = setTimeout(() => method.apply(this, args), handlerOptions.debounce);
137
- };
138
- }
139
- if (handlerOptions.throttle) {
140
- let lastCall = 0;
141
- let timeoutId;
142
- return function (...args) {
143
- const now = Date.now();
144
- const remaining = handlerOptions.throttle - (now - lastCall);
145
- if (remaining <= 0) {
146
- clearTimeout(timeoutId);
147
- lastCall = now;
148
- method.apply(this, args);
149
- }
150
- else if (!timeoutId) {
151
- timeoutId = setTimeout(() => {
152
- lastCall = Date.now();
153
- timeoutId = null;
154
- method.apply(this, args);
155
- }, remaining);
156
- }
157
- };
158
- }
159
- return method;
160
- };
161
- // Create the event handler with key modifier support
162
- const createEventHandler = (method) => {
163
- if (keyModifier && (baseEventName === 'keydown' || baseEventName === 'keyup' || baseEventName === 'keypress')) {
164
- return (event) => {
165
- const keyEvent = event;
166
- // Helper to normalize key names (e.g., "Space" -> " ")
167
- const normalizeKey = (key) => {
168
- if (key === 'Space')
169
- return ' ';
170
- return key;
171
- };
172
- // Check for "any modifiers" match with ~ prefix
173
- if (keyModifier.startsWith('~')) {
174
- const key = normalizeKey(keyModifier.slice(1)); // Remove the ~ and normalize
175
- // Match if key matches, regardless of modifiers
176
- if (keyEvent.key === key) {
177
- method(event);
178
- }
179
- return;
180
- }
181
- // Check for modifier combinations using +
182
- if (keyModifier.includes('+')) {
183
- const parts = keyModifier.split('+');
184
- const key = normalizeKey(parts[parts.length - 1]); // Last part is the actual key
185
- const modifiers = parts.slice(0, -1); // Everything else is modifiers
186
- // Check the actual key
187
- if (keyEvent.key !== key)
188
- return;
189
- // Create a set of expected modifiers
190
- const expectedModifiers = new Set(modifiers.map((m) => m.toLowerCase()));
191
- const hasCtrl = expectedModifiers.has('ctrl');
192
- const hasShift = expectedModifiers.has('shift');
193
- const hasAlt = expectedModifiers.has('alt');
194
- const hasMeta = expectedModifiers.has('meta') || expectedModifiers.has('cmd');
195
- // Check that expected modifiers are pressed and unexpected ones are not
196
- const modifiersMatch = keyEvent.ctrlKey === hasCtrl &&
197
- keyEvent.shiftKey === hasShift &&
198
- keyEvent.altKey === hasAlt &&
199
- keyEvent.metaKey === hasMeta;
200
- if (modifiersMatch) {
201
- method(event);
202
- }
203
- }
204
- else {
205
- // Default: exact match (no modifiers allowed)
206
- const key = normalizeKey(keyModifier);
207
- // Only match if key matches AND no modifiers are pressed
208
- if (keyEvent.key === key &&
209
- !keyEvent.ctrlKey &&
210
- !keyEvent.shiftKey &&
211
- !keyEvent.altKey &&
212
- !keyEvent.metaKey) {
213
- method(event);
214
- }
215
- }
216
- };
217
- }
218
- return method;
219
- };
220
- // Apply timing wrapper (debounce/throttle)
221
- const timedMethod = createTimedWrapper(originalMethod);
222
- // Wrap boundMethod in try-catch for error isolation
223
- const wrappedMethod = createEventHandler((event) => {
224
- try {
225
- // Apply automatic preventDefault/stopPropagation if configured
226
- if (handlerOptions.preventDefault) {
227
- event.preventDefault();
228
- }
229
- if (handlerOptions.stopPropagation) {
230
- event.stopPropagation();
231
- }
232
- return timedMethod(event);
233
- }
234
- catch (error) {
235
- console.error(`Error in event handler ${handler.methodName}:`, error);
236
- // Don't rethrow - allow other handlers to continue
237
- }
238
- });
239
- if (handler.selector) {
240
- // Delegated event handling - use shadow root if available
241
- const eventRoot = element.shadowRoot || element;
242
- const delegatedHandler = (event) => {
243
- const target = event.target;
244
- let shouldHandle = false;
245
- if (target.matches && target.matches(handler.selector)) {
246
- shouldHandle = true;
247
- }
248
- else if (target.closest) {
249
- const closest = target.closest(handler.selector);
250
- if (closest) {
251
- shouldHandle = true;
252
- }
253
- }
254
- if (shouldHandle) {
255
- // Apply automatic preventDefault/stopPropagation only if we're handling this event
256
- if (handlerOptions.preventDefault) {
257
- event.preventDefault();
258
- }
259
- if (handlerOptions.stopPropagation) {
260
- event.stopPropagation();
261
- event.stopImmediatePropagation(); // Also stop other handlers on same element
262
- }
263
- wrappedMethod(event);
264
- }
265
- };
266
- const listenerOptions = {
267
- capture: handlerOptions.capture || false,
268
- once: handlerOptions.once || false,
269
- passive: handlerOptions.passive || false
270
- };
271
- eventRoot.addEventListener(baseEventName, delegatedHandler, listenerOptions);
272
- instance[CLEANUP].events.push(() => {
273
- eventRoot.removeEventListener(baseEventName, delegatedHandler, listenerOptions);
274
- });
275
- }
276
- else {
277
- // Direct event handling - always on the element itself
278
- const listenerOptions = {
279
- capture: handlerOptions.capture || false,
280
- once: handlerOptions.once || false,
281
- passive: handlerOptions.passive || false
282
- };
283
- element.addEventListener(baseEventName, wrappedMethod, listenerOptions);
284
- instance[CLEANUP].events.push(() => {
285
- element.removeEventListener(baseEventName, wrappedMethod, listenerOptions);
286
- });
287
- }
288
- }
289
- }
290
- // Helper to cleanup event handlers
291
- function cleanupEventHandlers(instance) {
292
- if (instance[CLEANUP]?.events) {
293
- for (const cleanup of instance[CLEANUP].events) {
294
- cleanup();
295
- }
296
- instance[CLEANUP].events = [];
297
- }
298
- }
299
- /**
300
- * Decorator that automatically dispatches a custom event after a method is called.
301
- * The return value of the method becomes the event detail.
302
- *
303
- * @param eventName The name of the event to dispatch
304
- * @param options Optional configuration extending EventInit
305
- */
306
- function dispatch(eventName, options) {
307
- return function (originalMethod, _context) {
308
- return function (...args) {
309
- // Create timing wrappers for dispatch (per-instance)
310
- if (!this[DISPATCH_TIMERS]) {
311
- this[DISPATCH_TIMERS] = new Map();
312
- }
313
- const timerKey = `${eventName}_${_context.name}`;
314
- if (!this[DISPATCH_TIMERS].has(timerKey)) {
315
- this[DISPATCH_TIMERS].set(timerKey, {
316
- debounceTimeout: null,
317
- throttleLastCall: 0,
318
- throttleTimeout: null
319
- });
320
- }
321
- const timers = this[DISPATCH_TIMERS].get(timerKey);
322
- // Call the original method with preserved this context
323
- const result = originalMethod.apply(this, args);
324
- // Helper to dispatch the event
325
- const doDispatch = (detail) => {
326
- // Skip dispatch if result is undefined and dispatchOnUndefined is false
327
- if (detail === undefined && options?.dispatchOnUndefined === false) {
328
- return;
329
- }
330
- // Create event with spread operator for options
331
- const event = new CustomEvent(eventName, {
332
- bubbles: true, // Default to true for component events
333
- composed: true, // Allow crossing shadow DOM boundaries
334
- ...options, // Spread all EventInit options
335
- detail
336
- });
337
- this.dispatchEvent(event);
338
- };
339
- // Helper to handle timed dispatch
340
- const timedDispatch = (detail) => {
341
- if (options?.debounce) {
342
- clearTimeout(timers.debounceTimeout);
343
- timers.debounceTimeout = setTimeout(() => doDispatch(detail), options.debounce);
344
- }
345
- else if (options?.throttle) {
346
- const now = Date.now();
347
- const remaining = options.throttle - (now - timers.throttleLastCall);
348
- if (remaining <= 0) {
349
- clearTimeout(timers.throttleTimeout);
350
- timers.throttleLastCall = now;
351
- doDispatch(detail);
352
- }
353
- else if (!timers.throttleTimeout) {
354
- timers.throttleTimeout = setTimeout(() => {
355
- timers.throttleLastCall = Date.now();
356
- timers.throttleTimeout = null;
357
- doDispatch(detail);
358
- }, remaining);
359
- }
360
- }
361
- else {
362
- doDispatch(detail);
363
- }
364
- };
365
- // Handle async methods
366
- if (result instanceof Promise) {
367
- return result.then((resolvedResult) => {
368
- timedDispatch(resolvedResult);
369
- return resolvedResult;
370
- });
371
- }
372
- // Sync method
373
- timedDispatch(result);
374
- return result;
375
- };
376
- };
377
- }
81
+ // Render symbols (v3.0.0)
82
+ const RENDER_METHOD = getSymbol('render-method');
83
+ const RENDER_OPTIONS = getSymbol('render-options');
84
+ const RENDER_INSTANCE = getSymbol('render-instance');
85
+ const RENDER_SCHEDULED = getSymbol('render-scheduled');
86
+ const RENDER_TIMERS = getSymbol('render-timers');
87
+ const RENDER_CALLBACKS = getSymbol('render-callbacks');
88
+ const STYLES_METHOD = getSymbol('styles-method');
89
+ const STYLES_APPLIED = getSymbol('styles-applied');
90
+ // Navigation context symbols
91
+ const CONTEXT_HANDLER = getSymbol('context-handler');
92
+ getSymbol('context-method-name');
93
+ const NAVIGATION_CONTEXT_INSTANCE = getSymbol('navigation-context-instance');
94
+ const REGISTERED_ELEMENTS = getSymbol('registered-elements');
95
+ const IS_UPDATING = getSymbol('is-updating');
96
+ const CONTEXT_REGISTER = getSymbol('context-register');
97
+ const CONTEXT_UNREGISTER = getSymbol('context-unregister');
98
+ const CONTEXT_NOTIFY_ELEMENT = getSymbol('context-notify-element');
99
+ getSymbol('context-options');
100
+ const CONTEXT_TIMER = getSymbol('context-timer');
101
+ const CONTEXT_CALLED = getSymbol('context-called');
378
102
 
379
103
  // Global cache for MediaQueryList objects
380
104
  const mediaQueryCache = new Map();
@@ -401,21 +125,22 @@ function observe(observeTarget, selectorOrOptions, options) {
401
125
  }
402
126
  return function (target, context) {
403
127
  const propertyKey = context.name;
128
+ const initKey = `__observe_init_${propertyKey}`;
404
129
  context.addInitializer(function () {
405
130
  const constructor = this.constructor;
406
- // Store observer metadata
407
- if (!constructor.prototype[OBSERVERS]) {
408
- constructor.prototype[OBSERVERS] = [];
131
+ if (constructor[initKey])
132
+ return;
133
+ constructor[initKey] = true;
134
+ if (!constructor[OBSERVERS]) {
135
+ constructor[OBSERVERS] = [];
409
136
  }
410
- // Normalize to array
411
137
  const observeTargets = Array.isArray(observeTarget) ? observeTarget : [observeTarget];
412
- // Create an observer entry for each target
413
138
  for (const targetString of observeTargets) {
414
- // Parse the observation type from the observeTarget string
415
139
  const [type, ...modifiers] = targetString.split(':');
416
- constructor.prototype[OBSERVERS].push({
140
+ const targetStr = modifiers.join(':');
141
+ constructor[OBSERVERS].push({
417
142
  type,
418
- target: modifiers.join(':'), // Rejoin for media queries or mutation types
143
+ target: targetStr,
419
144
  selector,
420
145
  methodName: propertyKey,
421
146
  method: target,
@@ -427,8 +152,7 @@ function observe(observeTarget, selectorOrOptions, options) {
427
152
  }
428
153
  // Helper to setup observers for elements
429
154
  function setupObservers(instance, element) {
430
- // Only check the prototype, not the instance itself to avoid property access issues
431
- const observers = instance.constructor.prototype[OBSERVERS];
155
+ const observers = instance.constructor[OBSERVERS];
432
156
  if (!observers || !Array.isArray(observers) || observers.length === 0) {
433
157
  return;
434
158
  }
@@ -856,14 +580,16 @@ function request(requestName, options) {
856
580
  function respond(requestName, options) {
857
581
  return function (target, context) {
858
582
  const propertyKey = context.name;
583
+ const initKey = `__respond_init_${requestName}_${propertyKey}`;
859
584
  context.addInitializer(function () {
860
585
  const constructor = this.constructor;
861
- // Store response metadata on the prototype
862
- // This will be picked up by setupResponseHandlers
863
- if (!constructor.prototype[CHANNEL_HANDLERS]) {
864
- constructor.prototype[CHANNEL_HANDLERS] = [];
586
+ if (constructor[initKey])
587
+ return;
588
+ constructor[initKey] = true;
589
+ if (!constructor[CHANNEL_HANDLERS]) {
590
+ constructor[CHANNEL_HANDLERS] = [];
865
591
  }
866
- constructor.prototype[CHANNEL_HANDLERS].push({
592
+ constructor[CHANNEL_HANDLERS].push({
867
593
  channelName: requestName,
868
594
  methodName: propertyKey,
869
595
  method: target,
@@ -874,7 +600,7 @@ function respond(requestName, options) {
874
600
  }
875
601
  // Helper to setup response handlers for elements and controllers
876
602
  function setupResponseHandlers(instance, element) {
877
- const handlers = instance.constructor.prototype[CHANNEL_HANDLERS];
603
+ const handlers = instance.constructor[CHANNEL_HANDLERS];
878
604
  if (!handlers)
879
605
  return;
880
606
  // Store cleanup functions
@@ -974,71 +700,1263 @@ function cleanupResponseHandlers(instance) {
974
700
  }
975
701
  }
976
702
 
977
- // Controller-scoped cleanup registry
978
- class ControllerScope {
979
- constructor() {
980
- this.cleanupFns = new Map();
981
- this.pendingOperations = new Set();
982
- }
983
- register(key, cleanup) {
984
- this.cleanupFns.set(key, cleanup);
985
- }
986
- unregister(key) {
987
- this.cleanupFns.delete(key);
988
- }
989
- async cleanup() {
990
- // Wait for all pending operations
991
- await Promise.all(this.pendingOperations);
992
- // Run all cleanup functions
993
- for (const cleanup of this.cleanupFns.values()) {
994
- try {
995
- await cleanup();
996
- }
997
- catch (error) {
998
- console.error('Error during cleanup:', error);
999
- }
1000
- }
1001
- this.cleanupFns.clear();
703
+ /**
704
+ * Template system for Snice v3.0.0
705
+ * Provides html`` and css`` tagged template processors with differential rendering
706
+ */
707
+ // Unique symbols for type checking
708
+ const HTML_RESULT = Symbol('html-result');
709
+ const CSS_RESULT = Symbol('css-result');
710
+ /**
711
+ * Tagged template function for creating HTML templates
712
+ *
713
+ * @example
714
+ * ```typescript
715
+ * html`<div class="card">
716
+ * <h1>${this.title}</h1>
717
+ * <button @click=${this.handleClick}>Click me</button>
718
+ * </div>`
719
+ * ```
720
+ */
721
+ function html(strings, ...values) {
722
+ return {
723
+ _$litType$: HTML_RESULT,
724
+ strings,
725
+ values
726
+ };
727
+ }
728
+ /**
729
+ * Tagged template function for creating CSS
730
+ *
731
+ * @example
732
+ * ```typescript
733
+ * css`:host {
734
+ * display: block;
735
+ * padding: 1rem;
736
+ * }`
737
+ * ```
738
+ */
739
+ function css(strings, ...values) {
740
+ // Combine strings and values into final CSS text
741
+ let cssText = strings[0];
742
+ for (let i = 0; i < values.length; i++) {
743
+ cssText += String(values[i]) + strings[i + 1];
1002
744
  }
1003
- async runOperation(operation) {
1004
- const promise = operation();
1005
- const voidPromise = promise.then(() => { }, () => { });
1006
- this.pendingOperations.add(voidPromise);
745
+ const result = {
746
+ _$litType$: CSS_RESULT,
747
+ cssText
748
+ };
749
+ // Try to create constructable stylesheet for better performance
750
+ // This will be cached and reused across instances
751
+ if (typeof CSSStyleSheet !== 'undefined' && 'adoptedStyleSheets' in Document.prototype) {
1007
752
  try {
1008
- const result = await promise;
1009
- this.pendingOperations.delete(voidPromise);
1010
- return result;
753
+ const sheet = new CSSStyleSheet();
754
+ sheet.replaceSync(cssText);
755
+ result.styleSheet = sheet;
1011
756
  }
1012
- catch (error) {
1013
- this.pendingOperations.delete(voidPromise);
1014
- throw error;
757
+ catch (e) {
758
+ // Fall back to regular <style> tag if constructable stylesheets fail
1015
759
  }
1016
760
  }
761
+ return result;
1017
762
  }
1018
763
  /**
1019
- * Decorator to register a controller class with a name
1020
- * @param name The name to register the controller under
764
+ * Check if a value is a TemplateResult
1021
765
  */
1022
- function controller(name) {
1023
- return function (constructor, _context) {
1024
- snice.controllerRegistry.set(name, constructor);
1025
- // Mark as controller class for channel decorator detection
1026
- constructor.prototype[IS_CONTROLLER_CLASS] = true;
1027
- return constructor;
1028
- };
766
+ function isTemplateResult(value) {
767
+ return value && value._$litType$ === HTML_RESULT;
1029
768
  }
1030
769
  /**
1031
- * Attaches a controller to an element
1032
- * @param element The element to attach the controller to
1033
- * @param controllerName The name of the controller to attach
770
+ * Check if a value is a CSSResult
1034
771
  */
1035
- async function attachController(element, controllerName) {
1036
- const existingController = element[CONTROLLER_KEY];
1037
- const existingName = element[CONTROLLER_NAME_KEY];
1038
- // For native elements, check if this is actually the desired controller
1039
- const nativeController = element[NATIVE_CONTROLLER];
1040
- if (nativeController !== undefined && nativeController !== controllerName) {
1041
- // This attachment is outdated, skip it
772
+ function isCSSResult(value) {
773
+ return value && value._$litType$ === CSS_RESULT;
774
+ }
775
+ /**
776
+ * Nothing - represents no value (different from null/undefined)
777
+ * Used to remove content from templates
778
+ */
779
+ const nothing = Symbol('nothing');
780
+ // Unique symbol for unsafe HTML
781
+ const UNSAFE_HTML = Symbol('unsafe-html');
782
+ /**
783
+ * Mark a string as raw HTML that should not be escaped
784
+ * WARNING: Only use with sanitized content - using user input can lead to XSS!
785
+ *
786
+ * @example
787
+ * ```typescript
788
+ * const htmlString = '<span class="bold">Hello</span>';
789
+ * html`<div>${unsafeHTML(htmlString)}</div>`
790
+ * ```
791
+ */
792
+ function unsafeHTML(html) {
793
+ return {
794
+ _$litType$: UNSAFE_HTML,
795
+ html
796
+ };
797
+ }
798
+ /**
799
+ * Check if a value is an UnsafeHTML wrapper
800
+ */
801
+ function isUnsafeHTML(value) {
802
+ return value && value._$litType$ === UNSAFE_HTML;
803
+ }
804
+
805
+ // Unique marker for dynamic parts
806
+ // This parses as a comment node but doesn't get escaped in attributes
807
+ const marker = `snice$${Math.random().toFixed(9).slice(2)}$`;
808
+ const markerMatch = '?' + marker;
809
+ const nodeMarker = `<${markerMatch}>`;
810
+ const markerRegex = new RegExp(marker, 'g');
811
+ // Template cache - templates with same string array can be reused
812
+ const templateCache = new WeakMap();
813
+ /**
814
+ * A prepared template ready for rendering
815
+ */
816
+ class Template {
817
+ constructor(result, element, attrNamesForParts) {
818
+ this.parts = [];
819
+ this.element = element;
820
+ const walker = document.createTreeWalker(element.content, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
821
+ let partIndex = 0;
822
+ const nodesToRemove = [];
823
+ let node;
824
+ while ((node = walker.nextNode()) !== null) {
825
+ if (node.nodeType === Node.ELEMENT_NODE) {
826
+ const element = node;
827
+ const tagName = element.tagName.toLowerCase();
828
+ // Handle virtual elements: <if>, <case>
829
+ // Keep them in the DOM with display:contents for now
830
+ // Will optimize later with proper template extraction
831
+ if (tagName === 'if') {
832
+ // <if value="${condition}">children</if>
833
+ const valueAttr = element.getAttribute('value');
834
+ if (valueAttr && valueAttr.includes(marker)) {
835
+ // Remove the value attribute
836
+ element.removeAttribute('value');
837
+ this.parts.push({
838
+ type: 'conditional-if',
839
+ index: partIndex++,
840
+ element // Keep the <if> element
841
+ });
842
+ // Continue processing children normally
843
+ }
844
+ continue;
845
+ }
846
+ // Handle <case> element
847
+ if (tagName === 'case') {
848
+ // <case value="${value}">children</case>
849
+ const valueAttr = element.getAttribute('value');
850
+ if (valueAttr && valueAttr.includes(marker)) {
851
+ // Remove the value attribute
852
+ element.removeAttribute('value');
853
+ this.parts.push({
854
+ type: 'conditional-case',
855
+ index: partIndex++,
856
+ element // Keep the <case> element
857
+ });
858
+ // Continue processing children normally
859
+ }
860
+ continue;
861
+ }
862
+ if (element.hasAttributes()) {
863
+ const attributes = element.attributes;
864
+ const attrsToRemove = [];
865
+ for (let i = 0; i < attributes.length; i++) {
866
+ const attr = attributes[i];
867
+ const value = attr.value;
868
+ // Check for attribute bindings
869
+ if (value.includes(marker)) {
870
+ attrsToRemove.push(attr);
871
+ // Get original attribute name with preserved case
872
+ const originalName = attrNamesForParts[partIndex] || attr.name;
873
+ // Extract static string segments by splitting on marker
874
+ const attrStrings = value.split(marker);
875
+ if (originalName.startsWith('@')) {
876
+ // Event binding
877
+ this.parts.push({
878
+ type: 'event',
879
+ index: partIndex++,
880
+ name: originalName.slice(1),
881
+ element
882
+ });
883
+ }
884
+ else if (originalName.startsWith('.')) {
885
+ // Property binding - preserve original case for JavaScript properties
886
+ this.parts.push({
887
+ type: 'property',
888
+ index: partIndex++,
889
+ name: originalName.slice(1),
890
+ element
891
+ });
892
+ }
893
+ else if (originalName.startsWith('?')) {
894
+ // Boolean attribute
895
+ this.parts.push({
896
+ type: 'boolean-attribute',
897
+ index: partIndex++,
898
+ name: originalName.slice(1),
899
+ element
900
+ });
901
+ }
902
+ else {
903
+ // Regular attribute - use lowercased name from DOM
904
+ // Store static string segments for interpolation
905
+ this.parts.push({
906
+ type: 'attribute',
907
+ index: partIndex++,
908
+ name: attr.name,
909
+ element,
910
+ attrStrings
911
+ });
912
+ }
913
+ }
914
+ }
915
+ // Remove marker attributes
916
+ for (const attr of attrsToRemove) {
917
+ element.removeAttribute(attr.name);
918
+ }
919
+ }
920
+ }
921
+ else if (node.nodeType === Node.COMMENT_NODE) {
922
+ const comment = node;
923
+ // Check for marker match (processing instruction becomes comment)
924
+ if (comment.data === markerMatch) {
925
+ // Node part
926
+ const parent = comment.parentNode;
927
+ const endNode = document.createComment('');
928
+ parent.insertBefore(endNode, comment.nextSibling);
929
+ this.parts.push({
930
+ type: 'node',
931
+ index: partIndex++,
932
+ startNode: comment,
933
+ endNode
934
+ });
935
+ }
936
+ }
937
+ else if (node.nodeType === Node.TEXT_NODE) {
938
+ const text = node;
939
+ const data = text.data;
940
+ if (data.includes(marker)) {
941
+ // Split text node at markers
942
+ const parent = text.parentNode;
943
+ const parts = data.split(markerRegex);
944
+ const lastIndex = parts.length - 1;
945
+ for (let i = 0; i < lastIndex; i++) {
946
+ parent.insertBefore(document.createTextNode(parts[i]), text);
947
+ const comment = document.createComment('');
948
+ const endNode = document.createComment('');
949
+ parent.insertBefore(comment, text);
950
+ parent.insertBefore(endNode, text);
951
+ this.parts.push({
952
+ type: 'node',
953
+ index: partIndex++,
954
+ startNode: comment,
955
+ endNode
956
+ });
957
+ }
958
+ // Last part
959
+ if (parts[lastIndex] !== '') {
960
+ text.data = parts[lastIndex];
961
+ }
962
+ else {
963
+ nodesToRemove.push(text);
964
+ }
965
+ }
966
+ }
967
+ }
968
+ // Remove marker nodes
969
+ for (const node of nodesToRemove) {
970
+ node.parentNode?.removeChild(node);
971
+ }
972
+ }
973
+ }
974
+ /**
975
+ * Prepare a template for rendering
976
+ */
977
+ function prepareTemplate(result) {
978
+ // Check cache first
979
+ const { strings } = result;
980
+ const cached = templateCache.get(strings);
981
+ if (cached) {
982
+ return cached;
983
+ }
984
+ // Build HTML with markers and extract original attribute names
985
+ const htmlParts = [];
986
+ const attrNamesForParts = [];
987
+ for (let i = 0; i < strings.length; i++) {
988
+ const str = strings[i];
989
+ htmlParts.push(str);
990
+ if (i < strings.length - 1) {
991
+ // Check if we're in an attribute context
992
+ // Look backwards for = sign
993
+ const lastEquals = str.lastIndexOf('=');
994
+ const lastCloseTag = str.lastIndexOf('>');
995
+ if (lastEquals > lastCloseTag) {
996
+ // We're in an attribute value - extract and preserve the original attribute name
997
+ let attrStart = lastEquals - 1;
998
+ while (attrStart >= 0 && /\S/.test(str[attrStart])) {
999
+ attrStart--;
1000
+ }
1001
+ const attrName = str.substring(attrStart + 1, lastEquals).trim();
1002
+ attrNamesForParts.push(attrName);
1003
+ htmlParts.push(marker);
1004
+ }
1005
+ else {
1006
+ // Check if this is a meta element (<if> or <case>) by looking backwards
1007
+ // Match pattern: <if or <case followed by whitespace or >
1008
+ const metaElementMatch = str.match(/<(if|case)\s*$/);
1009
+ if (metaElementMatch) {
1010
+ // This is a meta element - add value attribute
1011
+ attrNamesForParts.push('value');
1012
+ htmlParts.push(`value="${marker}"`);
1013
+ }
1014
+ else {
1015
+ // We're in node content
1016
+ attrNamesForParts.push(''); // Empty string for node parts
1017
+ htmlParts.push(nodeMarker);
1018
+ }
1019
+ }
1020
+ }
1021
+ }
1022
+ const html = htmlParts.join('');
1023
+ const template = document.createElement('template');
1024
+ template.innerHTML = html;
1025
+ const tmpl = new Template(result, template, attrNamesForParts);
1026
+ // Cache the template for reuse
1027
+ templateCache.set(strings, tmpl);
1028
+ return tmpl;
1029
+ }
1030
+ /**
1031
+ * Instance of a rendered template
1032
+ */
1033
+ class TemplateInstance {
1034
+ constructor(result) {
1035
+ this.parts = [];
1036
+ this.fragment = null;
1037
+ this.conditionalParts = []; // if/case parts with their indices
1038
+ this.regularParts = []; // all other parts with their indices
1039
+ this.template = prepareTemplate(result);
1040
+ }
1041
+ renderFragment() {
1042
+ if (!this.fragment) {
1043
+ // First render - clone template and create parts
1044
+ this.fragment = this.template.element.content.cloneNode(true);
1045
+ // Build a map of nodes from template to cloned fragment
1046
+ const walker = document.createTreeWalker(this.template.element.content, NodeFilter.SHOW_ALL);
1047
+ const clonedWalker = document.createTreeWalker(this.fragment, NodeFilter.SHOW_ALL);
1048
+ const nodeMap = new Map();
1049
+ let templateNode = walker.currentNode;
1050
+ let clonedNode = clonedWalker.currentNode;
1051
+ while (templateNode && clonedNode) {
1052
+ nodeMap.set(templateNode, clonedNode);
1053
+ templateNode = walker.nextNode();
1054
+ clonedNode = clonedWalker.nextNode();
1055
+ }
1056
+ for (let i = 0; i < this.template.parts.length; i++) {
1057
+ const partDef = this.template.parts[i];
1058
+ let part;
1059
+ switch (partDef.type) {
1060
+ case 'node':
1061
+ const startNode = nodeMap.get(partDef.startNode);
1062
+ const endNode = nodeMap.get(partDef.endNode);
1063
+ part = new NodePart(startNode, endNode);
1064
+ break;
1065
+ case 'attribute':
1066
+ const attrElement = nodeMap.get(partDef.element);
1067
+ part = new AttributePart(attrElement, partDef.name, partDef.attrStrings);
1068
+ break;
1069
+ case 'property':
1070
+ const propElement = nodeMap.get(partDef.element);
1071
+ part = new PropertyPart(propElement, partDef.name);
1072
+ break;
1073
+ case 'boolean-attribute':
1074
+ const boolElement = nodeMap.get(partDef.element);
1075
+ part = new BooleanAttributePart(boolElement, partDef.name);
1076
+ break;
1077
+ case 'event':
1078
+ const eventElement = nodeMap.get(partDef.element);
1079
+ part = new EventPart(eventElement, partDef.name);
1080
+ break;
1081
+ case 'conditional-if':
1082
+ const conditionalIfElement = nodeMap.get(partDef.element);
1083
+ part = new ConditionalIfPart(conditionalIfElement);
1084
+ break;
1085
+ case 'conditional-case':
1086
+ const conditionalCaseElement = nodeMap.get(partDef.element);
1087
+ part = new ConditionalCasePart(conditionalCaseElement);
1088
+ break;
1089
+ default:
1090
+ throw new Error(`Unknown part type: ${partDef.type}`);
1091
+ }
1092
+ this.parts.push(part);
1093
+ // Separate conditional parts from regular parts for optimized update
1094
+ if (part instanceof ConditionalIfPart || part instanceof ConditionalCasePart) {
1095
+ this.conditionalParts.push({ part, index: i });
1096
+ }
1097
+ else {
1098
+ this.regularParts.push({ part, index: i });
1099
+ }
1100
+ }
1101
+ }
1102
+ return this.fragment;
1103
+ }
1104
+ render(values) {
1105
+ const fragment = this.renderFragment();
1106
+ // Commit values to parts
1107
+ this.update(values);
1108
+ return fragment;
1109
+ }
1110
+ update(values) {
1111
+ // Optimized: Process conditional parts first (if any), then regular parts
1112
+ // Using pre-separated arrays with cached indices avoids instanceof and indexOf calls
1113
+ // Process conditional parts first (they control visibility)
1114
+ for (const { part, index } of this.conditionalParts) {
1115
+ part.commit(values[index]);
1116
+ }
1117
+ // Then process regular parts
1118
+ for (const { part, index } of this.regularParts) {
1119
+ part.commit(values[index]);
1120
+ }
1121
+ }
1122
+ clear() {
1123
+ for (const part of this.parts) {
1124
+ part.clear();
1125
+ }
1126
+ }
1127
+ }
1128
+ /**
1129
+ * Base class for all parts
1130
+ */
1131
+ class Part {
1132
+ }
1133
+ /**
1134
+ * NodePart handles text content and nested templates
1135
+ */
1136
+ class NodePart extends Part {
1137
+ constructor(startNode, endNode) {
1138
+ super();
1139
+ this.value = undefined;
1140
+ this.startNode = startNode;
1141
+ this.endNode = endNode;
1142
+ }
1143
+ commit(value) {
1144
+ if (value === this.value)
1145
+ return;
1146
+ this.value = value;
1147
+ // Handle arrays
1148
+ if (Array.isArray(value)) {
1149
+ this.commitArray(value);
1150
+ return;
1151
+ }
1152
+ // Handle nested templates
1153
+ if (isTemplateResult(value)) {
1154
+ this.commitTemplate(value);
1155
+ return;
1156
+ }
1157
+ // Handle unsafe HTML
1158
+ if (isUnsafeHTML(value)) {
1159
+ this.commitUnsafeHTML(value);
1160
+ return;
1161
+ }
1162
+ // Handle primitives
1163
+ this.commitPrimitive(value);
1164
+ }
1165
+ commitPrimitive(value) {
1166
+ this.clear();
1167
+ const text = value === null || value === undefined ? '' : String(value);
1168
+ const textNode = document.createTextNode(text);
1169
+ this.insertBefore(textNode);
1170
+ }
1171
+ commitTemplate(template) {
1172
+ this.clear();
1173
+ const instance = new TemplateInstance(template);
1174
+ const fragment = instance.render(template.values);
1175
+ this.insertBefore(fragment);
1176
+ }
1177
+ commitArray(values) {
1178
+ this.clear();
1179
+ // Template caching (via prepareTemplate) still provides significant performance benefit
1180
+ for (const value of values) {
1181
+ if (isTemplateResult(value)) {
1182
+ const instance = new TemplateInstance(value);
1183
+ const fragment = instance.render(value.values);
1184
+ this.insertBefore(fragment);
1185
+ }
1186
+ else {
1187
+ const text = value === null || value === undefined ? '' : String(value);
1188
+ const textNode = document.createTextNode(text);
1189
+ this.insertBefore(textNode);
1190
+ }
1191
+ }
1192
+ }
1193
+ commitUnsafeHTML(value) {
1194
+ this.clear();
1195
+ // Create a temporary container to parse the HTML
1196
+ const temp = document.createElement('template');
1197
+ temp.innerHTML = value.html;
1198
+ // Insert all parsed nodes
1199
+ const fragment = temp.content;
1200
+ this.insertBefore(fragment);
1201
+ }
1202
+ insertBefore(node) {
1203
+ this.endNode.parentNode?.insertBefore(node, this.endNode);
1204
+ }
1205
+ clear() {
1206
+ const parent = this.startNode.parentNode;
1207
+ if (!parent)
1208
+ return;
1209
+ let node = this.startNode.nextSibling;
1210
+ while (node && node !== this.endNode) {
1211
+ const next = node.nextSibling;
1212
+ parent.removeChild(node);
1213
+ node = next;
1214
+ }
1215
+ }
1216
+ }
1217
+ /**
1218
+ * AttributePart handles regular attribute updates
1219
+ */
1220
+ class AttributePart extends Part {
1221
+ constructor(element, name, attrStrings) {
1222
+ super();
1223
+ this.value = undefined;
1224
+ this.element = element;
1225
+ this.name = name;
1226
+ this.attrStrings = attrStrings;
1227
+ }
1228
+ commit(value) {
1229
+ if (value === this.value)
1230
+ return;
1231
+ this.value = value;
1232
+ if (value === null || value === undefined) {
1233
+ this.element.removeAttribute(this.name);
1234
+ }
1235
+ else {
1236
+ // Inline attribute value computation for performance
1237
+ let finalValue;
1238
+ if (!this.attrStrings || this.attrStrings.length === 0) {
1239
+ finalValue = String(value);
1240
+ }
1241
+ else if (this.attrStrings.length === 1) {
1242
+ finalValue = this.attrStrings[0];
1243
+ }
1244
+ else {
1245
+ // Multiple segments: "prefix" + value + "suffix"
1246
+ finalValue = this.attrStrings[0] + String(value) + this.attrStrings[1];
1247
+ }
1248
+ this.element.setAttribute(this.name, finalValue);
1249
+ }
1250
+ }
1251
+ clear() {
1252
+ this.element.removeAttribute(this.name);
1253
+ }
1254
+ }
1255
+ /**
1256
+ * PropertyPart handles property bindings
1257
+ */
1258
+ class PropertyPart extends Part {
1259
+ constructor(element, name) {
1260
+ super();
1261
+ this.value = undefined;
1262
+ this.element = element;
1263
+ this.name = name;
1264
+ }
1265
+ commit(value) {
1266
+ if (value === this.value)
1267
+ return;
1268
+ this.value = value;
1269
+ this.element[this.name] = value;
1270
+ }
1271
+ clear() {
1272
+ this.element[this.name] = undefined;
1273
+ }
1274
+ }
1275
+ /**
1276
+ * BooleanAttributePart handles boolean attributes
1277
+ */
1278
+ class BooleanAttributePart extends Part {
1279
+ constructor(element, name) {
1280
+ super();
1281
+ this.value = undefined;
1282
+ this.element = element;
1283
+ this.name = name;
1284
+ }
1285
+ commit(value) {
1286
+ if (value === this.value)
1287
+ return;
1288
+ this.value = value;
1289
+ if (value) {
1290
+ this.element.setAttribute(this.name, '');
1291
+ }
1292
+ else {
1293
+ this.element.removeAttribute(this.name);
1294
+ }
1295
+ }
1296
+ clear() {
1297
+ this.element.removeAttribute(this.name);
1298
+ }
1299
+ }
1300
+ /**
1301
+ * EventPart handles event listener bindings with keyboard shortcut support
1302
+ */
1303
+ class EventPart extends Part {
1304
+ constructor(element, eventName) {
1305
+ super();
1306
+ this.listener = null;
1307
+ this.value = undefined;
1308
+ this.keyFilter = null;
1309
+ this.host = null; // Cache host element
1310
+ this.element = element;
1311
+ // Parse keyboard shortcuts:
1312
+ // Supports both dot notation (@keydown.enter) and colon notation (@keydown:Enter) to match @on decorator
1313
+ const dotIndex = eventName.indexOf('.');
1314
+ const colonIndex = eventName.indexOf(':');
1315
+ // Use whichever delimiter comes first (dot or colon)
1316
+ const delimiterIndex = dotIndex > 0 && colonIndex > 0
1317
+ ? Math.min(dotIndex, colonIndex)
1318
+ : Math.max(dotIndex, colonIndex);
1319
+ if (delimiterIndex > 0) {
1320
+ const baseEvent = eventName.substring(0, delimiterIndex);
1321
+ const keySpec = eventName.substring(delimiterIndex + 1);
1322
+ this.eventName = baseEvent;
1323
+ this.keyFilter = parseKeyboardFilter(keySpec);
1324
+ }
1325
+ else {
1326
+ this.eventName = eventName;
1327
+ }
1328
+ }
1329
+ commit(value) {
1330
+ // Skip if same value (but null/undefined always triggers update)
1331
+ if (value === this.value && value !== null && value !== undefined)
1332
+ return;
1333
+ // Remove old listener
1334
+ if (this.listener) {
1335
+ this.element.removeEventListener(this.eventName, this.listener);
1336
+ this.listener = null;
1337
+ }
1338
+ this.value = value;
1339
+ // Add new listener
1340
+ if (value === null || value === undefined) {
1341
+ return;
1342
+ }
1343
+ if (typeof value === 'function') {
1344
+ // Auto-bind to host element (the custom element with shadow root)
1345
+ // Cache host lookup for performance
1346
+ if (!this.host) {
1347
+ const rootNode = this.element.getRootNode();
1348
+ this.host = rootNode.host || null;
1349
+ }
1350
+ // Create a wrapper that calls the handler with the host as context
1351
+ if (this.host) {
1352
+ const host = this.host; // Capture for closure
1353
+ this.listener = ((event) => {
1354
+ if (this.keyFilter && !matchesKeyboardFilter(event, this.keyFilter)) {
1355
+ return;
1356
+ }
1357
+ value.call(host, event);
1358
+ });
1359
+ }
1360
+ else {
1361
+ this.listener = ((event) => {
1362
+ if (this.keyFilter && !matchesKeyboardFilter(event, this.keyFilter)) {
1363
+ return;
1364
+ }
1365
+ value.call(null, event);
1366
+ });
1367
+ }
1368
+ this.element.addEventListener(this.eventName, this.listener);
1369
+ }
1370
+ }
1371
+ clear() {
1372
+ if (this.listener) {
1373
+ this.element.removeEventListener(this.eventName, this.listener);
1374
+ this.listener = null;
1375
+ }
1376
+ }
1377
+ }
1378
+ /**
1379
+ * Parse keyboard shortcut specification
1380
+ * Examples:
1381
+ * "enter" -> { key: "Enter" }
1382
+ * "ctrl+s" -> { key: "s", ctrl: true }
1383
+ * "ctrl+shift+s" -> { key: "s", ctrl: true, shift: true }
1384
+ * "~enter" -> { key: "Enter", anyModifiers: true }
1385
+ */
1386
+ function parseKeyboardFilter(spec) {
1387
+ // Handle ~ prefix for matching regardless of modifiers
1388
+ const anyModifiers = spec.startsWith('~');
1389
+ if (anyModifiers) {
1390
+ spec = spec.substring(1);
1391
+ }
1392
+ const parts = spec.split('+');
1393
+ const filter = {
1394
+ key: '',
1395
+ anyModifiers
1396
+ };
1397
+ for (const part of parts) {
1398
+ const lower = part.toLowerCase();
1399
+ if (lower === 'ctrl' || lower === 'control') {
1400
+ filter.ctrl = true;
1401
+ }
1402
+ else if (lower === 'alt') {
1403
+ filter.alt = true;
1404
+ }
1405
+ else if (lower === 'shift') {
1406
+ filter.shift = true;
1407
+ }
1408
+ else if (lower === 'meta' || lower === 'cmd' || lower === 'command') {
1409
+ filter.meta = true;
1410
+ }
1411
+ else {
1412
+ // This is the key itself - normalize common keys
1413
+ filter.key = normalizeKey(part);
1414
+ }
1415
+ }
1416
+ return filter;
1417
+ }
1418
+ /**
1419
+ * Normalize key names to match KeyboardEvent.key
1420
+ */
1421
+ function normalizeKey(key) {
1422
+ const keyMap = {
1423
+ 'esc': 'Escape',
1424
+ 'escape': 'Escape',
1425
+ 'enter': 'Enter',
1426
+ 'return': 'Enter',
1427
+ 'space': ' ',
1428
+ 'spacebar': ' ',
1429
+ 'up': 'ArrowUp',
1430
+ 'down': 'ArrowDown',
1431
+ 'left': 'ArrowLeft',
1432
+ 'right': 'ArrowRight',
1433
+ 'arrowup': 'ArrowUp',
1434
+ 'arrowdown': 'ArrowDown',
1435
+ 'arrowleft': 'ArrowLeft',
1436
+ 'arrowright': 'ArrowRight',
1437
+ 'delete': 'Delete',
1438
+ 'del': 'Delete',
1439
+ 'backspace': 'Backspace',
1440
+ 'tab': 'Tab',
1441
+ 'home': 'Home',
1442
+ 'end': 'End',
1443
+ 'pageup': 'PageUp',
1444
+ 'pagedown': 'PageDown'
1445
+ };
1446
+ const lower = key.toLowerCase();
1447
+ return keyMap[lower] || key;
1448
+ }
1449
+ /**
1450
+ * Check if keyboard event matches the filter
1451
+ */
1452
+ function matchesKeyboardFilter(event, filter) {
1453
+ // Check key match
1454
+ if (event.key !== filter.key) {
1455
+ return false;
1456
+ }
1457
+ // If anyModifiers is true, we don't care about modifiers
1458
+ if (filter.anyModifiers) {
1459
+ return true;
1460
+ }
1461
+ // Check modifiers - by default, exact match
1462
+ // If filter specifies ctrl: true, event must have ctrlKey
1463
+ // If filter doesn't specify ctrl, event must NOT have ctrlKey
1464
+ const ctrlMatch = filter.ctrl ? event.ctrlKey : !event.ctrlKey;
1465
+ const altMatch = filter.alt ? event.altKey : !event.altKey;
1466
+ const shiftMatch = filter.shift ? event.shiftKey : !event.shiftKey;
1467
+ const metaMatch = filter.meta ? event.metaKey : !event.metaKey;
1468
+ return ctrlMatch && altMatch && shiftMatch && metaMatch;
1469
+ }
1470
+ /**
1471
+ * ConditionalIfPart handles <if> conditional rendering
1472
+ * Removes/inserts DOM nodes based on condition
1473
+ */
1474
+ class ConditionalIfPart extends Part {
1475
+ constructor(ifElement) {
1476
+ super();
1477
+ this.value = undefined;
1478
+ this.fragment = null;
1479
+ this.ifElement = ifElement;
1480
+ this.ifElement.style.display = 'contents';
1481
+ }
1482
+ commit(value) {
1483
+ const condition = Boolean(value);
1484
+ if (this.value === value)
1485
+ return;
1486
+ this.value = value;
1487
+ if (condition) {
1488
+ // Show: restore children from fragment
1489
+ if (this.fragment && this.fragment.hasChildNodes()) {
1490
+ this.ifElement.appendChild(this.fragment);
1491
+ }
1492
+ }
1493
+ else {
1494
+ // Hide: move children to fragment
1495
+ if (!this.fragment) {
1496
+ this.fragment = document.createDocumentFragment();
1497
+ }
1498
+ while (this.ifElement.firstChild) {
1499
+ this.fragment.appendChild(this.ifElement.firstChild);
1500
+ }
1501
+ }
1502
+ }
1503
+ clear() {
1504
+ if (!this.fragment) {
1505
+ this.fragment = document.createDocumentFragment();
1506
+ }
1507
+ while (this.ifElement.firstChild) {
1508
+ this.fragment.appendChild(this.ifElement.firstChild);
1509
+ }
1510
+ }
1511
+ }
1512
+ /**
1513
+ * ConditionalCasePart handles <case>/<when>/<default> conditional rendering
1514
+ * Removes/inserts matching branch based on value
1515
+ */
1516
+ class ConditionalCasePart extends Part {
1517
+ constructor(caseElement) {
1518
+ super();
1519
+ this.value = undefined;
1520
+ this.childrenMap = new Map();
1521
+ this.fragments = new Map();
1522
+ this.defaultChild = null;
1523
+ this.currentChild = null;
1524
+ this.caseElement = caseElement;
1525
+ // Build map and store children in fragments initially
1526
+ for (const child of Array.from(this.caseElement.children)) {
1527
+ const childTag = child.tagName.toLowerCase();
1528
+ if (childTag === 'when') {
1529
+ const whenValue = child.getAttribute('value') || '';
1530
+ this.childrenMap.set(whenValue, child);
1531
+ const fragment = document.createDocumentFragment();
1532
+ fragment.appendChild(child);
1533
+ this.fragments.set(child, fragment);
1534
+ }
1535
+ else if (childTag === 'default') {
1536
+ this.defaultChild = child;
1537
+ const fragment = document.createDocumentFragment();
1538
+ fragment.appendChild(child);
1539
+ this.fragments.set(child, fragment);
1540
+ }
1541
+ }
1542
+ }
1543
+ commit(value) {
1544
+ if (this.value === value)
1545
+ return;
1546
+ this.value = value;
1547
+ const valueStr = String(value);
1548
+ // Remove current child
1549
+ if (this.currentChild) {
1550
+ const fragment = this.fragments.get(this.currentChild);
1551
+ if (fragment && !fragment.hasChildNodes()) {
1552
+ fragment.appendChild(this.currentChild);
1553
+ }
1554
+ }
1555
+ // Insert matching child
1556
+ const matchingChild = this.childrenMap.get(valueStr) || this.defaultChild;
1557
+ if (matchingChild) {
1558
+ const fragment = this.fragments.get(matchingChild);
1559
+ if (fragment && fragment.hasChildNodes()) {
1560
+ this.caseElement.appendChild(fragment);
1561
+ }
1562
+ this.currentChild = matchingChild;
1563
+ }
1564
+ }
1565
+ clear() {
1566
+ if (this.currentChild) {
1567
+ const fragment = this.fragments.get(this.currentChild);
1568
+ if (fragment && !fragment.hasChildNodes()) {
1569
+ fragment.appendChild(this.currentChild);
1570
+ }
1571
+ this.currentChild = null;
1572
+ }
1573
+ }
1574
+ }
1575
+
1576
+ /**
1577
+ * @on decorator for listening to events
1578
+ * Use in elements or controllers to listen to DOM events or custom events
1579
+ */
1580
+ const ON_HANDLERS = getSymbol('on-handlers');
1581
+ /**
1582
+ * @on decorator for listening to events
1583
+ *
1584
+ * Works in both elements and controllers with full event delegation support.
1585
+ *
1586
+ * @param eventName - Event name(s) to listen for
1587
+ * @param selector - Optional CSS selector for event delegation
1588
+ * @param options - Event listener options including debounce/throttle
1589
+ *
1590
+ * @example
1591
+ * ```typescript
1592
+ * // In elements
1593
+ * @element('my-button')
1594
+ * class MyButton extends HTMLElement {
1595
+ * @on('click', 'button')
1596
+ * handleClick(e: MouseEvent) {
1597
+ * console.log('Button clicked!', e);
1598
+ * }
1599
+ *
1600
+ * @on('input', 'input', { debounce: 300 })
1601
+ * handleInput(e: Event) {
1602
+ * console.log('Input changed:', (e.target as HTMLInputElement).value);
1603
+ * }
1604
+ * }
1605
+ *
1606
+ * // In controllers
1607
+ * @controller('my-controller')
1608
+ * class MyController {
1609
+ * element!: HTMLElement;
1610
+ *
1611
+ * @on('count-changed')
1612
+ * handleCountChanged(e: CustomEvent) {
1613
+ * console.log('Count changed to:', e.detail.count);
1614
+ * }
1615
+ *
1616
+ * @on('click', '.item', { throttle: 100 })
1617
+ * handleItemClick(e: MouseEvent) {
1618
+ * console.log('Item clicked');
1619
+ * }
1620
+ * }
1621
+ * ```
1622
+ */
1623
+ function on(eventName, selectorOrOptions, options) {
1624
+ // Parse arguments to support multiple call signatures
1625
+ let selector = null;
1626
+ let opts = {};
1627
+ if (typeof selectorOrOptions === 'string') {
1628
+ // With selector: (eventName, selector, options)
1629
+ selector = selectorOrOptions;
1630
+ opts = options || {};
1631
+ }
1632
+ else if (selectorOrOptions === null && options) {
1633
+ // With null selector: (eventName, null, options)
1634
+ opts = options;
1635
+ }
1636
+ else if (selectorOrOptions && typeof selectorOrOptions === 'object') {
1637
+ // Without selector: (eventName, options)
1638
+ opts = selectorOrOptions;
1639
+ }
1640
+ return function (originalMethod, context) {
1641
+ const methodName = context.name;
1642
+ const initKey = `__on_init_${methodName}_${selector || ''}_${JSON.stringify(eventName)}`;
1643
+ context.addInitializer(function () {
1644
+ const constructor = this.constructor;
1645
+ // Only initialize once per class, not per instance
1646
+ if (constructor[initKey])
1647
+ return;
1648
+ constructor[initKey] = true;
1649
+ if (!constructor[ON_HANDLERS]) {
1650
+ constructor[ON_HANDLERS] = [];
1651
+ }
1652
+ const eventNames = Array.isArray(eventName) ? eventName : [eventName];
1653
+ for (const event of eventNames) {
1654
+ constructor[ON_HANDLERS].push({
1655
+ eventName: event,
1656
+ selector,
1657
+ methodName,
1658
+ method: originalMethod,
1659
+ options: opts
1660
+ });
1661
+ }
1662
+ });
1663
+ return originalMethod;
1664
+ };
1665
+ }
1666
+ /**
1667
+ * Create a debounced version of a function
1668
+ */
1669
+ function debounce$1(fn, delay) {
1670
+ let timeoutId = null;
1671
+ return function (...args) {
1672
+ if (timeoutId !== null) {
1673
+ clearTimeout(timeoutId);
1674
+ }
1675
+ timeoutId = setTimeout(() => {
1676
+ fn.apply(this, args);
1677
+ timeoutId = null;
1678
+ }, delay);
1679
+ };
1680
+ }
1681
+ /**
1682
+ * Create a throttled version of a function (leading edge only)
1683
+ */
1684
+ function throttle$1(fn, delay) {
1685
+ let lastCall = 0;
1686
+ return function (...args) {
1687
+ const now = Date.now();
1688
+ const timeSinceLastCall = now - lastCall;
1689
+ if (timeSinceLastCall >= delay) {
1690
+ lastCall = now;
1691
+ fn.apply(this, args);
1692
+ }
1693
+ // Events within the throttle window are simply ignored (leading edge only)
1694
+ };
1695
+ }
1696
+ /**
1697
+ * Events that don't bubble - these require capture phase for delegation
1698
+ */
1699
+ const NON_BUBBLING_EVENTS = new Set([
1700
+ 'scroll',
1701
+ 'focus',
1702
+ 'blur',
1703
+ 'load',
1704
+ 'unload',
1705
+ 'error',
1706
+ 'resize',
1707
+ 'abort',
1708
+ 'mouseenter',
1709
+ 'mouseleave',
1710
+ 'pointerenter',
1711
+ 'pointerleave',
1712
+ ]);
1713
+ /**
1714
+ * Setup event listeners for an element or controller instance
1715
+ * Called automatically during element connection or controller attachment
1716
+ */
1717
+ function setupEventHandlers(instance, targetElement) {
1718
+ const handlers = instance.constructor[ON_HANDLERS];
1719
+ if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
1720
+ return;
1721
+ }
1722
+ // Initialize cleanup object if needed
1723
+ if (!instance[CLEANUP]) {
1724
+ instance[CLEANUP] = { events: [], channels: [], observers: [] };
1725
+ }
1726
+ else if (!instance[CLEANUP].events) {
1727
+ instance[CLEANUP].events = [];
1728
+ }
1729
+ else if (instance[CLEANUP].events.length > 0) {
1730
+ // Events already set up - clean them up first to avoid duplicates
1731
+ cleanupEventHandlers(instance);
1732
+ }
1733
+ for (const handler of handlers) {
1734
+ // Get current method from instance (preserves decorator stacking)
1735
+ const currentMethod = instance[handler.methodName];
1736
+ let boundMethod = currentMethod ? currentMethod.bind(instance) : handler.method.bind(instance);
1737
+ const handlerOptions = handler.options || {};
1738
+ // Parse event name for key modifiers
1739
+ // Supports both dot notation (@keydown.enter) and colon notation (@keydown:Enter)
1740
+ const dotIndex = handler.eventName.indexOf('.');
1741
+ const colonIndex = handler.eventName.indexOf(':');
1742
+ const delimiterIndex = dotIndex > 0 && colonIndex > 0
1743
+ ? Math.min(dotIndex, colonIndex)
1744
+ : Math.max(dotIndex, colonIndex);
1745
+ const baseEventName = delimiterIndex > 0
1746
+ ? handler.eventName.substring(0, delimiterIndex)
1747
+ : handler.eventName;
1748
+ const keyModifier = delimiterIndex > 0
1749
+ ? handler.eventName.substring(delimiterIndex + 1)
1750
+ : null;
1751
+ // Apply debounce if specified
1752
+ if (handlerOptions.debounce && handlerOptions.debounce > 0) {
1753
+ boundMethod = debounce$1(boundMethod, handlerOptions.debounce);
1754
+ }
1755
+ // Apply throttle if specified (debounce takes precedence)
1756
+ else if (handlerOptions.throttle && handlerOptions.throttle > 0) {
1757
+ boundMethod = throttle$1(boundMethod, handlerOptions.throttle);
1758
+ }
1759
+ // Create event handler with key modifier support
1760
+ // Uses shared keyboard filter implementation from parts.ts
1761
+ let keyFilter = null;
1762
+ if (keyModifier && ['keydown', 'keyup', 'keypress'].includes(baseEventName)) {
1763
+ keyFilter = parseKeyboardFilter(keyModifier);
1764
+ }
1765
+ const createKeyModifierHandler = (method) => {
1766
+ if (!keyFilter) {
1767
+ return method;
1768
+ }
1769
+ return (event) => {
1770
+ const keyEvent = event;
1771
+ if (matchesKeyboardFilter(keyEvent, keyFilter)) {
1772
+ method(event);
1773
+ }
1774
+ };
1775
+ };
1776
+ // Apply key modifier wrapper
1777
+ const keyModifierMethod = createKeyModifierHandler(boundMethod);
1778
+ // Main event handler with error handling and event delegation
1779
+ if (handler.selector) {
1780
+ // Delegated event handling - use shadow root if available
1781
+ const eventRoot = targetElement.shadowRoot || targetElement;
1782
+ const delegatedHandler = (event) => {
1783
+ const target = event.target;
1784
+ let matchingElement = null;
1785
+ // Check if target itself matches the selector
1786
+ if (target.matches && target.matches(handler.selector)) {
1787
+ matchingElement = target;
1788
+ }
1789
+ // Check if any parent matches the selector (event delegation)
1790
+ else if (target.closest) {
1791
+ matchingElement = target.closest(handler.selector);
1792
+ }
1793
+ // Only handle if we found a match
1794
+ // Note: No need to check contains() since the event bubbled to eventRoot
1795
+ if (matchingElement) {
1796
+ // Apply automatic preventDefault/stopPropagation
1797
+ if (handlerOptions.preventDefault) {
1798
+ event.preventDefault();
1799
+ }
1800
+ if (handlerOptions.stopPropagation) {
1801
+ event.stopPropagation();
1802
+ event.stopImmediatePropagation();
1803
+ }
1804
+ try {
1805
+ keyModifierMethod(event);
1806
+ }
1807
+ catch (error) {
1808
+ console.error(`Error in event handler ${handler.methodName}:`, error);
1809
+ }
1810
+ }
1811
+ };
1812
+ // Auto-enable capture for non-bubbling events when using delegation
1813
+ const needsCapture = NON_BUBBLING_EVENTS.has(baseEventName);
1814
+ const useCapture = handlerOptions.capture !== undefined
1815
+ ? handlerOptions.capture
1816
+ : needsCapture;
1817
+ const listenerOptions = {
1818
+ capture: useCapture,
1819
+ once: handlerOptions.once || false,
1820
+ passive: handlerOptions.passive || false,
1821
+ };
1822
+ eventRoot.addEventListener(baseEventName, delegatedHandler, listenerOptions);
1823
+ instance[CLEANUP].events.push({
1824
+ target: eventRoot,
1825
+ eventName: baseEventName,
1826
+ handler: delegatedHandler,
1827
+ options: listenerOptions,
1828
+ });
1829
+ }
1830
+ else {
1831
+ // Direct event handling - on the element itself
1832
+ // If element has shadow root, listen on both shadow root AND host element
1833
+ // to catch events from inside shadow DOM (with correct target) and on host itself
1834
+ const shadowRoot = targetElement.shadowRoot;
1835
+ const handledSymbol = Symbol('snice-event-handled');
1836
+ const wrappedMethod = (event) => {
1837
+ // Prevent double-triggering when listening on both shadow root and host
1838
+ if (event[handledSymbol]) {
1839
+ return;
1840
+ }
1841
+ event[handledSymbol] = true;
1842
+ try {
1843
+ // Apply automatic preventDefault/stopPropagation
1844
+ if (handlerOptions.preventDefault) {
1845
+ event.preventDefault();
1846
+ }
1847
+ if (handlerOptions.stopPropagation) {
1848
+ event.stopPropagation();
1849
+ }
1850
+ keyModifierMethod(event);
1851
+ }
1852
+ catch (error) {
1853
+ console.error(`Error in event handler ${handler.methodName}:`, error);
1854
+ }
1855
+ };
1856
+ const listenerOptions = {
1857
+ capture: handlerOptions.capture || false,
1858
+ once: handlerOptions.once || false,
1859
+ passive: handlerOptions.passive || false,
1860
+ };
1861
+ if (shadowRoot) {
1862
+ // Listen on shadow root for events inside shadow DOM
1863
+ shadowRoot.addEventListener(baseEventName, wrappedMethod, listenerOptions);
1864
+ instance[CLEANUP].events.push({
1865
+ target: shadowRoot,
1866
+ eventName: baseEventName,
1867
+ handler: wrappedMethod,
1868
+ options: listenerOptions,
1869
+ });
1870
+ }
1871
+ // Also listen on host element (for clicks on host itself or when no shadow root)
1872
+ targetElement.addEventListener(baseEventName, wrappedMethod, listenerOptions);
1873
+ instance[CLEANUP].events.push({
1874
+ target: targetElement,
1875
+ eventName: baseEventName,
1876
+ handler: wrappedMethod,
1877
+ options: listenerOptions,
1878
+ });
1879
+ }
1880
+ }
1881
+ }
1882
+ /**
1883
+ * Cleanup event listeners for a controller instance
1884
+ * Called automatically by the controller system during detach
1885
+ */
1886
+ function cleanupEventHandlers(instance) {
1887
+ if (!instance[CLEANUP]?.events)
1888
+ return;
1889
+ for (const { target, eventName, handler, options } of instance[CLEANUP].events) {
1890
+ target.removeEventListener(eventName, handler, options);
1891
+ }
1892
+ instance[CLEANUP].events = [];
1893
+ }
1894
+
1895
+ // Controller-scoped cleanup registry
1896
+ class ControllerScope {
1897
+ constructor() {
1898
+ this.cleanupFns = new Map();
1899
+ this.pendingOperations = new Set();
1900
+ }
1901
+ register(key, cleanup) {
1902
+ this.cleanupFns.set(key, cleanup);
1903
+ }
1904
+ unregister(key) {
1905
+ this.cleanupFns.delete(key);
1906
+ }
1907
+ async cleanup() {
1908
+ // Wait for all pending operations
1909
+ await Promise.all(this.pendingOperations);
1910
+ // Run all cleanup functions
1911
+ for (const cleanup of this.cleanupFns.values()) {
1912
+ try {
1913
+ await cleanup();
1914
+ }
1915
+ catch (error) {
1916
+ console.error('Error during cleanup:', error);
1917
+ }
1918
+ }
1919
+ this.cleanupFns.clear();
1920
+ }
1921
+ async runOperation(operation) {
1922
+ const promise = operation();
1923
+ const voidPromise = promise.then(() => { }, () => { });
1924
+ this.pendingOperations.add(voidPromise);
1925
+ try {
1926
+ const result = await promise;
1927
+ this.pendingOperations.delete(voidPromise);
1928
+ return result;
1929
+ }
1930
+ catch (error) {
1931
+ this.pendingOperations.delete(voidPromise);
1932
+ throw error;
1933
+ }
1934
+ }
1935
+ }
1936
+ /**
1937
+ * Decorator to register a controller class with a name
1938
+ * @param name The name to register the controller under
1939
+ */
1940
+ function controller(name) {
1941
+ return function (constructor, _context) {
1942
+ snice.controllerRegistry.set(name, constructor);
1943
+ // Mark as controller class for channel decorator detection
1944
+ constructor.prototype[IS_CONTROLLER_CLASS] = true;
1945
+ return constructor;
1946
+ };
1947
+ }
1948
+ /**
1949
+ * Attaches a controller to an element
1950
+ * @param element The element to attach the controller to
1951
+ * @param controllerName The name of the controller to attach
1952
+ */
1953
+ async function attachController(element, controllerName) {
1954
+ const existingController = element[CONTROLLER_KEY];
1955
+ const existingName = element[CONTROLLER_NAME_KEY];
1956
+ // For native elements, check if this is actually the desired controller
1957
+ const nativeController = element[NATIVE_CONTROLLER];
1958
+ if (nativeController !== undefined && nativeController !== controllerName) {
1959
+ // This attachment is outdated, skip it
1042
1960
  return;
1043
1961
  }
1044
1962
  if (existingName === controllerName && existingController) {
@@ -1077,12 +1995,12 @@ async function attachController(element, controllerName) {
1077
1995
  await scope.runOperation(async () => {
1078
1996
  await controllerInstance.attach(element);
1079
1997
  });
1080
- // Setup @on event handlers for controller
1081
- setupEventHandlers(controllerInstance, element);
1082
1998
  // Setup @observe observers for controller
1083
1999
  setupObservers(controllerInstance, element);
1084
2000
  // Setup @channel handlers for controller
1085
2001
  setupResponseHandlers(controllerInstance, element);
2002
+ // Setup @on event handlers for controller
2003
+ setupEventHandlers(controllerInstance, element);
1086
2004
  element.dispatchEvent(new CustomEvent('@snice/controller-attached', {
1087
2005
  detail: { name: controllerName, controller: controllerInstance }
1088
2006
  }));
@@ -1108,12 +2026,12 @@ async function detachController(element) {
1108
2026
  await controllerInstance.detach(element);
1109
2027
  }
1110
2028
  controllerInstance.element = null;
1111
- // Cleanup @on event handlers for controller
1112
- cleanupEventHandlers(controllerInstance);
1113
2029
  // Cleanup @observe observers for controller
1114
2030
  cleanupObservers(controllerInstance);
1115
2031
  // Cleanup @channel handlers for controller
1116
2032
  cleanupResponseHandlers(controllerInstance);
2033
+ // Cleanup @on event handlers for controller
2034
+ cleanupEventHandlers(controllerInstance);
1117
2035
  // Cleanup the controller scope
1118
2036
  if (scope) {
1119
2037
  await scope.cleanup();
@@ -1196,33 +2114,168 @@ function useNativeElementControllers() {
1196
2114
  });
1197
2115
  }
1198
2116
  }
1199
- });
1200
- // Start observing when DOM is ready
1201
- if (document.readyState === 'loading') {
1202
- document.addEventListener('DOMContentLoaded', () => {
1203
- // Process existing elements (excluding custom elements)
1204
- document.querySelectorAll('[controller]:not([class*="-"])').forEach(processElement);
1205
- // Start observing
1206
- observer.observe(document.body, {
1207
- attributes: true,
1208
- attributeFilter: ['controller'],
1209
- childList: true,
1210
- subtree: true
1211
- });
1212
- });
2117
+ });
2118
+ // Start observing when DOM is ready
2119
+ if (document.readyState === 'loading') {
2120
+ document.addEventListener('DOMContentLoaded', () => {
2121
+ // Process existing elements (excluding custom elements)
2122
+ document.querySelectorAll('[controller]:not([class*="-"])').forEach(processElement);
2123
+ // Start observing
2124
+ observer.observe(document.body, {
2125
+ attributes: true,
2126
+ attributeFilter: ['controller'],
2127
+ childList: true,
2128
+ subtree: true
2129
+ });
2130
+ });
2131
+ }
2132
+ else {
2133
+ // DOM already loaded
2134
+ document.querySelectorAll('[controller]:not([class*="-"])').forEach(processElement);
2135
+ observer.observe(document.body, {
2136
+ attributes: true,
2137
+ attributeFilter: ['controller'],
2138
+ childList: true,
2139
+ subtree: true
2140
+ });
2141
+ }
2142
+ // Store observer reference for cleanup if needed
2143
+ globalThis.sniceNativeControllerObserver = observer;
2144
+ }
2145
+
2146
+ /**
2147
+ * @context decorator for receiving router context updates
2148
+ */
2149
+ const CONTEXT_HANDLERS = getSymbol('context-handlers');
2150
+ /**
2151
+ * @context decorator for receiving router context updates
2152
+ *
2153
+ * @example
2154
+ * ```typescript
2155
+ * @element('my-layout')
2156
+ * class MyLayout extends HTMLElement {
2157
+ * @context
2158
+ * handleContext(ctx: Context) {
2159
+ * this.renderNav(ctx.placards, ctx.currentRoute);
2160
+ * }
2161
+ *
2162
+ * @context({ debounce: 300 })
2163
+ * handleContextDebounced(ctx: Context) {
2164
+ * // Called after 300ms of no updates
2165
+ * }
2166
+ * }
2167
+ * ```
2168
+ */
2169
+ function context$1(options = {}) {
2170
+ return function (originalMethod, context) {
2171
+ const methodName = context.name;
2172
+ const initKey = `__context_init_${methodName}`;
2173
+ context.addInitializer(function () {
2174
+ const constructor = this.constructor;
2175
+ if (constructor[initKey])
2176
+ return;
2177
+ constructor[initKey] = true;
2178
+ if (!constructor[CONTEXT_HANDLERS]) {
2179
+ constructor[CONTEXT_HANDLERS] = [];
2180
+ }
2181
+ constructor[CONTEXT_HANDLERS].push({
2182
+ methodName,
2183
+ method: originalMethod,
2184
+ options,
2185
+ });
2186
+ });
2187
+ return originalMethod;
2188
+ };
2189
+ }
2190
+ /**
2191
+ * Setup context handler for an element instance
2192
+ * Called automatically during element connection
2193
+ */
2194
+ function setupContextHandler(element) {
2195
+ const handlers = element.constructor[CONTEXT_HANDLERS];
2196
+ if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
2197
+ return;
2198
+ }
2199
+ // Get the Context instance from the router
2200
+ const ctx = element[CONTEXT_HANDLER];
2201
+ if (!ctx) {
2202
+ return;
2203
+ }
2204
+ // Store the Context instance for cleanup
2205
+ element[NAVIGATION_CONTEXT_INSTANCE] = ctx;
2206
+ // Register each handler with the Context
2207
+ for (const handler of handlers) {
2208
+ const { methodName, method, options } = handler;
2209
+ const wrappedMethodName = `__wrapped_${methodName}`;
2210
+ // Create wrapped method with timing controls
2211
+ element[wrappedMethodName] = function (context) {
2212
+ // Skip if already called once
2213
+ if (options.once && element[CONTEXT_CALLED]) {
2214
+ return;
2215
+ }
2216
+ const callMethod = () => {
2217
+ method.call(element, context);
2218
+ // Handle once option
2219
+ if (options.once) {
2220
+ element[CONTEXT_CALLED] = true;
2221
+ // Unregister after first call
2222
+ const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
2223
+ if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
2224
+ ctx[CONTEXT_UNREGISTER](element);
2225
+ }
2226
+ }
2227
+ };
2228
+ // Handle debounce
2229
+ if (options.debounce) {
2230
+ clearTimeout(element[CONTEXT_TIMER]);
2231
+ element[CONTEXT_TIMER] = setTimeout(callMethod, options.debounce);
2232
+ }
2233
+ // Handle throttle
2234
+ else if (options.throttle) {
2235
+ const now = Date.now();
2236
+ const lastCall = element[CONTEXT_TIMER] || 0;
2237
+ if (now - lastCall >= options.throttle) {
2238
+ element[CONTEXT_TIMER] = now;
2239
+ callMethod();
2240
+ }
2241
+ }
2242
+ // No timing options - call immediately
2243
+ else {
2244
+ callMethod();
2245
+ }
2246
+ };
2247
+ // Register with the Context using the wrapped method name
2248
+ if (typeof ctx[CONTEXT_REGISTER] === 'function') {
2249
+ ctx[CONTEXT_REGISTER](element, wrappedMethodName);
2250
+ }
2251
+ }
2252
+ }
2253
+ /**
2254
+ * Cleanup context handler for an element instance
2255
+ * Called automatically during element disconnection
2256
+ */
2257
+ function cleanupContextHandler(element) {
2258
+ const handlers = element.constructor[CONTEXT_HANDLERS];
2259
+ if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
2260
+ return;
2261
+ }
2262
+ // Clear any pending debounce timer
2263
+ for (const handler of handlers) {
2264
+ if (handler.options.debounce && element[CONTEXT_TIMER]) {
2265
+ clearTimeout(element[CONTEXT_TIMER]);
2266
+ delete element[CONTEXT_TIMER];
2267
+ }
2268
+ // Clean up wrapped method
2269
+ const wrappedMethodName = `__wrapped_${handler.methodName}`;
2270
+ delete element[wrappedMethodName];
1213
2271
  }
1214
- else {
1215
- // DOM already loaded
1216
- document.querySelectorAll('[controller]:not([class*="-"])').forEach(processElement);
1217
- observer.observe(document.body, {
1218
- attributes: true,
1219
- attributeFilter: ['controller'],
1220
- childList: true,
1221
- subtree: true
1222
- });
2272
+ // Unregister from Context if available
2273
+ const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
2274
+ if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
2275
+ ctx[CONTEXT_UNREGISTER](element);
1223
2276
  }
1224
- // Store observer reference for cleanup if needed
1225
- globalThis.sniceNativeControllerObserver = observer;
2277
+ delete element[NAVIGATION_CONTEXT_INSTANCE];
2278
+ delete element[CONTEXT_CALLED];
1226
2279
  }
1227
2280
 
1228
2281
  /**
@@ -1283,6 +2336,87 @@ class SimpleArray {
1283
2336
  }
1284
2337
  }
1285
2338
 
2339
+ var _a, _b, _c;
2340
+ // Symbol for storing the Set of elements
2341
+ const REGISTERED_ELEMENTS_SET = Symbol('registered-elements-set');
2342
+ // Counter for generating unique context IDs
2343
+ let contextIdCounter = 0;
2344
+ /**
2345
+ * Represents the bundled router state that can notify registered elements of changes
2346
+ */
2347
+ class Context {
2348
+ constructor(context = {}, placards = [], currentRoute = '', routeParams = {}) {
2349
+ this[_a] = new WeakMap();
2350
+ this[_b] = new Set();
2351
+ this[_c] = false;
2352
+ this.id = contextIdCounter++;
2353
+ this.application = context;
2354
+ this.navigation = {
2355
+ placards,
2356
+ route: currentRoute,
2357
+ params: routeParams
2358
+ };
2359
+ }
2360
+ /**
2361
+ * Register an element to receive context updates
2362
+ * @internal Used by @context decorator
2363
+ */
2364
+ [(_a = REGISTERED_ELEMENTS, _b = REGISTERED_ELEMENTS_SET, _c = IS_UPDATING, CONTEXT_REGISTER)](element, methodName) {
2365
+ this[REGISTERED_ELEMENTS].set(element, methodName);
2366
+ this[REGISTERED_ELEMENTS_SET].add(element);
2367
+ }
2368
+ /**
2369
+ * Unregister an element from receiving context updates
2370
+ * @internal Used by @context decorator cleanup
2371
+ */
2372
+ [CONTEXT_UNREGISTER](element) {
2373
+ this[REGISTERED_ELEMENTS].delete(element);
2374
+ this[REGISTERED_ELEMENTS_SET].delete(element);
2375
+ }
2376
+ /**
2377
+ * Update the context and notify all registered elements
2378
+ * Prevents infinite loops by tracking update state
2379
+ */
2380
+ update(context, placards, currentRoute, routeParams) {
2381
+ // Prevent infinite loops
2382
+ if (this[IS_UPDATING]) {
2383
+ return;
2384
+ }
2385
+ this[IS_UPDATING] = true;
2386
+ // Update properties (id remains immutable)
2387
+ this.application = context;
2388
+ this.navigation.placards = placards;
2389
+ this.navigation.route = currentRoute;
2390
+ this.navigation.params = routeParams;
2391
+ // Notify all registered elements by calling their methods directly
2392
+ const elementsSet = this[REGISTERED_ELEMENTS_SET];
2393
+ const elementsMap = this[REGISTERED_ELEMENTS];
2394
+ for (const element of elementsSet) {
2395
+ const methodName = elementsMap.get(element);
2396
+ if (methodName && typeof element[methodName] === 'function') {
2397
+ try {
2398
+ element[methodName](this);
2399
+ }
2400
+ catch (error) {
2401
+ // Log error but continue notifying other elements
2402
+ console.error(`Error calling @context method ${methodName}:`, error);
2403
+ }
2404
+ }
2405
+ }
2406
+ this[IS_UPDATING] = false;
2407
+ }
2408
+ /**
2409
+ * Notify a specific element of the current context state
2410
+ * @internal Used by @context decorator
2411
+ */
2412
+ [CONTEXT_NOTIFY_ELEMENT](element) {
2413
+ const methodName = this[REGISTERED_ELEMENTS].get(element);
2414
+ if (methodName && typeof element[methodName] === 'function') {
2415
+ element[methodName](this);
2416
+ }
2417
+ }
2418
+ }
2419
+
1286
2420
  /**
1287
2421
  * Detects the type constructor from an initial value
1288
2422
  * @param initialValue - The default value assigned to a property field (e.g., `name = "default"` -> "default")
@@ -1411,6 +2545,297 @@ function valueToAttribute(value, propertyOptions, initialValue) {
1411
2545
  }
1412
2546
  }
1413
2547
 
2548
+ /**
2549
+ * @render and @styles decorators for Snice v3.0.0
2550
+ * Provides automatic differential rendering on property changes
2551
+ */
2552
+ /**
2553
+ * Global render scheduler for microtask batching
2554
+ * Batches multiple property changes into a single render
2555
+ */
2556
+ class RenderScheduler {
2557
+ constructor() {
2558
+ this.pending = new Set();
2559
+ this.scheduled = false;
2560
+ }
2561
+ /**
2562
+ * Schedule an element for rendering
2563
+ * Batches renders in a microtask unless sync option is enabled
2564
+ */
2565
+ schedule(element, options) {
2566
+ // Sync rendering - execute immediately
2567
+ if (options.sync) {
2568
+ performRender(element, options);
2569
+ return;
2570
+ }
2571
+ // Async rendering - batch in microtask
2572
+ this.pending.add(element);
2573
+ if (!this.scheduled) {
2574
+ this.scheduled = true;
2575
+ queueMicrotask(() => this.flush());
2576
+ }
2577
+ }
2578
+ /**
2579
+ * Flush all pending renders
2580
+ */
2581
+ flush() {
2582
+ const elements = Array.from(this.pending);
2583
+ this.pending.clear();
2584
+ this.scheduled = false;
2585
+ for (const element of elements) {
2586
+ const options = element[RENDER_OPTIONS] || {};
2587
+ performRender(element, options);
2588
+ }
2589
+ }
2590
+ }
2591
+ const renderScheduler = new RenderScheduler();
2592
+ /**
2593
+ * Perform the actual render of an element
2594
+ */
2595
+ function performRender(element, options, precomputedResult) {
2596
+ const renderMethod = element[RENDER_METHOD];
2597
+ if (!renderMethod)
2598
+ return;
2599
+ // If once is true and we've already rendered, skip
2600
+ if (options.once && element[RENDER_INSTANCE]) {
2601
+ return;
2602
+ }
2603
+ try {
2604
+ // Use precomputed result if provided, otherwise call the render method
2605
+ const result = precomputedResult !== undefined ? precomputedResult : renderMethod.call(element);
2606
+ // Check if differential rendering is disabled (expects string)
2607
+ const differential = options.differential !== false;
2608
+ if (!differential) {
2609
+ // Non-differential expects string
2610
+ if (typeof result !== 'string') {
2611
+ console.warn('Render method with differential: false must return a string');
2612
+ return;
2613
+ }
2614
+ // Simple string rendering
2615
+ if (!element.shadowRoot) {
2616
+ element.attachShadow({ mode: 'open' });
2617
+ }
2618
+ element.shadowRoot.innerHTML = result;
2619
+ // Mark scheduled flag as false
2620
+ element[RENDER_SCHEDULED] = false;
2621
+ // Call all registered render callbacks
2622
+ const callbacks = element[RENDER_CALLBACKS];
2623
+ if (callbacks && callbacks.length > 0) {
2624
+ const cbs = [...callbacks];
2625
+ element[RENDER_CALLBACKS] = [];
2626
+ cbs.forEach(cb => cb());
2627
+ }
2628
+ return;
2629
+ }
2630
+ if (!isTemplateResult(result)) {
2631
+ console.warn('Render method must return html`` template result');
2632
+ return;
2633
+ }
2634
+ // Get or create template instance (differential rendering)
2635
+ let instance = element[RENDER_INSTANCE];
2636
+ if (!instance) {
2637
+ // First render - create shadow root if needed and initial instance
2638
+ if (!element.shadowRoot) {
2639
+ element.attachShadow({ mode: 'open' });
2640
+ }
2641
+ instance = new TemplateInstance(result);
2642
+ element[RENDER_INSTANCE] = instance;
2643
+ // Create the fragment but don't commit values yet
2644
+ const fragment = instance.renderFragment();
2645
+ // Append to shadow root first so getRootNode() works in event handlers
2646
+ element.shadowRoot.appendChild(fragment);
2647
+ // Now commit values (this binds event handlers with correct host)
2648
+ instance.update(result.values);
2649
+ }
2650
+ else {
2651
+ // Subsequent render - just update the parts (differential rendering!)
2652
+ instance.update(result.values);
2653
+ }
2654
+ // Mark scheduled flag as false
2655
+ element[RENDER_SCHEDULED] = false;
2656
+ // Call all registered render callbacks (for testing/debugging)
2657
+ const callbacks = element[RENDER_CALLBACKS];
2658
+ if (callbacks && callbacks.length > 0) {
2659
+ const cbs = [...callbacks];
2660
+ element[RENDER_CALLBACKS] = [];
2661
+ cbs.forEach(cb => cb());
2662
+ }
2663
+ }
2664
+ catch (error) {
2665
+ console.error('Error rendering element:', error);
2666
+ }
2667
+ }
2668
+ /**
2669
+ * Request a render for an element
2670
+ * Respects debounce/throttle/once/sync options
2671
+ * @param immediate - Force immediate render (used for initial render)
2672
+ */
2673
+ function requestRender(element, immediate = false) {
2674
+ const options = element[RENDER_OPTIONS] || {};
2675
+ // Handle once option
2676
+ if (options.once && element[RENDER_INSTANCE]) {
2677
+ return;
2678
+ }
2679
+ // Force immediate render (for initial render)
2680
+ if (immediate) {
2681
+ performRender(element, options);
2682
+ return;
2683
+ }
2684
+ // Handle debounce
2685
+ if (options.debounce !== undefined && options.debounce > 0) {
2686
+ if (!element[RENDER_TIMERS]) {
2687
+ element[RENDER_TIMERS] = {};
2688
+ }
2689
+ clearTimeout(element[RENDER_TIMERS].debounce);
2690
+ element[RENDER_TIMERS].debounce = setTimeout(() => {
2691
+ renderScheduler.schedule(element, options);
2692
+ }, options.debounce);
2693
+ return;
2694
+ }
2695
+ // Handle throttle
2696
+ if (options.throttle !== undefined && options.throttle > 0) {
2697
+ if (!element[RENDER_TIMERS]) {
2698
+ element[RENDER_TIMERS] = { lastThrottle: 0 };
2699
+ }
2700
+ const timers = element[RENDER_TIMERS];
2701
+ const now = Date.now();
2702
+ if (timers.lastThrottle === 0 || now - timers.lastThrottle >= options.throttle) {
2703
+ timers.lastThrottle = now;
2704
+ renderScheduler.schedule(element, options);
2705
+ }
2706
+ else {
2707
+ // Schedule for later if not already scheduled
2708
+ if (!timers.throttleTimer) {
2709
+ const remaining = options.throttle - (now - timers.lastThrottle);
2710
+ timers.throttleTimer = setTimeout(() => {
2711
+ timers.throttleTimer = null;
2712
+ timers.lastThrottle = Date.now();
2713
+ renderScheduler.schedule(element, options);
2714
+ }, remaining);
2715
+ }
2716
+ }
2717
+ return;
2718
+ }
2719
+ // Normal rendering (with microtask batching unless sync)
2720
+ renderScheduler.schedule(element, options);
2721
+ }
2722
+ /**
2723
+ * @render decorator for component rendering
2724
+ *
2725
+ * Marks a method as the render method for the component.
2726
+ * The method should return html`...` template.
2727
+ * Automatically re-renders when properties change (unless once: true).
2728
+ *
2729
+ * @example
2730
+ * ```typescript
2731
+ * @render()
2732
+ * renderContent() {
2733
+ * return html`<div>${this.count}</div>`;
2734
+ * }
2735
+ * ```
2736
+ *
2737
+ * @example
2738
+ * ```typescript
2739
+ * // Debounced rendering
2740
+ * @render({ debounce: 100 })
2741
+ * renderContent() {
2742
+ * return html`<div>${this.searchTerm}</div>`;
2743
+ * }
2744
+ * ```
2745
+ *
2746
+ * @example
2747
+ * ```typescript
2748
+ * // Render only once (manual re-renders only)
2749
+ * @render({ once: true })
2750
+ * renderContent() {
2751
+ * return html`<div>Static content</div>`;
2752
+ * }
2753
+ * ```
2754
+ */
2755
+ function render(options = {}) {
2756
+ return function (originalMethod, context) {
2757
+ context.name;
2758
+ context.addInitializer(function () {
2759
+ // Store the render method and options
2760
+ this[RENDER_METHOD] = originalMethod;
2761
+ this[RENDER_OPTIONS] = options;
2762
+ });
2763
+ // Return wrapped method that triggers re-render when called manually
2764
+ return function (...args) {
2765
+ // Call original method to get the template
2766
+ const result = originalMethod.apply(this, args);
2767
+ // Always render when method is called manually (even if once: true)
2768
+ // Force immediate render to bypass all options, pass precomputed result to avoid calling method twice
2769
+ performRender(this, {}, result);
2770
+ return result;
2771
+ };
2772
+ };
2773
+ }
2774
+ /**
2775
+ * @styles decorator for component styles
2776
+ *
2777
+ * Marks a method as the styles method for the component.
2778
+ * The method should return css`...` template.
2779
+ * Styles are applied once when the component is connected.
2780
+ *
2781
+ * @example
2782
+ * ```typescript
2783
+ * @styles()
2784
+ * styles() {
2785
+ * return css`:host { display: block; }`;
2786
+ * }
2787
+ * ```
2788
+ */
2789
+ function styles() {
2790
+ return function (originalMethod, context) {
2791
+ context.addInitializer(function () {
2792
+ // Store the styles method
2793
+ this[STYLES_METHOD] = originalMethod;
2794
+ });
2795
+ return originalMethod;
2796
+ };
2797
+ }
2798
+ /**
2799
+ * Apply styles to an element
2800
+ * Called during element initialization
2801
+ */
2802
+ function applyStyles(element) {
2803
+ const stylesMethod = element[STYLES_METHOD];
2804
+ if (!stylesMethod)
2805
+ return;
2806
+ // Only apply once
2807
+ if (element[STYLES_APPLIED])
2808
+ return;
2809
+ element[STYLES_APPLIED] = true;
2810
+ try {
2811
+ const result = stylesMethod.call(element);
2812
+ if (!isCSSResult(result)) {
2813
+ console.warn('Styles method must return css`` template result');
2814
+ return;
2815
+ }
2816
+ // Ensure shadow root exists
2817
+ if (!element.shadowRoot) {
2818
+ element.attachShadow({ mode: 'open' });
2819
+ }
2820
+ // Create base styles for meta elements (if, case)
2821
+ const baseStyleSheet = new CSSStyleSheet();
2822
+ baseStyleSheet.replaceSync('if, case { display: contents; }');
2823
+ // Try to use constructable stylesheets for better performance
2824
+ if (element.shadowRoot && result.styleSheet && 'adoptedStyleSheets' in element.shadowRoot) {
2825
+ element.shadowRoot.adoptedStyleSheets = [baseStyleSheet, result.styleSheet];
2826
+ }
2827
+ else if (element.shadowRoot) {
2828
+ // Fallback to <style> tag
2829
+ const style = document.createElement('style');
2830
+ style.textContent = 'if, case { display: contents; }\n' + result.cssText;
2831
+ element.shadowRoot.appendChild(style);
2832
+ }
2833
+ }
2834
+ catch (error) {
2835
+ console.error('Error applying styles:', error);
2836
+ }
2837
+ }
2838
+
1414
2839
  /**
1415
2840
  * Applies core element functionality to a constructor
1416
2841
  * This is shared between @element and @page decorators
@@ -1455,6 +2880,9 @@ function applyElementFunctionality(constructor) {
1455
2880
  enumerable: true,
1456
2881
  configurable: true
1457
2882
  });
2883
+ // Note: rendered promise is stored via symbols RENDERED_PROMISE and RENDERED_RESOLVE
2884
+ // It's not exposed as a public property - only accessible via test utilities
2885
+ // This prevents accidental misuse in production code
1458
2886
  // Add controller property
1459
2887
  Object.defineProperty(constructor.prototype, 'controller', {
1460
2888
  get() {
@@ -1492,6 +2920,7 @@ function applyElementFunctionality(constructor) {
1492
2920
  // Re-establish handlers that get cleaned up on disconnect
1493
2921
  setupEventHandlers(this, this);
1494
2922
  setupResponseHandlers(this, this);
2923
+ setupContextHandler(this);
1495
2924
  // Re-establish observers that get cleaned up on disconnect
1496
2925
  try {
1497
2926
  setupObservers(this, this);
@@ -1506,6 +2935,9 @@ function applyElementFunctionality(constructor) {
1506
2935
  return;
1507
2936
  }
1508
2937
  try {
2938
+ // Mark that properties are being initialized from attributes
2939
+ // This allows property setters to work during initialization
2940
+ this[PROPERTIES_INITIALIZED] = true;
1509
2941
  // Initialize properties from attributes before rendering
1510
2942
  const properties = constructor[PROPERTIES];
1511
2943
  if (properties) {
@@ -1524,82 +2956,69 @@ function applyElementFunctionality(constructor) {
1524
2956
  }
1525
2957
  }
1526
2958
  }
1527
- // Mark that properties have been initialized
1528
- this[PROPERTIES_INITIALIZED] = true;
2959
+ // Apply any properties that were set before element was connected
2960
+ // BUT only if they don't have an HTML attribute (don't override HTML attributes)
2961
+ if (this[PRE_INIT_PROPERTY_VALUES]) {
2962
+ for (const [propName, propValue] of this[PRE_INIT_PROPERTY_VALUES]) {
2963
+ // Remove from map first so getter doesn't return it during setter call
2964
+ this[PRE_INIT_PROPERTY_VALUES].delete(propName);
2965
+ // Only apply pre-init value if NO HTML attribute exists
2966
+ // This prevents field initializers from overwriting HTML attributes
2967
+ const propOptions = properties?.get(propName);
2968
+ const attributeName = typeof propOptions?.attribute === 'string' ? propOptions.attribute : propName.toLowerCase();
2969
+ if (!this.hasAttribute(attributeName)) {
2970
+ this[propName] = propValue;
2971
+ }
2972
+ }
2973
+ // Clear the pre-init values map
2974
+ delete this[PRE_INIT_PROPERTY_VALUES];
2975
+ }
1529
2976
  // Properties are now stateless and read from DOM attributes only
1530
2977
  // Initial values are not automatically reflected
1531
- // Create shadow root if it doesn't exist
1532
- if (!this.shadowRoot) {
1533
- this.attachShadow({ mode: 'open' });
1534
- }
1535
- // Build the shadow DOM content
1536
- let shadowContent = '';
1537
- // Add HTML first (maintaining original order)
1538
- if (this.html) {
1539
- try {
1540
- const htmlResult = this.html();
1541
- // Handle both async and sync html
1542
- const htmlContent = htmlResult instanceof Promise ? await htmlResult : htmlResult;
1543
- if (htmlContent !== undefined) {
1544
- shadowContent += htmlContent;
2978
+ // v3.0.0: Apply @styles decorator if present
2979
+ // This creates the shadow root and applies styles
2980
+ applyStyles(this);
2981
+ // v3.0.0: Perform initial @render if present
2982
+ // This uses differential rendering with template system
2983
+ // Defer initial render to next microtask to allow property bindings
2984
+ // from parent to be set first (avoids infinite loops in nested elements)
2985
+ if (this[RENDER_METHOD]) {
2986
+ queueMicrotask(() => {
2987
+ requestRender(this, true);
2988
+ // Setup observers after first render completes so shadow DOM content exists
2989
+ try {
2990
+ setupObservers(this, this);
1545
2991
  }
1546
- }
1547
- catch (error) {
1548
- console.error(`Error in html() method for ${this.tagName}:`, error);
1549
- }
2992
+ catch (error) {
2993
+ console.error(`Error setting up observers for ${this.tagName}:`, error);
2994
+ }
2995
+ // Mark element as ready after initial render completes
2996
+ if (this[READY_RESOLVE]) {
2997
+ this[READY_RESOLVE]();
2998
+ this[READY_RESOLVE] = null;
2999
+ }
3000
+ });
1550
3001
  }
1551
- // Add CSS after HTML (maintaining original order)
1552
- if (this.css) {
3002
+ else {
3003
+ // No render method, setup observers immediately
1553
3004
  try {
1554
- const cssResult = this.css();
1555
- // Handle both async and sync css
1556
- const cssResolved = cssResult instanceof Promise ? await cssResult : cssResult;
1557
- if (cssResolved) {
1558
- // Handle both string and array of strings
1559
- const cssContent = Array.isArray(cssResolved) ? cssResolved.join('\n') : cssResolved;
1560
- // No need for scoping with Shadow DOM, but add data attribute for compatibility
1561
- shadowContent += `<style data-component-css>${cssContent}</style>`;
1562
- }
3005
+ setupObservers(this, this);
1563
3006
  }
1564
3007
  catch (error) {
1565
- console.error(`Error in css() method for ${this.tagName}:`, error);
3008
+ console.error(`Error setting up observers for ${this.tagName}:`, error);
1566
3009
  }
1567
- }
1568
- // Set shadow DOM content
1569
- if (shadowContent) {
1570
- this.shadowRoot.innerHTML = shadowContent;
1571
- }
1572
- // Render all @part methods into their corresponding elements
1573
- const parts = constructor[PARTS];
1574
- if (parts && this.shadowRoot) {
1575
- for (const [partName, partHandler] of parts) {
1576
- try {
1577
- const partElement = this.shadowRoot.querySelector(`[part="${partName}"]`);
1578
- if (partElement) {
1579
- // For initial render, call original method directly to avoid timing restrictions
1580
- const partResult = partHandler.method.call(this);
1581
- const partContent = partResult instanceof Promise ? await partResult : partResult;
1582
- if (partContent !== undefined) {
1583
- partElement.innerHTML = partContent;
1584
- }
1585
- }
1586
- }
1587
- catch (error) {
1588
- console.error(`Error rendering @part('${partName}') in ${this.tagName}:`, error);
1589
- }
3010
+ // Mark element as ready immediately if no render method
3011
+ if (this[READY_RESOLVE]) {
3012
+ this[READY_RESOLVE]();
3013
+ this[READY_RESOLVE] = null;
1590
3014
  }
1591
3015
  }
1592
- // Setup @on event handlers - use element for host events, shadow root for delegated events
3016
+ // Setup @on event handlers (v2.5.4 compatibility restored!)
1593
3017
  setupEventHandlers(this, this);
1594
3018
  // Setup @respond handlers for elements
1595
3019
  setupResponseHandlers(this, this);
1596
- // Setup @observe observers
1597
- try {
1598
- setupObservers(this, this);
1599
- }
1600
- catch (error) {
1601
- console.error(`Error setting up observers for ${this.tagName}:`, error);
1602
- }
3020
+ // Setup @context handler for elements
3021
+ setupContextHandler(this);
1603
3022
  // Mark as initialized
1604
3023
  this[INITIALIZED] = true;
1605
3024
  // NOW call the original user-defined connectedCallback after shadow DOM is set up
@@ -1608,11 +3027,8 @@ function applyElementFunctionality(constructor) {
1608
3027
  }
1609
3028
  }
1610
3029
  finally {
1611
- // Always mark element as ready, even if there were errors
1612
- if (this[READY_RESOLVE]) {
1613
- this[READY_RESOLVE]();
1614
- this[READY_RESOLVE] = null; // Clear the resolver
1615
- }
3030
+ // Ready is now resolved inside the render microtask (or immediately if no render)
3031
+ // This ensures ready waits for initial render to complete
1616
3032
  }
1617
3033
  // Call @ready handlers after everything is set up and ready promise is resolved
1618
3034
  const readyHandlers = constructor[READY_HANDLERS];
@@ -1649,10 +3065,12 @@ function applyElementFunctionality(constructor) {
1649
3065
  console.error(`Failed to detach controller:`, error);
1650
3066
  });
1651
3067
  }
1652
- // Cleanup @on event handlers
3068
+ // Cleanup @on event handlers (v2.5.4 compatibility restored!)
1653
3069
  cleanupEventHandlers(this);
1654
3070
  // Cleanup @respond handlers
1655
3071
  cleanupResponseHandlers(this);
3072
+ // Cleanup @context handler
3073
+ cleanupContextHandler(this);
1656
3074
  // Cleanup @observe observers
1657
3075
  cleanupObservers(this);
1658
3076
  };
@@ -1715,6 +3133,10 @@ function applyElementFunctionality(constructor) {
1715
3133
  }
1716
3134
  }
1717
3135
  }
3136
+ // Trigger auto-render on attribute change (same as property setter)
3137
+ if (this[RENDER_METHOD] && this[INITIALIZED]) {
3138
+ requestRender(this);
3139
+ }
1718
3140
  }
1719
3141
  }
1720
3142
  break;
@@ -1754,7 +3176,7 @@ function applyElementFunctionality(constructor) {
1754
3176
  }
1755
3177
  };
1756
3178
  }
1757
- function element(tagName) {
3179
+ function element(tagName, options) {
1758
3180
  return function (constructor, context) {
1759
3181
  // Transfer metadata from context to constructor
1760
3182
  if (context.metadata && context.metadata[PROPERTIES]) {
@@ -1765,6 +3187,11 @@ function element(tagName) {
1765
3187
  constructor[PROPERTIES].set(key, value);
1766
3188
  }
1767
3189
  }
3190
+ // Set up form association if requested via options
3191
+ // MUST be done BEFORE applyElementFunctionality and customElements.define
3192
+ if (options?.formAssociated === true) {
3193
+ constructor.formAssociated = true;
3194
+ }
1768
3195
  applyElementFunctionality(constructor);
1769
3196
  customElements.define(tagName, constructor);
1770
3197
  return constructor;
@@ -1820,29 +3247,43 @@ function property(options) {
1820
3247
  if (attrValue !== null) {
1821
3248
  return parseAttributeValue(attrValue, finalOptions || {}, undefined, initialValue);
1822
3249
  }
1823
- // For Boolean properties that have been explicitly set via attribute,
3250
+ // For Boolean properties that have been explicitly set via attribute (and then removed),
1824
3251
  // follow HTML boolean attribute semantics (absence = false)
1825
3252
  const inferredType = finalOptions?.type || detectType(initialValue);
1826
3253
  if (inferredType === Boolean && this[EXPLICITLY_SET_PROPERTIES]?.has(propertyKey)) {
1827
3254
  return false;
1828
3255
  }
1829
- // Otherwise return initial value
3256
+ // Check for pre-init property values (set before element was connected)
3257
+ if (this[PRE_INIT_PROPERTY_VALUES]?.has(propertyKey)) {
3258
+ return this[PRE_INIT_PROPERTY_VALUES].get(propertyKey);
3259
+ }
3260
+ // Otherwise return initial value (respects default values like showRememberMe = true)
1830
3261
  return initialValue;
1831
3262
  },
1832
3263
  set(newValue) {
1833
- // Get old value from stored state for change detection
1834
- if (!this[PROPERTY_VALUES]) {
1835
- this[PROPERTY_VALUES] = {};
1836
- }
1837
- const oldValue = this[PROPERTY_VALUES][propertyKey];
3264
+ // Get old value by calling the getter (which reads from attribute)
3265
+ const oldValue = this[propertyKey];
1838
3266
  // Check if value actually changed
1839
3267
  if (oldValue === newValue)
1840
3268
  return;
1841
- // Update stored value
1842
- this[PROPERTY_VALUES][propertyKey] = newValue;
1843
- // Always reflect to DOM - properties are always backed by attributes
3269
+ // Don't reflect to DOM until connectedCallback has started
3270
+ // This prevents field initializers from overwriting HTML attributes
3271
+ if (!this[PROPERTIES_INITIALIZED]) {
3272
+ // Store value for later application when element is connected
3273
+ if (!this[PRE_INIT_PROPERTY_VALUES]) {
3274
+ this[PRE_INIT_PROPERTY_VALUES] = new Map();
3275
+ }
3276
+ this[PRE_INIT_PROPERTY_VALUES].set(propertyKey, newValue);
3277
+ return;
3278
+ }
3279
+ // Reflect to DOM - properties are backed by attributes
1844
3280
  const attributeName = typeof finalOptions.attribute === 'string' ? finalOptions.attribute : propertyKey.toLowerCase();
1845
3281
  const attributeValue = valueToAttribute(newValue, finalOptions, initialValue);
3282
+ // Mark as explicitly set for boolean handling
3283
+ if (!this[EXPLICITLY_SET_PROPERTIES]) {
3284
+ this[EXPLICITLY_SET_PROPERTIES] = new Set();
3285
+ }
3286
+ this[EXPLICITLY_SET_PROPERTIES].add(propertyKey);
1846
3287
  // Flag to prevent attributeChangedCallback from triggering watchers for this change
1847
3288
  if (!this._settingFromProperty)
1848
3289
  this._settingFromProperty = new Set();
@@ -1884,8 +3325,12 @@ function property(options) {
1884
3325
  }
1885
3326
  }
1886
3327
  }
1887
- if (this.requestUpdate) {
1888
- this.requestUpdate(propertyKey, oldValue);
3328
+ // v3.0.0: Trigger auto-render on property change
3329
+ // This respects @render options (debounce, throttle, once, sync)
3330
+ // Only trigger renders after element is fully initialized to avoid
3331
+ // infinite loops during initial setup
3332
+ if (this[RENDER_METHOD] && this[INITIALIZED]) {
3333
+ requestRender(this);
1889
3334
  }
1890
3335
  },
1891
3336
  configurable: true,
@@ -1980,12 +3425,15 @@ function queryAll(selector, options = {}) {
1980
3425
  function watch(...propertyNames) {
1981
3426
  return function (target, context) {
1982
3427
  const methodName = context.name;
3428
+ const initKey = `__watch_init_${methodName}`;
1983
3429
  context.addInitializer(function () {
1984
3430
  const constructor = this.constructor;
3431
+ if (constructor[initKey])
3432
+ return;
3433
+ constructor[initKey] = true;
1985
3434
  if (!constructor[PROPERTY_WATCHERS]) {
1986
3435
  constructor[PROPERTY_WATCHERS] = new Map();
1987
3436
  }
1988
- // Store the watcher method for each property
1989
3437
  for (const propertyName of propertyNames) {
1990
3438
  if (!constructor[PROPERTY_WATCHERS].has(propertyName)) {
1991
3439
  constructor[PROPERTY_WATCHERS].set(propertyName, []);
@@ -2064,8 +3512,12 @@ function context() {
2064
3512
  function ready() {
2065
3513
  return function (target, context) {
2066
3514
  const methodName = context.name;
3515
+ const initKey = `__ready_init_${methodName}`;
2067
3516
  context.addInitializer(function () {
2068
3517
  const constructor = this.constructor;
3518
+ if (constructor[initKey])
3519
+ return;
3520
+ constructor[initKey] = true;
2069
3521
  if (!constructor[READY_HANDLERS]) {
2070
3522
  constructor[READY_HANDLERS] = [];
2071
3523
  }
@@ -2083,8 +3535,12 @@ function ready() {
2083
3535
  function dispose() {
2084
3536
  return function (target, context) {
2085
3537
  const methodName = context.name;
3538
+ const initKey = `__dispose_init_${methodName}`;
2086
3539
  context.addInitializer(function () {
2087
3540
  const constructor = this.constructor;
3541
+ if (constructor[initKey])
3542
+ return;
3543
+ constructor[initKey] = true;
2088
3544
  if (!constructor[DISPOSE_HANDLERS]) {
2089
3545
  constructor[DISPOSE_HANDLERS] = [];
2090
3546
  }
@@ -2102,8 +3558,12 @@ function dispose() {
2102
3558
  function moved(options = {}) {
2103
3559
  return function (originalMethod, context) {
2104
3560
  const methodName = context.name;
3561
+ const initKey = `__moved_init_${methodName}`;
2105
3562
  context.addInitializer(function () {
2106
3563
  const constructor = this.constructor;
3564
+ if (constructor[initKey])
3565
+ return;
3566
+ constructor[initKey] = true;
2107
3567
  if (!constructor[MOVED_HANDLERS]) {
2108
3568
  constructor[MOVED_HANDLERS] = [];
2109
3569
  }
@@ -2188,102 +3648,20 @@ function adopted(options = {}) {
2188
3648
  return function (...args) {
2189
3649
  // Initialize timers storage if not present
2190
3650
  if (!this[ADOPTED_TIMERS]) {
2191
- this[ADOPTED_TIMERS] = new Map();
2192
- }
2193
- // Get or create timers for this specific method
2194
- if (!this[ADOPTED_TIMERS].has(methodName)) {
2195
- this[ADOPTED_TIMERS].set(methodName, {
2196
- throttleTimer: null,
2197
- debounceTimer: null,
2198
- lastThrottleCall: 0
2199
- });
2200
- }
2201
- const timers = this[ADOPTED_TIMERS].get(methodName);
2202
- // Helper function to execute method
2203
- const executeMethod = (...methodArgs) => {
2204
- return originalMethod.apply(this, methodArgs);
2205
- };
2206
- const hasDebounce = options.debounce !== undefined && options.debounce > 0;
2207
- const hasThrottle = options.throttle !== undefined && options.throttle > 0;
2208
- // Handle timing based on priority: debounce > throttle > immediate
2209
- switch (true) {
2210
- case hasDebounce: {
2211
- clearTimeout(timers.debounceTimer);
2212
- timers.debounceTimer = setTimeout(() => executeMethod(...args), options.debounce);
2213
- return undefined;
2214
- }
2215
- case hasThrottle: {
2216
- const throttleMs = options.throttle;
2217
- const now = Date.now();
2218
- const canExecuteImmediately = timers.lastThrottleCall === 0 || now - timers.lastThrottleCall >= throttleMs;
2219
- if (canExecuteImmediately) {
2220
- timers.lastThrottleCall = now;
2221
- return executeMethod(...args);
2222
- }
2223
- const hasScheduledTimer = !!timers.throttleTimer;
2224
- if (!hasScheduledTimer) {
2225
- const remainingTime = throttleMs - (now - timers.lastThrottleCall);
2226
- timers.throttleTimer = setTimeout(() => {
2227
- timers.throttleTimer = null;
2228
- timers.lastThrottleCall = Date.now();
2229
- executeMethod(...args);
2230
- }, remainingTime);
2231
- }
2232
- return undefined;
2233
- }
2234
- default:
2235
- return executeMethod(...args);
2236
- }
2237
- };
2238
- };
2239
- }
2240
- /**
2241
- * Decorator for methods that render specific parts of the template
2242
- * Parts are identified by the 'part' attribute in the HTML template
2243
- * When the decorated method is called, it automatically re-renders its part
2244
- */
2245
- function part(partName, options = {}) {
2246
- return function (originalMethod, context) {
2247
- const methodName = context.name;
2248
- context.addInitializer(function () {
2249
- const constructor = this.constructor;
2250
- if (!constructor[PARTS]) {
2251
- constructor[PARTS] = new Map();
2252
- }
2253
- constructor[PARTS].set(partName, {
2254
- methodName,
2255
- method: originalMethod
2256
- });
2257
- });
2258
- // Return wrapped method that automatically re-renders the part when called
2259
- return function (...args) {
2260
- // Initialize timers storage if not present
2261
- if (!this[PART_TIMERS]) {
2262
- this[PART_TIMERS] = new Map();
3651
+ this[ADOPTED_TIMERS] = new Map();
2263
3652
  }
2264
- // Get or create timers for this specific part
2265
- if (!this[PART_TIMERS].has(partName)) {
2266
- this[PART_TIMERS].set(partName, {
3653
+ // Get or create timers for this specific method
3654
+ if (!this[ADOPTED_TIMERS].has(methodName)) {
3655
+ this[ADOPTED_TIMERS].set(methodName, {
2267
3656
  throttleTimer: null,
2268
3657
  debounceTimer: null,
2269
3658
  lastThrottleCall: 0
2270
3659
  });
2271
3660
  }
2272
- const timers = this[PART_TIMERS].get(partName);
2273
- // Helper function to execute method and update DOM
2274
- const executeAndUpdate = (...methodArgs) => {
2275
- const result = originalMethod.apply(this, methodArgs);
2276
- const updateDOM = (content) => {
2277
- const hasContent = content !== undefined;
2278
- const hasElement = this.shadowRoot?.querySelector(`[part="${partName}"]`);
2279
- if (hasContent && hasElement) {
2280
- hasElement.innerHTML = content;
2281
- }
2282
- };
2283
- const isPromise = result instanceof Promise;
2284
- return isPromise
2285
- ? result.then(content => { updateDOM(content); return content; })
2286
- : (updateDOM(result), result);
3661
+ const timers = this[ADOPTED_TIMERS].get(methodName);
3662
+ // Helper function to execute method
3663
+ const executeMethod = (...methodArgs) => {
3664
+ return originalMethod.apply(this, methodArgs);
2287
3665
  };
2288
3666
  const hasDebounce = options.debounce !== undefined && options.debounce > 0;
2289
3667
  const hasThrottle = options.throttle !== undefined && options.throttle > 0;
@@ -2291,7 +3669,7 @@ function part(partName, options = {}) {
2291
3669
  switch (true) {
2292
3670
  case hasDebounce: {
2293
3671
  clearTimeout(timers.debounceTimer);
2294
- timers.debounceTimer = setTimeout(() => executeAndUpdate(...args), options.debounce);
3672
+ timers.debounceTimer = setTimeout(() => executeMethod(...args), options.debounce);
2295
3673
  return undefined;
2296
3674
  }
2297
3675
  case hasThrottle: {
@@ -2300,7 +3678,7 @@ function part(partName, options = {}) {
2300
3678
  const canExecuteImmediately = timers.lastThrottleCall === 0 || now - timers.lastThrottleCall >= throttleMs;
2301
3679
  if (canExecuteImmediately) {
2302
3680
  timers.lastThrottleCall = now;
2303
- return executeAndUpdate(...args);
3681
+ return executeMethod(...args);
2304
3682
  }
2305
3683
  const hasScheduledTimer = !!timers.throttleTimer;
2306
3684
  if (!hasScheduledTimer) {
@@ -2308,17 +3686,19 @@ function part(partName, options = {}) {
2308
3686
  timers.throttleTimer = setTimeout(() => {
2309
3687
  timers.throttleTimer = null;
2310
3688
  timers.lastThrottleCall = Date.now();
2311
- executeAndUpdate(...args);
3689
+ executeMethod(...args);
2312
3690
  }, remainingTime);
2313
3691
  }
2314
3692
  return undefined;
2315
3693
  }
2316
3694
  default:
2317
- return executeAndUpdate(...args);
3695
+ return executeMethod(...args);
2318
3696
  }
2319
3697
  };
2320
3698
  };
2321
3699
  }
3700
+ // @part decorator removed in v3.0.0
3701
+ // Use @render with differential rendering instead
2322
3702
 
2323
3703
  /*!
2324
3704
  * pica-route v1.1.2
@@ -3069,6 +4449,8 @@ function Router(options) {
3069
4449
  let currentLayoutName = null; // Track current layout name
3070
4450
  let currentLayoutTimestamp = null; // Track current layout timestamp
3071
4451
  const context = options.context || {}; // Store context for guards
4452
+ // Create Context instance for managing router state
4453
+ const navigationContext = new Context(context, [], '', {});
3072
4454
  function getCurrentLayoutElement(target) {
3073
4455
  const noCurrentLayout = !currentLayoutName || !currentLayoutTimestamp;
3074
4456
  if (noCurrentLayout) {
@@ -3106,33 +4488,19 @@ function Router(options) {
3106
4488
  // Extend the connectedCallback to add router-specific functionality
3107
4489
  const elementConnectedCallback = constructor.prototype.connectedCallback;
3108
4490
  constructor.prototype.connectedCallback = function () {
4491
+ // Store the Context instance for @context decorated methods to access
4492
+ this[CONTEXT_HANDLER] = navigationContext;
3109
4493
  // Call the element's connectedCallback first
3110
4494
  elementConnectedCallback?.call(this);
3111
- // Setup context request handler for nested elements
3112
- const contextRequestHandler = (event) => {
3113
- // Only respond if this element has context
3114
- if (this[ROUTER_CONTEXT] !== undefined) {
3115
- event.detail.context = this[ROUTER_CONTEXT];
3116
- event.stopPropagation(); // Stop bubbling once context is provided
3117
- }
3118
- };
3119
- this.addEventListener('@context/request', contextRequestHandler);
3120
- // Store handler for cleanup
3121
- this[CONTEXT_REQUEST_HANDLER] = contextRequestHandler;
3122
4495
  };
3123
4496
  // Extend the disconnectedCallback to clean up router-specific stuff
3124
4497
  const elementDisconnectedCallback = constructor.prototype.disconnectedCallback;
3125
4498
  constructor.prototype.disconnectedCallback = function () {
3126
4499
  // Call element's disconnectedCallback first
3127
4500
  elementDisconnectedCallback?.call(this);
3128
- // Clean up context request handler
3129
- const handler = this[CONTEXT_REQUEST_HANDLER];
3130
- if (handler) {
3131
- this.removeEventListener('@context/request', handler);
3132
- delete this[CONTEXT_REQUEST_HANDLER];
3133
- }
3134
- // Clean up context reference
4501
+ // Clean up context references
3135
4502
  delete this[ROUTER_CONTEXT];
4503
+ delete this[CONTEXT_HANDLER];
3136
4504
  };
3137
4505
  // Define the custom element
3138
4506
  customElements.define(pageOptions.tag, constructor);
@@ -3191,8 +4559,8 @@ function Router(options) {
3191
4559
  * initialize();
3192
4560
  */
3193
4561
  function initialize() {
3194
- const targetExists = !!document.querySelector(options.target);
3195
- if (!targetExists) {
4562
+ const target = document.querySelector(options.target);
4563
+ if (!target) {
3196
4564
  throw new Error(`Target element not found: ${options.target}`);
3197
4565
  }
3198
4566
  const needsSorting = !is_sorted;
@@ -3216,6 +4584,10 @@ function Router(options) {
3216
4584
  : placard;
3217
4585
  });
3218
4586
  }
4587
+ function emitContextUpdate(target, currentPath, routeParams) {
4588
+ // Update the navigation context and notify all registered elements
4589
+ navigationContext.update(context, placards, currentPath, routeParams);
4590
+ }
3219
4591
  function updateLayout(layoutElement, currentPath, routeParams) {
3220
4592
  // Check if layout implements the update method
3221
4593
  if (typeof layoutElement.update === 'function') {
@@ -3298,6 +4670,7 @@ function Router(options) {
3298
4670
  }
3299
4671
  const newPageElement = document.createElement(route.tag);
3300
4672
  newPageElement[ROUTER_CONTEXT] = context;
4673
+ newPageElement[CONTEXT_HANDLER] = navigationContext;
3301
4674
  const routeParams = params;
3302
4675
  Object.keys(routeParams).forEach(key => newPageElement.setAttribute(key, routeParams[key]));
3303
4676
  return { result: RouteResult.SUCCESS, element: newPageElement, transition: route.transition, layout: route.layout, routeParams };
@@ -3388,7 +4761,7 @@ function Router(options) {
3388
4761
  // Collect fresh placards before navigation
3389
4762
  collectPlacards();
3390
4763
  window.scrollTo(0, 0);
3391
- const isHomePath = (path.trim() === '' || path === '/') && !!home;
4764
+ const isHomePath = (path?.trim() === '' || path === '/') && !!home;
3392
4765
  if (isHomePath) {
3393
4766
  const homeRoute = routes.find(r => r.route.match('/'));
3394
4767
  const guardsAllowed = checkGuards(homeRoute?.guards, {}, target);
@@ -3402,11 +4775,15 @@ function Router(options) {
3402
4775
  const hasLayout = layoutElement !== null || getCurrentLayoutElement(target) !== null;
3403
4776
  if (hasLayout) {
3404
4777
  await renderWithLayout(target, element, finalTransition, layoutElement, needsNewLayout, path, {});
4778
+ emitContextUpdate(target, path, {});
3405
4779
  return;
3406
4780
  }
3407
4781
  await renderDirect(target, element, finalTransition);
4782
+ emitContextUpdate(target, path, {});
3408
4783
  return;
3409
4784
  }
4785
+ if (!path)
4786
+ return;
3410
4787
  const routeResult = resolveRoute(path, target);
3411
4788
  const isGuardsFailed = routeResult.result === RouteResult.GUARDS_FAILED;
3412
4789
  if (isGuardsFailed) {
@@ -3421,9 +4798,11 @@ function Router(options) {
3421
4798
  const hasLayout = layoutElement !== null || getCurrentLayoutElement(target) !== null;
3422
4799
  if (hasLayout) {
3423
4800
  await renderWithLayout(target, element, finalTransition, layoutElement, needsNewLayout, path, routeParams);
4801
+ emitContextUpdate(target, path, routeParams);
3424
4802
  return;
3425
4803
  }
3426
4804
  await renderDirect(target, element, finalTransition);
4805
+ emitContextUpdate(target, path, routeParams);
3427
4806
  return;
3428
4807
  }
3429
4808
  const { element, transition, layout } = create404Element();
@@ -3433,9 +4812,11 @@ function Router(options) {
3433
4812
  const hasLayout = layoutElement !== null || getCurrentLayoutElement(target) !== null;
3434
4813
  if (hasLayout) {
3435
4814
  await renderWithLayout(target, element, finalTransition, layoutElement, needsNewLayout, path, {});
4815
+ emitContextUpdate(target, path, {});
3436
4816
  return;
3437
4817
  }
3438
4818
  await renderDirect(target, element, finalTransition);
4819
+ emitContextUpdate(target, path, {});
3439
4820
  }
3440
4821
  async function performTransition$1(container, oldElement, newElement, transition) {
3441
4822
  return performTransition(container, oldElement, newElement, transition);
@@ -3448,5 +4829,555 @@ function Router(options) {
3448
4829
  };
3449
4830
  }
3450
4831
 
3451
- export { IS_CONTROLLER_INSTANCE, Router, SimpleArray, adopted, applyElementFunctionality, context, controller, dispatch, dispose, element, getSymbol, layout, moved, observe, on, part, property, query, queryAll, ready, request, respond, useNativeElementControllers, watch };
4832
+ // @on decorator removed in v3.0.0 - use template event syntax instead: @click=${handler}
4833
+ /**
4834
+ * Decorator that automatically dispatches a custom event after a method is called.
4835
+ * The return value of the method becomes the event detail.
4836
+ *
4837
+ * @param eventName The name of the event to dispatch
4838
+ * @param options Optional configuration extending EventInit
4839
+ */
4840
+ function dispatch(eventName, options) {
4841
+ return function (originalMethod, _context) {
4842
+ return function (...args) {
4843
+ // Create timing wrappers for dispatch (per-instance)
4844
+ if (!this[DISPATCH_TIMERS]) {
4845
+ this[DISPATCH_TIMERS] = new Map();
4846
+ }
4847
+ const timerKey = `${eventName}_${_context.name}`;
4848
+ if (!this[DISPATCH_TIMERS].has(timerKey)) {
4849
+ this[DISPATCH_TIMERS].set(timerKey, {
4850
+ debounceTimeout: null,
4851
+ throttleLastCall: 0,
4852
+ throttleTimeout: null
4853
+ });
4854
+ }
4855
+ const timers = this[DISPATCH_TIMERS].get(timerKey);
4856
+ // Call the original method with preserved this context
4857
+ const result = originalMethod.apply(this, args);
4858
+ // Helper to dispatch the event
4859
+ const doDispatch = (detail) => {
4860
+ // Skip dispatch if result is undefined and dispatchOnUndefined is false
4861
+ if (detail === undefined && options?.dispatchOnUndefined === false) {
4862
+ return;
4863
+ }
4864
+ // Create event with spread operator for options
4865
+ const event = new CustomEvent(eventName, {
4866
+ bubbles: true, // Default to true for component events
4867
+ composed: true, // Allow crossing shadow DOM boundaries
4868
+ ...options, // Spread all EventInit options
4869
+ detail
4870
+ });
4871
+ this.dispatchEvent(event);
4872
+ };
4873
+ // Helper to handle timed dispatch
4874
+ const timedDispatch = (detail) => {
4875
+ if (options?.debounce) {
4876
+ clearTimeout(timers.debounceTimeout);
4877
+ timers.debounceTimeout = setTimeout(() => doDispatch(detail), options.debounce);
4878
+ }
4879
+ else if (options?.throttle) {
4880
+ const now = Date.now();
4881
+ const remaining = options.throttle - (now - timers.throttleLastCall);
4882
+ if (remaining <= 0) {
4883
+ clearTimeout(timers.throttleTimeout);
4884
+ timers.throttleLastCall = now;
4885
+ doDispatch(detail);
4886
+ }
4887
+ else if (!timers.throttleTimeout) {
4888
+ timers.throttleTimeout = setTimeout(() => {
4889
+ timers.throttleLastCall = Date.now();
4890
+ timers.throttleTimeout = null;
4891
+ doDispatch(detail);
4892
+ }, remaining);
4893
+ }
4894
+ }
4895
+ else {
4896
+ doDispatch(detail);
4897
+ }
4898
+ };
4899
+ // Handle async methods
4900
+ if (result instanceof Promise) {
4901
+ return result.then((resolvedResult) => {
4902
+ timedDispatch(resolvedResult);
4903
+ return resolvedResult;
4904
+ });
4905
+ }
4906
+ // Sync method
4907
+ timedDispatch(result);
4908
+ return result;
4909
+ };
4910
+ };
4911
+ }
4912
+
4913
+ /**
4914
+ * Custom element readiness utilities
4915
+ * Handles waiting for custom elements to be defined with timeout warnings
4916
+ */
4917
+ /**
4918
+ * Global flag to disable custom element readiness timeout warnings
4919
+ * Set this to true in environments where slow element registration is expected
4920
+ */
4921
+ let DISABLE_ELEMENT_READY_WARNINGS = false;
4922
+ /**
4923
+ * Set whether to disable custom element readiness timeout warnings
4924
+ */
4925
+ function setDisableElementReadyWarnings(value) {
4926
+ DISABLE_ELEMENT_READY_WARNINGS = value;
4927
+ }
4928
+ /**
4929
+ * Default timeout for custom element registration warning (500ms)
4930
+ */
4931
+ const DEFAULT_WARNING_TIMEOUT = 500;
4932
+ /**
4933
+ * Wait for a custom element to be defined
4934
+ * Logs a warning if it takes longer than the warning timeout
4935
+ *
4936
+ * @param tagName - The custom element tag name
4937
+ * @param warningTimeout - Time in ms before warning (default 500ms)
4938
+ * @returns Promise that resolves when element is defined
4939
+ */
4940
+ async function waitForElementDefined(tagName, warningTimeout = DEFAULT_WARNING_TIMEOUT) {
4941
+ // If already defined, return immediately
4942
+ if (customElements.get(tagName)) {
4943
+ return;
4944
+ }
4945
+ // Set up warning timer if not disabled
4946
+ let warningTimer = null;
4947
+ if (!DISABLE_ELEMENT_READY_WARNINGS) {
4948
+ warningTimer = setTimeout(() => {
4949
+ console.warn(`Custom element <${tagName}> is taking longer than ${warningTimeout}ms to register. ` +
4950
+ `This may indicate a missing import or circular dependency. ` +
4951
+ `Set DISABLE_ELEMENT_READY_WARNINGS=true to disable this warning.`);
4952
+ }, warningTimeout);
4953
+ }
4954
+ try {
4955
+ // Wait for element to be defined
4956
+ await customElements.whenDefined(tagName);
4957
+ }
4958
+ finally {
4959
+ // Clear warning timer
4960
+ if (warningTimer) {
4961
+ clearTimeout(warningTimer);
4962
+ }
4963
+ }
4964
+ }
4965
+ /**
4966
+ * Wait for a custom element to be defined and ready
4967
+ * First waits for the element to be registered, then waits for its ready promise
4968
+ *
4969
+ * @param element - The custom element instance
4970
+ * @param warningTimeout - Time in ms before warning about registration (default 500ms)
4971
+ * @returns Promise that resolves when element is defined and ready
4972
+ */
4973
+ async function waitForElementReady(element, warningTimeout = DEFAULT_WARNING_TIMEOUT) {
4974
+ const tagName = element.tagName.toLowerCase();
4975
+ // Wait for element to be defined
4976
+ await waitForElementDefined(tagName, warningTimeout);
4977
+ // Wait for element's ready promise if it exists
4978
+ if ('ready' in element && typeof element.ready?.then === 'function') {
4979
+ await element.ready;
4980
+ }
4981
+ }
4982
+ /**
4983
+ * Process all custom elements in a node tree and wait for them to be ready
4984
+ * This is useful after inserting a template with custom elements
4985
+ *
4986
+ * @param node - The root node to scan for custom elements
4987
+ * @param warningTimeout - Time in ms before warning about registration (default 500ms)
4988
+ * @returns Promise that resolves when all custom elements are ready
4989
+ */
4990
+ async function waitForAllCustomElements(node, warningTimeout = DEFAULT_WARNING_TIMEOUT) {
4991
+ const customElements = [];
4992
+ // Find all custom elements (tag names with hyphens)
4993
+ if (node instanceof Element) {
4994
+ if (node.tagName.includes('-')) {
4995
+ customElements.push(node);
4996
+ }
4997
+ // Also check in shadow DOM
4998
+ if (node.shadowRoot) {
4999
+ const shadowCustomElements = node.shadowRoot.querySelectorAll('*');
5000
+ shadowCustomElements.forEach(el => {
5001
+ if (el.tagName.includes('-')) {
5002
+ customElements.push(el);
5003
+ }
5004
+ });
5005
+ }
5006
+ }
5007
+ // If it's a DocumentFragment, check all children
5008
+ if (node instanceof DocumentFragment) {
5009
+ const elements = node.querySelectorAll('*');
5010
+ elements.forEach(el => {
5011
+ if (el.tagName.includes('-')) {
5012
+ customElements.push(el);
5013
+ }
5014
+ });
5015
+ }
5016
+ // Wait for all custom elements to be ready
5017
+ await Promise.all(customElements.map(el => waitForElementReady(el, warningTimeout)));
5018
+ }
5019
+
5020
+ /**
5021
+ * Render debugging utilities for Snice v3.0.0
5022
+ * For testing and debugging only - not recommended for production use
5023
+ */
5024
+ /**
5025
+ * Track renders of an element using an async generator
5026
+ * Each call to tracker.next() waits for the next render to complete
5027
+ *
5028
+ * @example
5029
+ * ```typescript
5030
+ * const tracker = trackRenders(element);
5031
+ *
5032
+ * element.someProp = 'new value';
5033
+ * await tracker.next(); // Waits for render
5034
+ * // DOM is now updated
5035
+ *
5036
+ * element.someProp = 'another value';
5037
+ * await tracker.next(); // Waits for next render
5038
+ * // DOM is updated again
5039
+ * ```
5040
+ *
5041
+ * WARNING: For testing/debugging only!
5042
+ * - Do not use in production code
5043
+ * - The generator yields indefinitely - use it only in controlled test environments
5044
+ * - Each yield waits for the next render event
5045
+ */
5046
+ async function* trackRenders(element) {
5047
+ while (true) {
5048
+ await new Promise(resolve => {
5049
+ if (!element[RENDER_CALLBACKS]) {
5050
+ element[RENDER_CALLBACKS] = [];
5051
+ }
5052
+ element[RENDER_CALLBACKS].push(resolve);
5053
+ });
5054
+ yield;
5055
+ }
5056
+ }
5057
+
5058
+ /**
5059
+ * Method decorators for common patterns
5060
+ * @debounce, @throttle, @once, @memoize
5061
+ */
5062
+ const DEBOUNCE_TIMERS = getSymbol('debounce-timers');
5063
+ const THROTTLE_TIMERS = getSymbol('throttle-timers');
5064
+ const ONCE_CALLED = getSymbol('once-called');
5065
+ const MEMOIZE_CACHE = getSymbol('memoize-cache');
5066
+ /**
5067
+ * @debounce decorator - delays function execution until after wait time has elapsed
5068
+ * since the last invocation
5069
+ *
5070
+ * @param wait - Time to wait in milliseconds (default: 300)
5071
+ * @param options - Debounce options
5072
+ * @param options.leading - Invoke on the leading edge (default: false)
5073
+ * @param options.trailing - Invoke on the trailing edge (default: true)
5074
+ * @param options.maxWait - Maximum time to wait before invoking (default: undefined)
5075
+ *
5076
+ * @example
5077
+ * ```typescript
5078
+ * @element('search-input')
5079
+ * class SearchInput extends HTMLElement {
5080
+ * @debounce(500)
5081
+ * handleSearch(query: string) {
5082
+ * // Only called 500ms after last keystroke
5083
+ * fetch(`/api/search?q=${query}`);
5084
+ * }
5085
+ * }
5086
+ * ```
5087
+ */
5088
+ function debounce(wait = 300, options = {}) {
5089
+ const { leading = false, trailing = true, maxWait } = options;
5090
+ return function (originalMethod, context) {
5091
+ const methodName = context.name;
5092
+ return function (...args) {
5093
+ if (!this[DEBOUNCE_TIMERS]) {
5094
+ this[DEBOUNCE_TIMERS] = {};
5095
+ }
5096
+ const timers = this[DEBOUNCE_TIMERS];
5097
+ const timerKey = methodName;
5098
+ // Clear existing timer
5099
+ if (timers[timerKey]) {
5100
+ clearTimeout(timers[timerKey].timeout);
5101
+ }
5102
+ // Track when debounce started for maxWait
5103
+ const now = Date.now();
5104
+ const isFirstCall = !timers[timerKey];
5105
+ const startTime = isFirstCall ? now : timers[timerKey].startTime;
5106
+ // Check if maxWait exceeded
5107
+ const shouldInvokeFromMaxWait = maxWait !== undefined && now - startTime >= maxWait;
5108
+ // Leading edge invocation
5109
+ if (leading && isFirstCall) {
5110
+ const result = originalMethod.apply(this, args);
5111
+ timers[timerKey] = {
5112
+ timeout: null,
5113
+ startTime,
5114
+ lastArgs: args,
5115
+ };
5116
+ return result;
5117
+ }
5118
+ // Set new timer for trailing edge
5119
+ const timeout = setTimeout(() => {
5120
+ if (trailing || shouldInvokeFromMaxWait) {
5121
+ originalMethod.apply(this, timers[timerKey].lastArgs);
5122
+ }
5123
+ delete timers[timerKey];
5124
+ }, shouldInvokeFromMaxWait ? 0 : wait);
5125
+ timers[timerKey] = {
5126
+ timeout,
5127
+ startTime,
5128
+ lastArgs: args,
5129
+ };
5130
+ };
5131
+ };
5132
+ }
5133
+ /**
5134
+ * @throttle decorator - ensures function is called at most once per specified time period
5135
+ *
5136
+ * @param wait - Time to wait in milliseconds (default: 300)
5137
+ * @param options - Throttle options
5138
+ * @param options.leading - Invoke on the leading edge (default: true)
5139
+ * @param options.trailing - Invoke on the trailing edge (default: true)
5140
+ *
5141
+ * @example
5142
+ * ```typescript
5143
+ * @element('scroll-tracker')
5144
+ * class ScrollTracker extends HTMLElement {
5145
+ * @throttle(100)
5146
+ * handleScroll(e: Event) {
5147
+ * // Called at most once every 100ms
5148
+ * this.updateScrollPosition();
5149
+ * }
5150
+ * }
5151
+ * ```
5152
+ */
5153
+ function throttle(wait = 300, options = {}) {
5154
+ const { leading = true, trailing = true } = options;
5155
+ return function (originalMethod, context) {
5156
+ const methodName = context.name;
5157
+ return function (...args) {
5158
+ if (!this[THROTTLE_TIMERS]) {
5159
+ this[THROTTLE_TIMERS] = {};
5160
+ }
5161
+ const timers = this[THROTTLE_TIMERS];
5162
+ const timerKey = methodName;
5163
+ const now = Date.now();
5164
+ if (!timers[timerKey]) {
5165
+ // First call
5166
+ if (leading) {
5167
+ originalMethod.apply(this, args);
5168
+ }
5169
+ timers[timerKey] = {
5170
+ lastInvoke: now,
5171
+ timeout: null,
5172
+ lastArgs: args,
5173
+ };
5174
+ if (trailing && !leading) {
5175
+ // If no leading edge, set up trailing
5176
+ timers[timerKey].timeout = setTimeout(() => {
5177
+ originalMethod.apply(this, timers[timerKey].lastArgs);
5178
+ delete timers[timerKey];
5179
+ }, wait);
5180
+ }
5181
+ }
5182
+ else {
5183
+ // Subsequent calls
5184
+ const timeSinceLastInvoke = now - timers[timerKey].lastInvoke;
5185
+ // Update last args
5186
+ timers[timerKey].lastArgs = args;
5187
+ // Clear any pending trailing call
5188
+ if (timers[timerKey].timeout) {
5189
+ clearTimeout(timers[timerKey].timeout);
5190
+ }
5191
+ if (timeSinceLastInvoke >= wait) {
5192
+ // Enough time has passed, invoke immediately
5193
+ originalMethod.apply(this, args);
5194
+ timers[timerKey].lastInvoke = now;
5195
+ }
5196
+ else if (trailing) {
5197
+ // Set up trailing call
5198
+ const remaining = wait - timeSinceLastInvoke;
5199
+ timers[timerKey].timeout = setTimeout(() => {
5200
+ originalMethod.apply(this, timers[timerKey].lastArgs);
5201
+ timers[timerKey].lastInvoke = Date.now();
5202
+ timers[timerKey].timeout = null;
5203
+ }, remaining);
5204
+ }
5205
+ }
5206
+ };
5207
+ };
5208
+ }
5209
+ /**
5210
+ * @once decorator - ensures function is only called once
5211
+ * Subsequent calls return the result of the first call
5212
+ *
5213
+ * @param perInstance - If true, function can be called once per instance (default: true)
5214
+ * If false, function can only be called once globally across all instances
5215
+ *
5216
+ * @example
5217
+ * ```typescript
5218
+ * @element('data-loader')
5219
+ * class DataLoader extends HTMLElement {
5220
+ * @once()
5221
+ * async loadData() {
5222
+ * // Only loads data once, even if called multiple times
5223
+ * const data = await fetch('/api/data');
5224
+ * return data.json();
5225
+ * }
5226
+ * }
5227
+ * ```
5228
+ */
5229
+ function once(perInstance = true) {
5230
+ let globalCalled = false;
5231
+ let globalResult;
5232
+ return function (originalMethod, context) {
5233
+ const methodName = context.name;
5234
+ return function (...args) {
5235
+ if (perInstance) {
5236
+ // Per-instance tracking
5237
+ if (!this[ONCE_CALLED]) {
5238
+ this[ONCE_CALLED] = {};
5239
+ }
5240
+ if (!this[ONCE_CALLED][methodName]) {
5241
+ this[ONCE_CALLED][methodName] = {
5242
+ called: true,
5243
+ result: originalMethod.apply(this, args),
5244
+ };
5245
+ }
5246
+ return this[ONCE_CALLED][methodName].result;
5247
+ }
5248
+ else {
5249
+ // Global tracking
5250
+ if (!globalCalled) {
5251
+ globalCalled = true;
5252
+ globalResult = originalMethod.apply(this, args);
5253
+ }
5254
+ return globalResult;
5255
+ }
5256
+ };
5257
+ };
5258
+ }
5259
+ /**
5260
+ * @memoize decorator - caches function results based on arguments
5261
+ * Uses JSON.stringify for argument comparison by default
5262
+ *
5263
+ * @param options - Memoization options
5264
+ * @param options.keyGenerator - Custom function to generate cache key from arguments
5265
+ * @param options.maxSize - Maximum cache size (default: 100)
5266
+ * @param options.ttl - Time to live in milliseconds (default: undefined - no expiration)
5267
+ *
5268
+ * @example
5269
+ * ```typescript
5270
+ * @element('calculator')
5271
+ * class Calculator extends HTMLElement {
5272
+ * @memoize({ maxSize: 50 })
5273
+ * fibonacci(n: number): number {
5274
+ * // Results are cached, subsequent calls with same n are instant
5275
+ * if (n <= 1) return n;
5276
+ * return this.fibonacci(n - 1) + this.fibonacci(n - 2);
5277
+ * }
5278
+ * }
5279
+ * ```
5280
+ */
5281
+ function memoize(options = {}) {
5282
+ const { keyGenerator = (...args) => JSON.stringify(args), maxSize = 100, ttl } = options;
5283
+ return function (originalMethod, context) {
5284
+ const methodName = context.name;
5285
+ return function (...args) {
5286
+ if (!this[MEMOIZE_CACHE]) {
5287
+ this[MEMOIZE_CACHE] = {};
5288
+ }
5289
+ if (!this[MEMOIZE_CACHE][methodName]) {
5290
+ this[MEMOIZE_CACHE][methodName] = new Map();
5291
+ }
5292
+ const cache = this[MEMOIZE_CACHE][methodName];
5293
+ const key = keyGenerator(...args);
5294
+ // Check if cached
5295
+ if (cache.has(key)) {
5296
+ const cached = cache.get(key);
5297
+ // Check TTL
5298
+ if (ttl !== undefined) {
5299
+ const age = Date.now() - cached.timestamp;
5300
+ if (age > ttl) {
5301
+ cache.delete(key);
5302
+ }
5303
+ else {
5304
+ return cached.value;
5305
+ }
5306
+ }
5307
+ else {
5308
+ return cached.value;
5309
+ }
5310
+ }
5311
+ // Compute result
5312
+ const result = originalMethod.apply(this, args);
5313
+ // Store in cache
5314
+ cache.set(key, {
5315
+ value: result,
5316
+ timestamp: Date.now(),
5317
+ });
5318
+ // Enforce max size (LRU - delete oldest)
5319
+ if (cache.size > maxSize) {
5320
+ const firstKey = cache.keys().next().value;
5321
+ cache.delete(firstKey);
5322
+ }
5323
+ return result;
5324
+ };
5325
+ };
5326
+ }
5327
+ /**
5328
+ * Clear all debounce timers for an instance
5329
+ * Useful in cleanup/disconnectedCallback
5330
+ */
5331
+ function clearDebounceTimers(instance) {
5332
+ if (instance[DEBOUNCE_TIMERS]) {
5333
+ for (const timerKey in instance[DEBOUNCE_TIMERS]) {
5334
+ if (instance[DEBOUNCE_TIMERS][timerKey]?.timeout) {
5335
+ clearTimeout(instance[DEBOUNCE_TIMERS][timerKey].timeout);
5336
+ }
5337
+ }
5338
+ instance[DEBOUNCE_TIMERS] = {};
5339
+ }
5340
+ }
5341
+ /**
5342
+ * Clear all throttle timers for an instance
5343
+ * Useful in cleanup/disconnectedCallback
5344
+ */
5345
+ function clearThrottleTimers(instance) {
5346
+ if (instance[THROTTLE_TIMERS]) {
5347
+ for (const timerKey in instance[THROTTLE_TIMERS]) {
5348
+ if (instance[THROTTLE_TIMERS][timerKey]?.timeout) {
5349
+ clearTimeout(instance[THROTTLE_TIMERS][timerKey].timeout);
5350
+ }
5351
+ }
5352
+ instance[THROTTLE_TIMERS] = {};
5353
+ }
5354
+ }
5355
+ /**
5356
+ * Clear memoize cache for an instance
5357
+ */
5358
+ function clearMemoizeCache(instance, methodName) {
5359
+ if (instance[MEMOIZE_CACHE]) {
5360
+ if (methodName) {
5361
+ delete instance[MEMOIZE_CACHE][methodName];
5362
+ }
5363
+ else {
5364
+ instance[MEMOIZE_CACHE] = {};
5365
+ }
5366
+ }
5367
+ }
5368
+ /**
5369
+ * Reset once-called state for an instance
5370
+ */
5371
+ function resetOnce(instance, methodName) {
5372
+ if (instance[ONCE_CALLED]) {
5373
+ if (methodName) {
5374
+ delete instance[ONCE_CALLED][methodName];
5375
+ }
5376
+ else {
5377
+ instance[ONCE_CALLED] = {};
5378
+ }
5379
+ }
5380
+ }
5381
+
5382
+ export { Context, IS_CONTROLLER_INSTANCE, Router, SimpleArray, adopted, applyElementFunctionality, clearDebounceTimers, clearMemoizeCache, clearThrottleTimers, context$1 as context, context as contextProperty, controller, css, debounce, dispatch, dispose, element, getSymbol, html, layout, memoize, moved, nothing, observe, on, once, property, query, queryAll, ready, render, request, resetOnce, respond, setDisableElementReadyWarnings, styles, throttle, trackRenders, unsafeHTML, useNativeElementControllers, waitForAllCustomElements, waitForElementDefined, waitForElementReady, watch };
3452
5383
  //# sourceMappingURL=index.esm.js.map