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