snice 2.5.4 → 3.1.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 (323) hide show
  1. package/README.md +501 -882
  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/breadcrumbs/snice-breadcrumbs.d.ts +5 -12
  20. package/dist/components/breadcrumbs/snice-breadcrumbs.js +88 -89
  21. package/dist/components/breadcrumbs/snice-breadcrumbs.js.map +1 -1
  22. package/dist/components/button/snice-button.d.ts +3 -7
  23. package/dist/components/button/snice-button.js +37 -58
  24. package/dist/components/button/snice-button.js.map +1 -1
  25. package/dist/components/card/snice-card.d.ts +5 -8
  26. package/dist/components/card/snice-card.js +71 -56
  27. package/dist/components/card/snice-card.js.map +1 -1
  28. package/dist/components/checkbox/snice-checkbox.d.ts +4 -13
  29. package/dist/components/checkbox/snice-checkbox.js +66 -137
  30. package/dist/components/checkbox/snice-checkbox.js.map +1 -1
  31. package/dist/components/chip/snice-chip.d.ts +5 -11
  32. package/dist/components/chip/snice-chip.js +44 -47
  33. package/dist/components/chip/snice-chip.js.map +1 -1
  34. package/dist/components/date-picker/snice-date-picker.d.ts +11 -11
  35. package/dist/components/date-picker/snice-date-picker.js +134 -133
  36. package/dist/components/date-picker/snice-date-picker.js.map +1 -1
  37. package/dist/components/divider/snice-divider.d.ts +2 -4
  38. package/dist/components/divider/snice-divider.js +14 -22
  39. package/dist/components/divider/snice-divider.js.map +1 -1
  40. package/dist/components/drawer/snice-drawer.d.ts +4 -4
  41. package/dist/components/drawer/snice-drawer.js +25 -19
  42. package/dist/components/drawer/snice-drawer.js.map +1 -1
  43. package/dist/components/input/snice-input.d.ts +8 -6
  44. package/dist/components/input/snice-input.js +122 -105
  45. package/dist/components/input/snice-input.js.map +1 -1
  46. package/dist/components/layout/snice-layout-blog.d.ts +4 -4
  47. package/dist/components/layout/snice-layout-blog.js +21 -19
  48. package/dist/components/layout/snice-layout-blog.js.map +1 -1
  49. package/dist/components/layout/snice-layout-card.d.ts +2 -2
  50. package/dist/components/layout/snice-layout-card.js +16 -9
  51. package/dist/components/layout/snice-layout-card.js.map +1 -1
  52. package/dist/components/layout/snice-layout-centered.d.ts +2 -2
  53. package/dist/components/layout/snice-layout-centered.js +14 -7
  54. package/dist/components/layout/snice-layout-centered.js.map +1 -1
  55. package/dist/components/layout/snice-layout-dashboard.d.ts +5 -5
  56. package/dist/components/layout/snice-layout-dashboard.js +38 -30
  57. package/dist/components/layout/snice-layout-dashboard.js.map +1 -1
  58. package/dist/components/layout/snice-layout-fullscreen.d.ts +2 -2
  59. package/dist/components/layout/snice-layout-fullscreen.js +17 -10
  60. package/dist/components/layout/snice-layout-fullscreen.js.map +1 -1
  61. package/dist/components/layout/snice-layout-landing.d.ts +4 -4
  62. package/dist/components/layout/snice-layout-landing.js +21 -19
  63. package/dist/components/layout/snice-layout-landing.js.map +1 -1
  64. package/dist/components/layout/snice-layout-minimal.d.ts +2 -2
  65. package/dist/components/layout/snice-layout-minimal.js +17 -6
  66. package/dist/components/layout/snice-layout-minimal.js.map +1 -1
  67. package/dist/components/layout/snice-layout-sidebar.d.ts +5 -4
  68. package/dist/components/layout/snice-layout-sidebar.js +42 -20
  69. package/dist/components/layout/snice-layout-sidebar.js.map +1 -1
  70. package/dist/components/layout/snice-layout-split.d.ts +2 -2
  71. package/dist/components/layout/snice-layout-split.js +14 -7
  72. package/dist/components/layout/snice-layout-split.js.map +1 -1
  73. package/dist/components/layout/snice-layout.d.ts +4 -4
  74. package/dist/components/layout/snice-layout.js +16 -10
  75. package/dist/components/layout/snice-layout.js.map +1 -1
  76. package/dist/components/login/snice-login.d.ts +6 -11
  77. package/dist/components/login/snice-login.js +97 -71
  78. package/dist/components/login/snice-login.js.map +1 -1
  79. package/dist/components/modal/snice-modal.d.ts +5 -9
  80. package/dist/components/modal/snice-modal.js +47 -78
  81. package/dist/components/modal/snice-modal.js.map +1 -1
  82. package/dist/components/nav/snice-nav.d.ts +13 -7
  83. package/dist/components/nav/snice-nav.js +191 -100
  84. package/dist/components/nav/snice-nav.js.map +1 -1
  85. package/dist/components/nav/snice-nav.types.d.ts +3 -3
  86. package/dist/components/pagination/snice-pagination.d.ts +6 -7
  87. package/dist/components/pagination/snice-pagination.js +94 -81
  88. package/dist/components/pagination/snice-pagination.js.map +1 -1
  89. package/dist/components/progress/snice-progress.d.ts +2 -7
  90. package/dist/components/progress/snice-progress.js +41 -98
  91. package/dist/components/progress/snice-progress.js.map +1 -1
  92. package/dist/components/radio/snice-radio.d.ts +4 -4
  93. package/dist/components/radio/snice-radio.js +52 -44
  94. package/dist/components/radio/snice-radio.js.map +1 -1
  95. package/dist/components/select/snice-option.d.ts +2 -1
  96. package/dist/components/select/snice-option.js +12 -5
  97. package/dist/components/select/snice-option.js.map +1 -1
  98. package/dist/components/select/snice-select.d.ts +9 -21
  99. package/dist/components/select/snice-select.js +98 -170
  100. package/dist/components/select/snice-select.js.map +1 -1
  101. package/dist/components/skeleton/snice-skeleton.d.ts +2 -6
  102. package/dist/components/skeleton/snice-skeleton.js +18 -49
  103. package/dist/components/skeleton/snice-skeleton.js.map +1 -1
  104. package/dist/components/snice-cell-BLFVdxPp.js +4 -0
  105. package/dist/components/snice-cell-BLFVdxPp.js.map +1 -0
  106. package/dist/components/switch/snice-switch.d.ts +2 -2
  107. package/dist/components/switch/snice-switch.js +38 -26
  108. package/dist/components/switch/snice-switch.js.map +1 -1
  109. package/dist/components/table/snice-cell-actions.d.ts +24 -0
  110. package/dist/components/table/snice-cell-actions.js +149 -0
  111. package/dist/components/table/snice-cell-actions.js.map +1 -0
  112. package/dist/components/table/snice-cell-boolean.d.ts +2 -2
  113. package/dist/components/table/snice-cell-boolean.js +13 -7
  114. package/dist/components/table/snice-cell-boolean.js.map +1 -1
  115. package/dist/components/table/snice-cell-color.d.ts +18 -0
  116. package/dist/components/table/snice-cell-color.js +149 -0
  117. package/dist/components/table/snice-cell-color.js.map +1 -0
  118. package/dist/components/table/snice-cell-currency.d.ts +24 -0
  119. package/dist/components/table/snice-cell-currency.js +235 -0
  120. package/dist/components/table/snice-cell-currency.js.map +1 -0
  121. package/dist/components/table/snice-cell-date.d.ts +2 -2
  122. package/dist/components/table/snice-cell-date.js +14 -8
  123. package/dist/components/table/snice-cell-date.js.map +1 -1
  124. package/dist/components/table/snice-cell-duration.d.ts +2 -2
  125. package/dist/components/table/snice-cell-duration.js +12 -6
  126. package/dist/components/table/snice-cell-duration.js.map +1 -1
  127. package/dist/components/table/snice-cell-email.d.ts +15 -0
  128. package/dist/components/table/snice-cell-email.js +125 -0
  129. package/dist/components/table/snice-cell-email.js.map +1 -0
  130. package/dist/components/table/snice-cell-filesize.d.ts +2 -2
  131. package/dist/components/table/snice-cell-filesize.js +12 -6
  132. package/dist/components/table/snice-cell-filesize.js.map +1 -1
  133. package/dist/components/table/snice-cell-image.d.ts +20 -0
  134. package/dist/components/table/snice-cell-image.js +162 -0
  135. package/dist/components/table/snice-cell-image.js.map +1 -0
  136. package/dist/components/table/snice-cell-json.d.ts +20 -0
  137. package/dist/components/table/snice-cell-json.js +186 -0
  138. package/dist/components/table/snice-cell-json.js.map +1 -0
  139. package/dist/components/table/snice-cell-link.d.ts +17 -0
  140. package/dist/components/table/snice-cell-link.js +142 -0
  141. package/dist/components/table/snice-cell-link.js.map +1 -0
  142. package/dist/components/table/snice-cell-location.d.ts +19 -0
  143. package/dist/components/table/snice-cell-location.js +185 -0
  144. package/dist/components/table/snice-cell-location.js.map +1 -0
  145. package/dist/components/table/snice-cell-number.d.ts +2 -2
  146. package/dist/components/table/snice-cell-number.js +12 -6
  147. package/dist/components/table/snice-cell-number.js.map +1 -1
  148. package/dist/components/table/snice-cell-percentage.d.ts +22 -0
  149. package/dist/components/table/snice-cell-percentage.js +208 -0
  150. package/dist/components/table/snice-cell-percentage.js.map +1 -0
  151. package/dist/components/table/snice-cell-phone.d.ts +18 -0
  152. package/dist/components/table/snice-cell-phone.js +153 -0
  153. package/dist/components/table/snice-cell-phone.js.map +1 -0
  154. package/dist/components/table/snice-cell-progress.d.ts +2 -2
  155. package/dist/components/table/snice-cell-progress.js +12 -6
  156. package/dist/components/table/snice-cell-progress.js.map +1 -1
  157. package/dist/components/table/snice-cell-rating.d.ts +2 -2
  158. package/dist/components/table/snice-cell-rating.js +12 -6
  159. package/dist/components/table/snice-cell-rating.js.map +1 -1
  160. package/dist/components/table/snice-cell-sparkline.d.ts +2 -2
  161. package/dist/components/table/snice-cell-sparkline.js +13 -7
  162. package/dist/components/table/snice-cell-sparkline.js.map +1 -1
  163. package/dist/components/table/snice-cell-status.d.ts +17 -0
  164. package/dist/components/table/snice-cell-status.js +144 -0
  165. package/dist/components/table/snice-cell-status.js.map +1 -0
  166. package/dist/components/table/snice-cell-tag.d.ts +16 -0
  167. package/dist/components/table/snice-cell-tag.js +131 -0
  168. package/dist/components/table/snice-cell-tag.js.map +1 -0
  169. package/dist/components/table/snice-cell-text.d.ts +2 -2
  170. package/dist/components/table/snice-cell-text.js +14 -8
  171. package/dist/components/table/snice-cell-text.js.map +1 -1
  172. package/dist/components/table/snice-cell.d.ts +2 -2
  173. package/dist/components/table/snice-cell.js +12 -6
  174. package/dist/components/table/snice-cell.js.map +1 -1
  175. package/dist/components/table/snice-column.d.ts +1 -1
  176. package/dist/components/table/snice-column.js +6 -3
  177. package/dist/components/table/snice-column.js.map +1 -1
  178. package/dist/components/table/snice-header.d.ts +5 -5
  179. package/dist/components/table/snice-header.js +60 -50
  180. package/dist/components/table/snice-header.js.map +1 -1
  181. package/dist/components/table/snice-progress.d.ts +2 -2
  182. package/dist/components/table/snice-progress.js +18 -11
  183. package/dist/components/table/snice-progress.js.map +1 -1
  184. package/dist/components/table/snice-rating.d.ts +2 -2
  185. package/dist/components/table/snice-rating.js +15 -8
  186. package/dist/components/table/snice-rating.js.map +1 -1
  187. package/dist/components/table/snice-row.d.ts +17 -6
  188. package/dist/components/table/snice-row.js +95 -44
  189. package/dist/components/table/snice-row.js.map +1 -1
  190. package/dist/components/table/snice-table.d.ts +18 -10
  191. package/dist/components/table/snice-table.js +355 -173
  192. package/dist/components/table/snice-table.js.map +1 -1
  193. package/dist/components/table/snice-table.types.d.ts +101 -2
  194. package/dist/components/tabs/snice-tab-panel.d.ts +2 -2
  195. package/dist/components/tabs/snice-tab-panel.js +12 -6
  196. package/dist/components/tabs/snice-tab-panel.js.map +1 -1
  197. package/dist/components/tabs/snice-tab.d.ts +6 -5
  198. package/dist/components/tabs/snice-tab.js +36 -19
  199. package/dist/components/tabs/snice-tab.js.map +1 -1
  200. package/dist/components/tabs/snice-tabs.d.ts +5 -5
  201. package/dist/components/tabs/snice-tabs.js +38 -28
  202. package/dist/components/tabs/snice-tabs.js.map +1 -1
  203. package/dist/components/toast/snice-toast-container.d.ts +7 -7
  204. package/dist/components/toast/snice-toast-container.js +19 -12
  205. package/dist/components/toast/snice-toast-container.js.map +1 -1
  206. package/dist/components/toast/snice-toast.d.ts +3 -15
  207. package/dist/components/toast/snice-toast.js +49 -108
  208. package/dist/components/toast/snice-toast.js.map +1 -1
  209. package/dist/components/tooltip/snice-tooltip.d.ts +2 -2
  210. package/dist/components/tooltip/snice-tooltip.js +14 -7
  211. package/dist/components/tooltip/snice-tooltip.js.map +1 -1
  212. package/dist/context.d.ts +44 -0
  213. package/dist/element-ready.d.ts +40 -0
  214. package/dist/{types/element.d.ts → element.d.ts} +2 -8
  215. package/dist/{types/events.d.ts → events.d.ts} +0 -4
  216. package/dist/index.cjs +2589 -605
  217. package/dist/index.cjs.map +1 -1
  218. package/dist/index.d.ts +21 -0
  219. package/dist/index.esm.js +2568 -604
  220. package/dist/index.esm.js.map +1 -1
  221. package/dist/index.iife.js +2589 -605
  222. package/dist/index.iife.js.map +1 -1
  223. package/dist/method-decorators.d.ts +121 -0
  224. package/dist/on.d.ts +59 -0
  225. package/dist/parts.d.ts +159 -0
  226. package/dist/render-debug.d.ts +27 -0
  227. package/dist/render-tracker.d.ts +14 -0
  228. package/dist/render.d.ts +96 -0
  229. package/dist/symbols.cjs +163 -0
  230. package/dist/symbols.cjs.map +1 -1
  231. package/dist/{types/symbols.d.ts → symbols.d.ts} +22 -0
  232. package/dist/symbols.esm.js +27 -3
  233. package/dist/symbols.esm.js.map +1 -1
  234. package/dist/template.d.ts +100 -0
  235. package/dist/transitions.cjs +219 -0
  236. package/dist/transitions.esm.js +2 -2
  237. package/dist/types/context.d.ts +48 -0
  238. package/dist/types/element-options.d.ts +26 -0
  239. package/dist/types/index.d.ts +25 -9
  240. package/dist/types/nav-context.d.ts +19 -0
  241. package/dist/types/{types/on-options.d.ts → on-options.d.ts} +2 -0
  242. package/dist/types/{types/placard.d.ts → placard.d.ts} +0 -1
  243. package/docs/ai/README.md +17 -0
  244. package/docs/ai/api.md +175 -0
  245. package/docs/ai/architecture.md +160 -0
  246. package/docs/ai/components/accordion.md +174 -0
  247. package/docs/ai/components/alert.md +77 -0
  248. package/docs/ai/components/avatar.md +61 -0
  249. package/docs/ai/components/badge.md +69 -0
  250. package/docs/ai/components/breadcrumbs.md +74 -0
  251. package/docs/ai/components/button.md +75 -0
  252. package/docs/ai/components/card.md +61 -0
  253. package/docs/ai/components/checkbox.md +74 -0
  254. package/docs/ai/components/chip.md +73 -0
  255. package/docs/ai/components/date-picker.md +75 -0
  256. package/docs/ai/components/divider.md +66 -0
  257. package/docs/ai/components/drawer.md +80 -0
  258. package/docs/ai/components/input.md +111 -0
  259. package/docs/ai/components/login.md +109 -0
  260. package/docs/ai/components/modal.md +67 -0
  261. package/docs/ai/components/nav.md +76 -0
  262. package/docs/ai/components/pagination.md +55 -0
  263. package/docs/ai/components/progress.md +72 -0
  264. package/docs/ai/components/radio.md +79 -0
  265. package/docs/ai/components/select.md +92 -0
  266. package/docs/ai/components/skeleton.md +57 -0
  267. package/docs/ai/components/switch.md +53 -0
  268. package/docs/ai/components/table.md +227 -0
  269. package/docs/ai/components/tabs.md +83 -0
  270. package/docs/ai/components/toast.md +140 -0
  271. package/docs/ai/components/tooltip.md +146 -0
  272. package/docs/ai/patterns.md +244 -0
  273. package/docs/components/accordion.md +558 -0
  274. package/docs/components/drawer.md +602 -0
  275. package/docs/components/modal.md +558 -0
  276. package/docs/components/nav.md +239 -0
  277. package/docs/components/pagination.md +289 -0
  278. package/docs/components/select.md +599 -0
  279. package/docs/components/switch.md +354 -0
  280. package/docs/components/tabs.md +546 -0
  281. package/docs/components/toast.md +506 -0
  282. package/docs/components/tooltip.md +523 -0
  283. package/docs/controllers.md +744 -0
  284. package/docs/elements.md +855 -0
  285. package/docs/events.md +807 -0
  286. package/docs/migration-v2-to-v3.md +569 -0
  287. package/docs/observe.md +588 -0
  288. package/docs/placards.md +401 -0
  289. package/docs/request-response.md +852 -0
  290. package/docs/routing.md +1186 -0
  291. package/package.json +10 -11
  292. package/dist/components/snice-cell-C9N6yGxQ.js +0 -4
  293. package/dist/components/snice-cell-C9N6yGxQ.js.map +0 -1
  294. package/dist/types/types/index.d.ts +0 -23
  295. /package/dist/{types/controller.d.ts → controller.d.ts} +0 -0
  296. /package/dist/{types/global.d.ts → global.d.ts} +0 -0
  297. /package/dist/{types/observe.d.ts → observe.d.ts} +0 -0
  298. /package/dist/{types/request-response.d.ts → request-response.d.ts} +0 -0
  299. /package/dist/{types/router.d.ts → router.d.ts} +0 -0
  300. /package/dist/{types/testing.d.ts → testing.d.ts} +0 -0
  301. /package/dist/{types/transitions.d.ts → transitions.d.ts} +0 -0
  302. /package/dist/types/{types/adopted-options.d.ts → adopted-options.d.ts} +0 -0
  303. /package/dist/types/{types/app-context.d.ts → app-context.d.ts} +0 -0
  304. /package/dist/types/{types/dispatch-options.d.ts → dispatch-options.d.ts} +0 -0
  305. /package/dist/types/{types/guard.d.ts → guard.d.ts} +0 -0
  306. /package/dist/types/{types/i-controller.d.ts → i-controller.d.ts} +0 -0
  307. /package/dist/types/{types/moved-options.d.ts → moved-options.d.ts} +0 -0
  308. /package/dist/types/{types/observe-options.d.ts → observe-options.d.ts} +0 -0
  309. /package/dist/types/{types/page-options.d.ts → page-options.d.ts} +0 -0
  310. /package/dist/types/{types/part-options.d.ts → part-options.d.ts} +0 -0
  311. /package/dist/types/{types/property-converter.d.ts → property-converter.d.ts} +0 -0
  312. /package/dist/types/{types/property-options.d.ts → property-options.d.ts} +0 -0
  313. /package/dist/types/{types/query-options.d.ts → query-options.d.ts} +0 -0
  314. /package/dist/types/{types/request-options.d.ts → request-options.d.ts} +0 -0
  315. /package/dist/types/{types/respond-options.d.ts → respond-options.d.ts} +0 -0
  316. /package/dist/types/{types/route-params.d.ts → route-params.d.ts} +0 -0
  317. /package/dist/types/{types/router-instance.d.ts → router-instance.d.ts} +0 -0
  318. /package/dist/types/{types/router-options.d.ts → router-options.d.ts} +0 -0
  319. /package/dist/types/{types/simple-array.d.ts → simple-array.d.ts} +0 -0
  320. /package/dist/types/{types/snice-element.d.ts → snice-element.d.ts} +0 -0
  321. /package/dist/types/{types/snice-global.d.ts → snice-global.d.ts} +0 -0
  322. /package/dist/types/{types/transition.d.ts → transition.d.ts} +0 -0
  323. /package/dist/{types/utils.d.ts → utils.d.ts} +0 -0
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
- # Snice
1
+ # Snice v3.0.0
2
2
 
3
- An imperative TypeScript framework for building vanilla web components with decorators and routing
3
+ > **AI Assistants:** For token-efficient documentation, read [docs/ai/](./docs/ai/) instead. Same content, 70% fewer tokens.
4
4
 
5
- ## Quick Start
5
+ A TypeScript framework for building sustainable web applications through clear separation of governance.
6
6
 
7
- Create a new Snice app with one command:
7
+ ## Quick Start
8
8
 
9
9
  ```bash
10
10
  npx snice create-app my-app
@@ -12,1101 +12,720 @@ cd my-app
12
12
  npm run dev
13
13
  ```
14
14
 
15
- ## Core Philosophy: Imperative, Not Reactive
15
+ ## Philosophy: Sustainable Architecture Through Separation of Concerns
16
16
 
17
- Snice takes an **imperative approach** to web components. Unlike reactive frameworks that automatically re-render when data changes, Snice components:
17
+ Most frameworks separate code by technology: HTML, CSS, JavaScript. Snice separates by **concerns of governance and data flow**. This shepherds you toward sustainable development by providing tools that encourage good practices.
18
18
 
19
- - **Render once** when connected to the DOM
20
- - **Never re-render** automatically
21
- - Require **explicit method calls** to update visual state
22
- - Give you **full control** over when and how updates happen
19
+ ### The Snice Architecture
23
20
 
24
- This approach gives you direct control over DOM updates without hidden complexity or automatic re-renders.
21
+ Every application has four distinct concerns:
25
22
 
26
- ## The Snice Way: Elements + Controllers
23
+ **1. Cross-Cutting Concerns** - Global state and navigation
24
+ - Authentication/principal
25
+ - Theming and preferences
26
+ - Routing and navigation
27
+ - Application-wide configuration
27
28
 
28
- Snice separates UI from data: **elements handle UI, controllers handle behavior and data**.
29
+ **2. Pages** - Code that handles human intent
30
+ - Understand what the user is trying to accomplish
31
+ - Orchestrate atomic elements to fulfill that intent
32
+ - Handle page-level data fetching and coordination
33
+ - Map URL parameters to user goals
29
34
 
30
- ```typescript
31
- import { element, controller, property, query } from 'snice';
35
+ **3. Elements** - Pure presentation
36
+ - Display data, nothing more
37
+ - No understanding of business logic or user intent
38
+ - Completely reusable across different contexts
39
+ - Concerned only with how things look and visual behaviors
32
40
 
33
- export interface IUserCard extends HTMLElement {
34
- userId: string;
35
- showUser(user: any): void;
36
- }
41
+ **4. Controllers** - Behavior and data management
42
+ - Handle server communication and data fetching when applicable
43
+ - Manage complex business logic when pages get too large
44
+ - Enable behavior swapping (A/B testing, feature flags)
45
+ - Clear separation of presentation from behavior
37
46
 
38
- // Element: Just UI
39
- @element('user-card')
40
- class UserCard extends HTMLElement implements IUserCard {
41
- @property({ attribute: 'user-id' })
42
- userId = '';
43
-
44
- @query('h3')
45
- nameElement!: HTMLHeadingElement;
46
-
47
- @query('p')
48
- emailElement!: HTMLParagraphElement;
49
-
50
- html() {
51
- return `
52
- <div class="card">
53
- <h3>Loading...</h3>
54
- <p>Please wait...</p>
55
- </div>
56
- `;
57
- }
58
-
59
- showUser(user: any) {
60
- this.nameElement.textContent = user.name;
61
- this.emailElement.textContent = user.email;
62
- }
63
- }
47
+ This architecture ensures **pages orchestrate**, **elements present**, and **controllers behave**. Data flows down, events flow up, and every piece knows only what it needs to know.
64
48
 
65
- // Controller: Data and behavior
66
- @controller('user-loader')
67
- class UserLoaderController {
68
- element!: IUserCard;
69
-
70
- async attach(element: IUserCard) {
71
- const response = await fetch(`/api/users/${element.userId}`);
72
- const user = await response.json();
73
-
74
- element.showUser(user);
75
- }
76
-
77
- async detach(element: IUserCard) { /* Cleanup */ }
78
- }
79
- ```
49
+ ## Why This Matters
80
50
 
81
- Connect them in HTML:
82
- ```html
83
- <user-card user-id="123" controller="user-loader"></user-card>
84
- ```
51
+ Traditional component architectures blur these lines. A "UserProfile" component might handle routing, authentication, API calls, and rendering. When requirements change, you can't swap behavior without touching presentation. When you need to reuse the UI, you can't because it's coupled to specific business logic.
85
52
 
86
- That's it. Element renders UI, controller fetches data, they communicate through method calls, events, and request/response channels.
53
+ Snice encourages boundaries:
54
+ - Want different behavior? Swap the controller, keep the element
55
+ - Need to reuse UI? Elements don't know about your business logic
56
+ - Debugging data flow? Follow the clear page → element → controller boundaries
57
+ - Onboarding new developers? The architecture guides them to the right place
87
58
 
88
- ## Core Concepts
59
+ ## The Tools
89
60
 
90
- Snice provides a clear separation of concerns through decorators:
61
+ Snice provides decorators and utilities that map directly to these architectural concerns:
91
62
 
92
- ### Class Decorators
93
- - **`@element`** - Creates custom HTML elements with encapsulated visual behavior and styling
94
- - **`@controller`** - Handles data fetching, server communication, and business logic separate from visual components
95
- - **`@page`** - Defines routable page components that render when their route is active, with URL params passed as attributes
63
+ ### 1. Cross-Cutting Concerns: Router + Context
96
64
 
97
- ### Property & Query Decorators
98
- - **`@property`** - Declares properties that automatically sync with DOM attributes
99
- - **`@query`** - Queries a single element from shadow DOM
100
- - **`@queryAll`** - Queries multiple elements from shadow DOM
101
- - **`@watch`** - Watches property changes and calls a method when they occur
102
- - **`@ready`** - Runs a method after the element's shadow DOM is ready
103
- - **`@dispose`** - Runs a method when the element is removed from the DOM
65
+ ```typescript
66
+ // app-context.ts
67
+ class AppContext {
68
+ user: User | null = null;
69
+ theme: 'light' | 'dark' = 'light';
104
70
 
105
- ### Event Decorators
106
- - **`@on`** - Listens for events on elements
107
- - **`@dispatch`** - Dispatches custom events after method execution
108
- - **`@request`** - Makes requests from elements or controllers
109
- - **`@respond`** - Responds to requests in elements or controllers
71
+ setUser(user: User) { this.user = user; }
72
+ getUser() { return this.user; }
73
+ }
110
74
 
111
- This separation keeps your components focused: elements handle presentation, controllers manage data, and pages define navigation.
75
+ // main.ts
76
+ import { Router } from 'snice';
112
77
 
113
- ## Basic Component
78
+ const { page, navigate, initialize } = Router({
79
+ target: '#app',
80
+ context: new AppContext() // Global state
81
+ });
114
82
 
115
- ```typescript
116
- import { element } from 'snice';
83
+ // Any page can access context
84
+ @page({ tag: 'dashboard-page', routes: ['/dashboard'] })
85
+ class DashboardPage extends HTMLElement {
86
+ private appContext?: AppContext;
117
87
 
118
- @element('my-button')
119
- class MyButton extends HTMLElement {
120
- html() {
121
- return `<button>Click me</button>`;
88
+ @context()
89
+ handleContext(ctx: Context) {
90
+ this.appContext = ctx.application;
91
+ cost user = this.getUser();
122
92
  }
93
+ // ...
123
94
  }
124
95
  ```
125
96
 
126
- That's it. Your component renders when added to the DOM:
97
+ ### 2. Pages: Orchestrating Intent
127
98
 
128
- ```html
129
- <my-button></my-button>
130
- ```
131
-
132
- ## The Imperative Way
99
+ ```typescript
100
+ // pages/user-profile-page.ts
101
+ @page({ tag: 'user-profile-page', routes: ['/users/:userId'] })
102
+ class UserProfilePage extends HTMLElement {
103
+ @property()
104
+ userId = ''; // From URL parameter
133
105
 
134
- In Snice, updates are explicit. Components expose methods that controllers or other components call to update state:
106
+ @property({ type: Object })
107
+ user = null;
135
108
 
136
- ```typescript
137
- import { element, property, query } from 'snice';
109
+ @property({ type: Object })
110
+ userStats = null;
138
111
 
139
- @element('counter-display')
140
- class CounterDisplay extends HTMLElement {
141
- @property({ type: Number })
142
- count = 0;
143
-
144
- @query('.count')
145
- countElement!: HTMLSpanElement;
146
-
147
- @query('.status')
148
- statusElement!: HTMLSpanElement;
149
-
150
- html() {
151
- // Renders ONCE - no automatic re-rendering
152
- return `
153
- <div class="counter">
154
- <span class="count">${this.count}</span>
155
- <span class="status">Ready</span>
156
- </div>
112
+ @ready()
113
+ async loadUserData() {
114
+ // Pages handle data fetching, elements just display
115
+ const [user, stats] = await Promise.all([
116
+ fetch(`/api/users/${this.userId}`).then(r => r.json()),
117
+ fetch(`/api/users/${this.userId}/stats`).then(r => r.json())
118
+ ]);
119
+ this.user = user;
120
+ this.userStats = stats;
121
+ }
122
+
123
+ @render()
124
+ renderContent() {
125
+ return html`
126
+ <page-header .user=${this.user}></page-header>
127
+ <user-stats .stats=${this.userStats}></user-stats>
128
+ <user-activity .userId=${this.userId}></user-activity>
157
129
  `;
158
130
  }
159
-
160
- // Imperative update methods - YOU control when updates happen
161
- setCount(newCount: number) {
162
- this.count = newCount;
163
- this.countElement.textContent = String(newCount);
164
- }
165
-
166
- setStatus(status: string) {
167
- this.statusElement.textContent = status;
168
- }
169
-
170
- increment() {
171
- this.setCount(this.count + 1);
172
- this.setStatus('Incremented!');
173
- }
174
131
  }
175
132
  ```
176
133
 
177
- **Key Points:**
178
- - The `html()` method runs **once** when the element connects
179
- - Updates happen through **explicit method calls** like `setCount()`
180
- - You have **full control** over what updates and when
181
- - No surprises, no magic, no hidden re-renders
182
-
183
- ## Properties
184
-
185
- Properties automatically sync with DOM attributes in both directions. The HTML is rendered once when the element connects to the DOM. Use properties for initial configuration and watch for changes to update the UI.
134
+ ### 3. Elements: Pure Presentation
186
135
 
187
136
  ```typescript
188
- import { element, property } from 'snice';
189
-
190
- @element('user-card')
191
- class UserCard extends HTMLElement {
192
- @property()
193
- name = 'Anonymous';
194
-
195
- @property({ attribute: 'user-role' }) // Maps to user-role attribute
196
- role = 'User';
137
+ // elements/user-stats.ts
138
+ @element('user-stats')
139
+ class UserStats extends HTMLElement {
140
+ @property({ type: Object })
141
+ stats = null;
197
142
 
198
- @property({ type: Boolean })
199
- verified = false;
143
+ @render()
144
+ renderContent() {
145
+ if (!this.stats) return html`<div>Loading...</div>`;
200
146
 
201
- html() {
202
- // This renders ONCE with the initial property values
203
- return `
204
- <div class="card">
205
- <h3>${this.name}</h3>
206
- <span class="role">${this.role}</span>
207
- ${this.verified ? '<span class="badge">✓ Verified</span>' : ''}
147
+ return html`
148
+ <div class="stats">
149
+ <div class="stat">
150
+ <span class="label">Views</span>
151
+ <span class="value">${this.stats.views}</span>
152
+ </div>
153
+ <div class="stat">
154
+ <span class="label">Followers</span>
155
+ <span class="value">${this.stats.followers}</span>
156
+ </div>
208
157
  </div>
209
158
  `;
210
159
  }
160
+
161
+ @styles()
162
+ statsStyles() {
163
+ return css`
164
+ .stats { display: flex; gap: 2rem; }
165
+ .stat { text-align: center; }
166
+ `;
167
+ }
211
168
  }
212
- ```
213
169
 
214
- Use it with attributes (both ways work):
215
- ```html
216
- <!-- Setting attributes automatically updates properties -->
217
- <user-card name="Jane Doe" user-role="Admin" verified></user-card>
218
-
219
- <script>
220
- const card = document.querySelector('user-card');
221
- card.name = 'John Smith'; // Sets name="John Smith" attribute
222
- card.verified = true; // Sets verified attribute
223
- </script>
170
+ // Usage in parent page (which handles data fetching):
171
+ // <user-stats .stats=${this.userStats}></user-stats>
224
172
  ```
225
173
 
226
- For arrays of basic types, use `SimpleArray` for safe reflection:
174
+ ### 4. Controllers: Behavior Management
227
175
 
228
176
  ```typescript
229
- import { element, property, SimpleArray } from 'snice';
177
+ // controllers/real-time-user-loader.ts
178
+ @controller('real-time-user-loader')
179
+ class RealTimeUserLoader {
180
+ async attach(element: IUserList) {
181
+ this.socket = new WebSocket('/api/users/stream');
182
+ this.socket.onmessage = (e) => {
183
+ element.setUsers(JSON.parse(e.data));
184
+ };
185
+ }
186
+ // ...
187
+ }
188
+
189
+ // controllers/cached-user-loader.ts
190
+ @controller('cached-user-loader')
191
+ class CachedUserLoader {
192
+ async attach(element: IUserList) {
193
+ const cached = localStorage.getItem('users');
194
+ if (cached) element.setUsers(JSON.parse(cached));
195
+ }
196
+ // ...
197
+ }
230
198
 
231
- @element('tag-list')
232
- class TagList extends HTMLElement {
233
- @property({ type: SimpleArray })
234
- tags = ['javascript', 'typescript', 'web'];
199
+ // elements/user-list.ts - stays the same
200
+ @element('user-list')
201
+ class UserList extends HTMLElement {
202
+ setUsers(users: User[]) {
203
+ this.users = users;
204
+ // ...
205
+ }
235
206
 
236
- html() {
237
- return `<div>${this.tags.join(', ')}</div>`;
207
+ @render()
208
+ renderContent() {
209
+ return html`
210
+ <ul>${this.users.map(u => html`<li>${u.name}</li>`)}</ul>
211
+ `;
238
212
  }
239
213
  }
240
214
  ```
241
215
 
216
+ Usage - swap behavior without touching presentation:
217
+
242
218
  ```html
243
- <tag-list tags="react,vue,angular"></tag-list>
219
+ <user-list controller="real-time-user-loader"></user-list>
220
+ <user-list controller="cached-user-loader"></user-list>
244
221
  ```
245
222
 
246
- ## Watching Property Changes
223
+ ## Key Features
247
224
 
248
- Use `@watch` to imperatively update DOM when properties change:
225
+ **Differential Rendering** - Only updates changed parts of the DOM, not entire components
249
226
 
250
- ```typescript
251
- import { element, property, watch, query } from 'snice';
227
+ **Auto-Rendering** - Components automatically re-render when properties change
252
228
 
253
- @element('theme-toggle')
254
- class ThemeToggle extends HTMLElement {
255
- @property()
256
- theme: 'light' | 'dark' = 'light';
229
+ **Template Syntax** - Clean `html\`...\`` and `css\`...\`` tagged templates
257
230
 
258
- @query('.icon')
259
- icon!: HTMLSpanElement;
260
-
261
- html() {
262
- return `
263
- <button>
264
- <span class="icon">🌞</span>
265
- </button>
266
- `;
267
- }
268
-
269
- @watch('theme')
270
- updateTheme(oldValue: string, newValue: string) {
271
- this.icon.textContent = newValue === 'dark' ? '🌙' : '🌞';
272
- }
273
-
274
- @on('click', 'button')
275
- toggle() {
276
- this.theme = this.theme === 'light' ? 'dark' : 'light';
277
- }
278
- }
279
- ```
231
+ **Type Safety** - Full TypeScript support with decorator-based APIs
280
232
 
281
- **Key Points:**
282
- - `@watch` methods are called when the property value changes
283
- - Receives `oldValue`, `newValue`, and `propertyName` as parameters
284
- - Perfect for imperatively updating DOM elements
285
- - Can watch multiple properties with multiple decorators
286
- - Works with both programmatic changes and attribute changes
233
+ **Zero Dependencies** - No external runtime dependencies, small bundle size
287
234
 
288
- You can watch multiple properties with a single decorator:
235
+ **Standards-Based** - Built on web components, works with any framework
289
236
 
290
- ```typescript
291
- @watch('width', 'height', 'scale')
292
- updateDimensions(_old: number, _new: number, _name: string) {
293
- // Called when any of these properties change
294
- console.log(`${_name} changed from ${_old} to ${_new}`);
295
- this.recalculateLayout();
296
- }
297
- ```
237
+ ## Core APIs
298
238
 
299
- Watch all property changes with the wildcard:
239
+ ### Class Decorators
300
240
 
241
+ **`@element('tag-name')`** - Define reusable UI components
301
242
  ```typescript
302
- @watch('*')
303
- handleAnyPropertyChange(_old: any, _new: any, _name: string) {
304
- console.log(`Property ${_name} changed from ${_old} to ${_new}`);
305
- // Useful for debugging or when all properties affect the same output
306
- }
243
+ @element('my-button')
244
+ class MyButton extends HTMLElement { }
307
245
  ```
308
246
 
309
- ## Queries
310
-
311
- Query single elements with `@query`:
312
-
247
+ **`@page({ tag, routes })`** - Define routable pages
313
248
  ```typescript
314
- import { element, query } from 'snice';
315
-
316
- @element('my-form')
317
- class MyForm extends HTMLElement {
318
- @query('input')
319
- input!: HTMLInputElement;
320
-
321
- html() {
322
- return `<input type="text" />`;
323
- }
324
-
325
- getValue() {
326
- return this.input.value;
327
- }
328
- }
249
+ @page({ tag: 'home-page', routes: ['/'] })
250
+ class HomePage extends HTMLElement { }
329
251
  ```
330
252
 
331
- Query multiple elements with `@queryAll`:
332
-
253
+ **`@controller('controller-name')`** - Define behavior modules
333
254
  ```typescript
334
- import { element, queryAll } from 'snice';
335
-
336
- @element('checkbox-group')
337
- class CheckboxGroup extends HTMLElement {
338
- @queryAll('input[type="checkbox"]')
339
- checkboxes!: NodeListOf<HTMLInputElement>;
340
-
341
- html() {
342
- return `
343
- <input type="checkbox" value="option1" />
344
- <input type="checkbox" value="option2" />
345
- <input type="checkbox" value="option3" />
346
- `;
347
- }
348
-
349
- getSelectedValues() {
350
- return Array.from(this.checkboxes)
351
- .filter(cb => cb.checked)
352
- .map(cb => cb.value);
353
- }
255
+ @controller('data-loader')
256
+ class DataLoader {
257
+ async attach(element) { }
258
+ async detach(element) { }
354
259
  }
355
260
  ```
356
261
 
357
- ## Events
358
-
359
- Listen for events with `@on`:
262
+ ### Rendering
360
263
 
264
+ **`@render(options?)`** - Define component template (auto re-renders on property changes)
361
265
  ```typescript
362
- import { element, on } from 'snice';
363
-
364
- @element('my-clicker')
365
- class MyClicker extends HTMLElement {
366
- html() {
367
- return `
368
- <button>Click me</button>
369
- <input type="text" placeholder="Press Enter" />
370
- `;
371
- }
372
-
373
- @on('click', 'button')
374
- handleClick() {
375
- console.log('Button clicked!');
376
- }
377
-
378
- @on('keydown:Enter', 'input') // Only plain Enter (no modifiers)
379
- handleEnter() {
380
- console.log('Enter pressed!');
381
- }
382
-
383
- @on('keydown:ctrl+Enter', 'input') // Only Ctrl+Enter
384
- handleCtrlEnter() {
385
- console.log('Ctrl + Enter pressed!');
386
- }
387
-
388
- @on('focus') // Listen on the host element itself (no target)
389
- handleFocus() {
390
- console.log('Element received focus!');
391
- }
266
+ @render()
267
+ renderContent() {
268
+ return html`<div>${this.data}</div>`;
392
269
  }
393
270
  ```
394
271
 
395
- ## Dispatching Events
396
-
397
- Automatically dispatch custom events with `@dispatch`:
398
-
272
+ **`@styles()`** - Define scoped styles
399
273
  ```typescript
400
- import { element, dispatch, on, query } from 'snice';
401
-
402
- @element('toggle-switch')
403
- class ToggleSwitch extends HTMLElement {
404
- private isOn = false;
405
-
406
- @query('.toggle')
407
- toggleButton!: HTMLElement;
408
-
409
- html() {
410
- return `<button class="toggle">OFF</button>`;
411
- }
412
-
413
- @on('click', '.toggle')
414
- @dispatch('toggled')
415
- toggle() {
416
- this.isOn = !this.isOn;
417
- this.toggleButton.textContent = this.isOn ? 'ON' : 'OFF';
418
- return { on: this.isOn };
419
- }
274
+ @styles()
275
+ componentStyles() {
276
+ return css`.container { padding: 1rem; }`;
420
277
  }
421
278
  ```
422
279
 
423
- The `@dispatch` decorator:
424
- - Dispatches after the method completes
425
- - Uses the return value as the event detail
426
- - Works with async methods
427
- - Bubbles by default
280
+ ### Properties & State
428
281
 
282
+ **`@property(options?)`** - Reactive properties that sync with attributes
429
283
  ```typescript
430
- // With options from EventInit
431
- @dispatch('my-event', { bubbles: false, cancelable: true })
284
+ @property()
285
+ name = 'default';
432
286
 
433
- // Don't dispatch if method returns undefined
434
- @dispatch('maybe-data', { dispatchOnUndefined: false })
287
+ @property({ type: Boolean })
288
+ enabled = false;
435
289
  ```
436
290
 
437
- ## Styling
438
-
291
+ **`@watch(...propertyNames)`** - React to property changes
439
292
  ```typescript
440
- @element('styled-card')
441
- class StyledCard extends HTMLElement {
442
- html() {
443
- return `<div class="card">Hello</div>`;
444
- }
445
-
446
- css() {
447
- return `
448
- .card {
449
- padding: 20px;
450
- background: #f0f0f0;
451
- border-radius: 8px;
452
- }
453
- `;
454
- }
293
+ @watch('name')
294
+ onNameChange(oldVal, newVal) {
295
+ console.log(`Name changed from ${oldVal} to ${newVal}`);
455
296
  }
456
297
  ```
457
298
 
458
- CSS is automatically scoped to your component.
459
-
460
- ## Routing
299
+ ### Lifecycle
461
300
 
301
+ **`@ready()`** - Runs after initial render completes
462
302
  ```typescript
463
- import { Router } from 'snice';
464
-
465
- const router = Router({ target: '#app', type: 'hash' });
466
-
467
- const { page, navigate, initialize } = router;
468
-
469
- @page({ tag: 'home-page', routes: ['/'] })
470
- class HomePage extends HTMLElement {
471
- html() {
472
- return `<h1>Home</h1>`;
473
- }
303
+ @ready()
304
+ async initialize() {
305
+ // Fetch data, set up listeners, etc.
474
306
  }
307
+ ```
475
308
 
476
- @page({ tag: 'about-page', routes: ['/about'] })
477
- class AboutPage extends HTMLElement {
478
- html() {
479
- return `<h1>About</h1>`;
480
- }
481
- }
482
-
483
- // Page with URL parameter
484
- import { property } from 'snice';
485
-
486
- @page({ tag: 'user-page', routes: ['/users/:userId'] })
487
- class UserPage extends HTMLElement {
488
- @property()
489
- userId = '';
490
-
491
- html() {
492
- return `<h1>User ${this.userId}</h1>`;
493
- }
309
+ **`@dispose()`** - Runs when element is removed from DOM
310
+ ```typescript
311
+ @dispose()
312
+ cleanup() {
313
+ // Clean up listeners, close connections, etc.
494
314
  }
495
-
496
- // Start the router
497
- initialize();
498
-
499
- // Navigate programmatically
500
- navigate('/about');
501
- navigate('/users/123'); // Sets userId="123" on UserPage
502
315
  ```
503
316
 
504
- ### Route Guards
505
-
506
- Protect routes with guard functions that control access:
317
+ ### DOM Queries
507
318
 
319
+ **`@query(selector)`** - Query single element from shadow DOM
508
320
  ```typescript
509
- import { Router, Guard, RouteParams } from 'snice';
510
-
511
- // Create router with context
512
- const router = Router({
513
- target: '#app',
514
- type: 'hash',
515
- context: new AppContext(), // Your app's context object
516
- });
321
+ @query('input')
322
+ input!: HTMLInputElement;
323
+ ```
517
324
 
518
- const { page, navigate, initialize } = router;
325
+ **`@queryAll(selector)`** - Query multiple elements from shadow DOM
326
+ ```typescript
327
+ @queryAll('.item')
328
+ items!: NodeListOf<HTMLElement>;
329
+ ```
519
330
 
520
- // Simple guards (params is empty object for non-parameterized routes)
521
- const isAuthenticated: Guard<AppContext> = (ctx, params) => ctx.getUser() !== null;
522
- const isAdmin: Guard<AppContext> = (ctx, params) => ctx.getUser()?.role === 'admin';
331
+ ### Events & Communication
523
332
 
524
- // Protected page with single guard
525
- @page({ tag: 'dashboard-page', routes: ['/dashboard'], guards: isAuthenticated })
526
- class DashboardPage extends HTMLElement {
527
- html() {
528
- return `<h1>Dashboard</h1>`;
529
- }
530
- }
333
+ **Template Events** - Handle events directly in templates (with keyboard modifiers!)
334
+ ```typescript
335
+ html`
336
+ <button @click=${this.handleClick}>Click</button>
337
+ <input @keydown:Enter=${this.submit} />
338
+ <input @keydown:ctrl+s=${this.save} />
339
+ `
340
+ ```
531
341
 
532
- // Admin page with multiple guards (all must pass)
533
- @page({ tag: 'admin-page', routes: ['/admin'], guards: [isAuthenticated, isAdmin] })
534
- class AdminPage extends HTMLElement {
535
- html() {
536
- return `<h1>Admin Panel</h1>`;
537
- }
342
+ **`@on` Decorator** - Event delegation with selectors
343
+ ```typescript
344
+ // Works in both elements AND controllers
345
+ @on('click', 'button') // Event delegation
346
+ handleClick(e: Event) {
347
+ console.log('Button clicked!');
538
348
  }
539
349
 
540
- // Guard that uses route params to check resource-specific permissions
541
- const canEditUser: Guard<AppContext> = (ctx, params) => {
542
- const user = ctx.getUser();
543
- if (!user) return false;
544
-
545
- // params.id comes from route '/users/:id/edit'
546
- return ctx.hasPermission('users.edit', params.id);
547
- };
548
-
549
- // Guard that checks ownership
550
- const ownsItem: Guard<AppContext> = (ctx, params) => {
551
- const user = ctx.getUser();
552
- if (!user) return false;
553
-
554
- // params.itemId comes from route '/items/:itemId'
555
- return user.ownedItems.includes(parseInt(params.itemId));
556
- };
557
-
558
- @page({ tag: 'user-edit', routes: ['/users/:id/edit'], guards: [isAuthenticated, canEditUser] })
559
- class UserEditPage extends HTMLElement {
560
- @property()
561
- id = ''; // Automatically set from route param
562
-
563
- html() {
564
- return `<h1>Edit User ${this.id}</h1>`;
565
- }
350
+ @on('keydown:Enter', 'input') // Keyboard modifiers
351
+ handleEnter(e: KeyboardEvent) {
352
+ this.submit();
566
353
  }
567
354
 
568
- @page({ tag: 'item-view', routes: ['/items/:itemId'], guards: [isAuthenticated, ownsItem] })
569
- class ItemView extends HTMLElement {
570
- @property()
571
- itemId = ''; // Automatically set from route param
572
-
573
- html() {
574
- return `<h1>Item ${this.itemId}</h1>`;
575
- }
355
+ @on('input', 'input', { debounce: 300 }) // Debounce support
356
+ handleInput(e: Event) {
357
+ this.search((e.target as HTMLInputElement).value);
576
358
  }
359
+ ```
577
360
 
578
- // Custom 403 page (optional)
579
- @page({ tag: 'forbidden-page', routes: ['/403'] })
580
- class ForbiddenPage extends HTMLElement {
581
- html() {
582
- return `
583
- <h1>Access Denied</h1>
584
- <p>You don't have permission to view this page.</p>
585
- <a href="#/">Return to home</a>
586
- `;
587
- }
361
+ **`@dispatch(eventName)`** - Auto-dispatch custom events after method execution
362
+ ```typescript
363
+ @dispatch('value-changed')
364
+ setValue(val: string) {
365
+ this.value = val;
366
+ return { value: val }; // Event detail
588
367
  }
589
-
590
- initialize();
591
368
  ```
592
369
 
593
- When a guard denies access:
594
- - The 403 page is rendered if defined
595
- - Otherwise, a default "Unauthorized" message is shown
596
- - The URL doesn't change (no redirect)
597
-
598
- ## Controllers (Data Fetching)
599
-
600
- Controllers handle server communication separately from visual components:
370
+ ### Global State
601
371
 
372
+ **`@context(options?)`** - Receive router context updates (global state)
602
373
  ```typescript
603
- import { controller, element } from 'snice';
604
-
605
- interface IUserElement extends HTMLElement {
606
- setUsers(users: any[]): void;
374
+ // Method decorator that receives context updates
375
+ @context()
376
+ handleContext(ctx: Context) {
377
+ this.appContext = ctx.application;
378
+ this.requestRender();
607
379
  }
608
380
 
609
- @controller('user-controller')
610
- class UserController {
611
- element!: IUserElement;
612
-
613
- async attach(element: IUserElement) {
614
- const response = await fetch('/api/users');
615
- const users = await response.json();
616
- element.setUsers(users);
617
- }
618
-
619
- async detach(element: IUserElement) { /* Cleanup */ }
381
+ // With timing options
382
+ @context({ debounce: 300 })
383
+ handleContextDebounced(ctx: Context) {
384
+ // Called after 300ms of no updates
620
385
  }
621
386
 
622
- @element('user-list')
623
- class UserList extends HTMLElement {
624
- users: any[] = [];
625
-
626
- html() {
627
- return `
628
- <ul>
629
- ${this.users.map(u => `<li>${u.name}</li>`).join('')}
630
- </ul>
631
- `;
632
- }
387
+ @context({ throttle: 100 })
388
+ handleContextThrottled(ctx: Context) {
389
+ // Called at most once per 100ms
390
+ }
633
391
 
634
- setUsers(users: any[]) {
635
- this.users = users;
636
- if (this.shadowRoot) {
637
- this.shadowRoot.innerHTML = this.html();
638
- }
639
- }
392
+ @context({ once: true })
393
+ handleContextOnce(ctx: Context) {
394
+ // Called only once, then unregisters
640
395
  }
641
396
  ```
642
397
 
643
- Use it:
644
-
645
- ```html
646
- <user-list controller="user-controller"></user-list>
398
+ **Context Object Structure:**
399
+ ```typescript
400
+ interface Context {
401
+ application: AppContext; // Your router context
402
+ navigation: {
403
+ placards: Placard[]; // Page metadata
404
+ route: string; // Current route
405
+ params: Record<string, string>; // Route parameters
406
+ };
407
+ update(): void; // Notify all subscribers
408
+ }
647
409
  ```
648
410
 
649
- ## Request/Response
411
+ **Triggering Context Updates:**
650
412
 
651
- Bidirectional communication between elements and controllers:
413
+ When you modify the application context, call `update()` to notify all subscribers:
652
414
 
653
415
  ```typescript
654
- import { element, request, type Response } from 'snice';
416
+ @page({ tag: 'login-page', routes: ['/login'] })
417
+ class LoginPage extends HTMLElement {
418
+ private ctx?: Context<AppContext>;
655
419
 
656
- // Element makes request, controller responds
657
- @element('user-profile')
658
- class UserProfile extends HTMLElement {
659
-
660
- @request('fetch-user')
661
- async *getUser(): Response<{ name: string; email: string }> {
662
- const user = await (yield { userId: 123 });
663
- return user;
420
+ @context()
421
+ handleContext(ctx: Context<AppContext>) {
422
+ this.ctx = ctx;
423
+ this.requestRender();
664
424
  }
665
425
 
666
- @ready()
667
- async load() {
668
- const userData = await this.getUser();
669
- this.displayUser(userData);
426
+ login(user: User) {
427
+ // Modify the application context
428
+ this.ctx!.application.setUser(user);
429
+
430
+ // Notify all @context subscribers
431
+ this.ctx!.update();
670
432
  }
671
-
672
433
  }
434
+ ```
673
435
 
674
- @controller('user-controller')
675
- class UserController {
436
+ **Note:** The router calls `update()` automatically during navigation. Only call it manually when you change application state (like login/logout, theme changes, etc.).
676
437
 
677
- @respond('fetch-user')
678
- async handleFetchUser(request: { userId: number }) {
679
- const response = await fetch(`/api/users/${request.userId}`);
680
- return response.json();
681
- }
438
+ ### Request/Response
682
439
 
683
- }
684
- ```
440
+ **`@request(channel)`** - Make requests to controllers
441
+ **`@respond(channel)`** - Respond to requests from elements
685
442
 
686
- ## Layouts
443
+ See [Request/Response documentation](./docs/request-response.md) for details.
687
444
 
688
- Wrap your pages in shared layout components for consistent navigation, headers, and footers across your application.
445
+ ## Template Syntax
689
446
 
690
- ### Basic Layout Usage
447
+ ### Auto-Rendering with Differential Updates
691
448
 
692
449
  ```typescript
693
- import { Router, layout, page } from 'snice';
450
+ @element('counter-display')
451
+ class CounterDisplay extends HTMLElement {
452
+ @property({ type: Number })
453
+ count = 0;
694
454
 
695
- // Create a layout component
696
- @layout('app-shell')
697
- class AppShell extends HTMLElement {
698
- html() {
699
- return `
700
- <header>
701
- <nav>
702
- <a href="#/">Home</a>
703
- <a href="#/about">About</a>
704
- </nav>
705
- </header>
706
- <main>
707
- <slot name="page"></slot>
708
- </main>
709
- <footer>© 2024 My App</footer>
455
+ @render()
456
+ renderContent() {
457
+ return html`
458
+ <div class="counter">
459
+ <span class="count">${this.count}</span>
460
+ <button @click=${this.increment}>+</button>
461
+ </div>
710
462
  `;
711
463
  }
712
- }
713
464
 
714
- // Configure router with default layout
715
- const router = Router({
716
- target: '#app',
717
- type: 'hash',
718
- layout: 'app-shell' // All pages use this layout by default
719
- });
720
-
721
- const { page, initialize } = router;
722
-
723
- // Pages automatically render inside the layout
724
- @page({ tag: 'home-page', routes: ['/'] })
725
- class HomePage extends HTMLElement {
726
- html() {
727
- return `<h1>Home Content</h1>`;
465
+ @styles()
466
+ counterStyles() {
467
+ return css`.counter { display: flex; gap: 1rem; }`;
728
468
  }
729
- }
730
469
 
731
- // Override layout per page
732
- @page({ tag: 'full-page', routes: ['/fullscreen'], layout: false })
733
- class FullPage extends HTMLElement {
734
- html() {
735
- return `<div>No layout wrapper</div>`;
470
+ increment() {
471
+ this.count++;
472
+ // Auto re-renders! Only <span class="count"> updates
736
473
  }
737
474
  }
738
-
739
- initialize();
740
475
  ```
741
476
 
742
- ### Layout Features
477
+ **Key Points:**
478
+ - Properties trigger automatic re-renders
479
+ - Only changed parts update (differential rendering)
480
+ - Event handlers: `@click=${this.method}`
481
+ - Batched updates (multiple changes = single render)
743
482
 
744
- - **Shared wrapper**: Layout components wrap page content using `<slot name="page"></slot>`
745
- - **Default layouts**: Set `layout: 'component-name'` in router options
746
- - **Per-page override**: Use `layout: 'other-layout'` or `layout: false` in page options
747
- - **Smooth transitions**: Layout persists during page transitions for better UX
748
- - **Nested layouts**: Layouts can contain other layouts for complex structures
483
+ ### Property Binding
749
484
 
750
- ## Router Context
485
+ Use `.property=${value}` to set element properties directly:
751
486
 
752
- Access router context in page components, nested elements, and controllers using the `@context` decorator.
487
+ ```typescript
488
+ html`
489
+ <input .value=${this.text} />
490
+ <custom-element .complexData=${this.dataObject}></custom-element>
491
+ `
492
+ ```
753
493
 
754
- ### ⚠️ Important Warning About Global State
494
+ ### Boolean Attributes
755
495
 
756
- **Context is global shared state and should be treated with extreme caution.** Mutating context from multiple components creates hard-to-debug issues, race conditions, and tightly coupled code. This is a serious footgun if used improperly.
496
+ Use `?attribute=${boolean}` for boolean attributes:
757
497
 
758
- **Best practices:**
759
- - Treat context as **read-only** in components
760
- - Only mutate context through well-defined methods in the context class
761
- - Use context for truly global, app-wide state (user auth, theme, locale)
762
- - For component-specific state, use properties instead
763
- - Consider the context immutable from the component's perspective
498
+ ```typescript
499
+ html`
500
+ <button ?disabled=${this.isLoading}>Submit</button>
501
+ <input type="checkbox" ?checked=${this.isChecked} />
502
+ `
503
+ ```
764
504
 
765
- ### Basic Usage in Pages
505
+ ### Conditionals
766
506
 
767
507
  ```typescript
768
- // Define your context class with controlled mutations
769
- class AppContext {
770
- private user: User | null = null;
771
-
772
- // Controlled mutation through methods
773
- setUser(user: User) {...}
774
-
775
- // Read-only access
776
- getUser() {...}
777
-
778
- isAuthenticated() {...}
779
- }
780
-
781
- // Create router with context
782
- const appContext = new AppContext();
783
- const { page, initialize } = Router({
784
- target: '#app',
785
- type: 'hash',
786
- context: appContext
787
- });
508
+ // Ternary operator
509
+ html`
510
+ ${this.isLoggedIn
511
+ ? html`<span>Welcome!</span>`
512
+ : html`<a href="/login">Login</a>`
513
+ }
514
+ `
515
+
516
+ // <if> conditional element
517
+ html`
518
+ <if ${this.isLoggedIn}>
519
+ <span>Welcome, ${this.user.name}!</span>
520
+ <button @click=${this.logout}>Logout</button>
521
+ </if>
522
+ <if ${!this.isLoggedIn}>
523
+ <a href="/login">Login</a>
524
+ </if>
525
+ `
526
+
527
+ // <case>/<when>/<default> for multiple branches
528
+ html`
529
+ <case ${this.status}>
530
+ <when value="loading">
531
+ <span>Loading...</span>
532
+ </when>
533
+ <when value="success">
534
+ <span>Success!</span>
535
+ </when>
536
+ <when value="error">
537
+ <span>Error occurred</span>
538
+ </when>
539
+ <default>
540
+ <span>Unknown status</span>
541
+ </default>
542
+ </case>
543
+ `
544
+ ```
545
+
546
+ ### Lists
788
547
 
789
- // Access context in page components
790
- @page({ tag: 'profile-page', routes: ['/profile'] })
791
- class ProfilePage extends HTMLElement {
792
- @context()
793
- ctx?: AppContext;
794
-
795
- html() {
796
- // READ context, don't mutate it directly
797
- const user = this.ctx?.getUser();
798
- if (!user) {
799
- return `<p>Please log in</p>`;
800
- }
801
- return `
802
- <h1>Welcome, ${user.name}!</h1>
803
- <p>Email: ${user.email}</p>
804
- `;
805
- }
806
- }
548
+ ```typescript
549
+ html`
550
+ <ul>
551
+ ${this.items.map(item => html`
552
+ <li @click=${() => this.select(item.id)}>${item.name}</li>
553
+ `)}
554
+ </ul>
555
+ `
807
556
  ```
808
557
 
809
- ### Context in Nested Elements
810
-
811
- Nested elements within pages can also access context:
558
+ ### Keyboard Shortcuts
812
559
 
813
560
  ```typescript
814
- // This element can be used inside any page
815
- @element('user-avatar')
816
- class UserAvatar extends HTMLElement {
817
- @context()
818
- ctx?: AppContext;
819
-
820
- html() {
821
- // Context is available even in nested elements
822
- const user = this.ctx?.getUser();
823
- return user
824
- ? `<img src="${user.avatar}" alt="${user.name}">`
825
- : `<div class="placeholder">?</div>`;
826
- }
827
- }
828
-
829
- // Use it in a page
830
- @page({ tag: 'dashboard', routes: ['/'] })
831
- class Dashboard extends HTMLElement {
832
- html() {
833
- return `
834
- <h1>Dashboard</h1>
835
- <user-avatar></user-avatar> <!-- Will have access to context -->
836
- `;
837
- }
838
- }
561
+ html`
562
+ <input @keydown.enter=${this.submit} />
563
+ <input @keydown.ctrl+s=${this.save} />
564
+ <input @keydown.ctrl+shift+s=${this.saveAs} />
565
+ <input @keydown.escape=${this.cancel} />
566
+ <input @keydown.~enter=${this.submitAny} />
567
+ `
839
568
  ```
840
569
 
841
- ### Context in Controllers
570
+ Keyboard syntax:
571
+ - `@keydown.enter` - Plain Enter (no modifiers)
572
+ - `@keydown.ctrl+s` - Ctrl+S combination
573
+ - `@keydown.~enter` - Enter with any modifiers
574
+ - `@keydown.down` - Arrow keys (up, down, left, right)
575
+ - `@keydown.escape` - Escape key
842
576
 
843
- Controllers attached to page elements automatically acquire context:
577
+ ## Router
844
578
 
845
579
  ```typescript
846
- @controller('nav-controller')
847
- class NavController {
848
- element!: HTMLElement;
849
-
850
- @context()
851
- ctx?: AppContext;
852
-
853
- attach(element: HTMLElement) {
854
- // Context is available in controllers too
855
- if (!this.ctx?.isAuthenticated()) {
856
- window.location.hash = '#/login';
857
- }
858
- }
859
-
860
- detach(element: HTMLElement) { /* Cleanup */ }
861
- }
580
+ // main.ts
581
+ const { page, navigate, initialize } = Router({
582
+ target: '#app',
583
+ context: new AppContext()
584
+ });
862
585
 
863
- @page({ tag: 'admin-page', routes: ['/admin'] })
864
- class AdminPage extends HTMLElement {
865
- html() {
866
- return `<div controller="nav-controller">Admin Panel</div>`;
586
+ // pages/home-page.ts
587
+ @page({ tag: 'home-page', routes: ['/'] })
588
+ class HomePage extends HTMLElement {
589
+ @render()
590
+ renderContent() {
591
+ return html`<h1>Home</h1>`;
867
592
  }
868
593
  }
869
- ```
870
594
 
871
- The `@context` decorator:
872
- - Injects the router's context into page components
873
- - Available to nested elements via event bubbling
874
- - Available to controllers attached to pages
875
- - Returns the same context instance everywhere
876
- - Automatically cleaned up when elements are removed
595
+ // pages/user-page.ts
596
+ @page({ tag: 'user-page', routes: ['/users/:userId'] })
597
+ class UserPage extends HTMLElement {
598
+ @property()
599
+ userId = ''; // Auto-populated from URL
600
+ // ...
601
+ }
877
602
 
878
- **Remember:** With great power comes great responsibility. Global state is dangerous - use it wisely and sparingly.
603
+ // main.ts
604
+ initialize();
605
+ navigate('/users/123');
606
+ ```
879
607
 
880
- ## Observing External Changes
608
+ ### Route Guards
881
609
 
882
- The `@observe` decorator provides lifecycle-managed observation of external changes like viewport intersection, element resize, media queries, and DOM mutations:
610
+ Protect routes with guard functions:
883
611
 
884
612
  ```typescript
885
- import { element, observe } from 'snice';
886
-
887
- @element('lazy-image')
888
- class LazyImage extends HTMLElement {
889
- html() {
890
- return `
891
- <img data-src="photo.jpg" class="lazy" />
892
- <div class="loading">Loading...</div>
893
- `;
894
- }
895
-
896
- // Observe when image enters viewport
897
- @observe('intersection', '.lazy', { threshold: 0.1 })
898
- loadImage(entry: IntersectionObserverEntry) {
899
- if (entry.isIntersecting) {
900
- const img = entry.target as HTMLImageElement;
901
- img.src = img.dataset.src!;
902
- img.classList.add('loaded');
903
- return false; // Stop observing after loading
904
- }
905
- }
613
+ const isAuthenticated: Guard<AppContext> = (ctx) => ctx.getUser() !== null;
906
614
 
907
- // Respond to viewport size changes
908
- @observe('media:(min-width: 768px)')
909
- handleDesktop(matches: boolean) {
910
- this.classList.toggle('desktop-mode', matches);
911
- }
912
- }
615
+ @page({
616
+ tag: 'dashboard-page',
617
+ routes: ['/dashboard'],
618
+ guards: isAuthenticated
619
+ })
620
+ class DashboardPage extends HTMLElement { }
913
621
  ```
914
622
 
915
- All observers are automatically cleaned up when elements disconnect from the DOM. See the [Observe API documentation](./docs/observe.md) for more examples.
916
-
917
- ## Parts - Selective Re-rendering
623
+ ## Layouts
918
624
 
919
- For complex components with frequent updates to specific sections, the `@part` decorator enables selective re-rendering of template parts without rebuilding the entire component:
625
+ Layouts wrap pages with shared UI and dynamically build navigation from page metadata:
920
626
 
921
627
  ```typescript
922
- import { element, part, property, on } from 'snice';
923
-
924
- @element('user-dashboard')
925
- class UserDashboard extends HTMLElement {
926
- @property()
927
- user = { name: 'Loading...', stats: { views: 0, likes: 0 } };
928
-
929
- notifications = [];
930
- messages = [];
931
-
932
- html() {
933
- return `
934
- <header part="user-info"></header>
935
- <main>
936
- <section part="stats"></section>
937
- <aside part="notifications"></aside>
938
- <div part="messages"></div>
939
- </main>
940
- `;
941
- }
942
-
943
- @part('user-info')
944
- renderUserInfo() {
945
- return `
946
- <h1>${this.user.name}</h1>
947
- <button id="refresh-user">Refresh</button>
948
- `;
949
- }
950
-
951
- @part('stats')
952
- renderStats() {
953
- return `
954
- <div class="stats">
955
- <span>Views: ${this.user.stats.views}</span>
956
- <span>Likes: ${this.user.stats.likes}</span>
957
- </div>
958
- `;
959
- }
628
+ // layouts/app-shell.ts
629
+ @layout('app-shell')
630
+ class AppShell extends HTMLElement implements Layout {
631
+ private placards: Placard[] = [];
632
+ private currentRoute = '';
960
633
 
961
- @part('notifications', { throttle: 300 })
962
- renderNotifications() {
963
- return `
964
- <h3>Notifications (${this.notifications.length})</h3>
965
- ${this.notifications.map(n => `<div>${n}</div>`).join('')}
634
+ @render()
635
+ renderContent() {
636
+ return html`
637
+ <header>
638
+ <nav>
639
+ ${this.placards
640
+ .filter(p => p.show !== false)
641
+ .map(p => html`
642
+ <a href="#/${p.name}"
643
+ class="${this.currentRoute === p.name ? 'active' : ''}">
644
+ ${p.icon} ${p.title}
645
+ </a>
646
+ `)}
647
+ </nav>
648
+ </header>
649
+ <main><slot name="page"></slot></main>
966
650
  `;
967
651
  }
968
652
 
969
- @part('messages')
970
- async renderMessages() {
971
- if (this.messages.length === 0) {
972
- return '<p>No messages</p>';
973
- }
974
- return this.messages.map(m => `<div class="message">${m}</div>`).join('');
975
- }
976
-
977
- // Update specific parts without re-rendering everything
978
- updateUserName(newName) {
979
- this.user.name = newName;
980
- this.renderUserInfo(); // Only re-renders the header
981
- }
982
-
983
- incrementViews() {
984
- this.user.stats.views++;
985
- this.renderStats(); // Only re-renders the stats section
986
- }
987
-
988
- addNotification(notification) {
989
- this.notifications.unshift(notification);
990
- this.renderNotifications(); // Only re-renders notifications
991
- }
992
-
993
- @on('click', '#refresh-user')
994
- async handleRefreshUser() {
995
- // Simulate API call
996
- const userData = await this.fetchUserData();
997
- this.user = userData;
998
- this.renderUserInfo(); // Update just the user info part
999
- this.renderStats(); // Update just the stats part
653
+ // Called when route changes
654
+ update(appContext, placards, currentRoute, routeParams) {
655
+ this.placards = placards;
656
+ this.currentRoute = currentRoute;
657
+ // Property changes trigger re-render
1000
658
  }
1001
659
  }
1002
- ```
1003
-
1004
- **Benefits of `@part`:**
1005
- - **Performance** - Update only what changed instead of re-rendering entire templates
1006
- - **Granular Control** - Target specific sections for updates
1007
- - **Complex UIs** - Perfect for dashboards, lists, or components with independent sections
1008
- - **Async Support** - Part methods can be async for data fetching
1009
- - **Throttle/Debounce** - Control render frequency to optimize performance
1010
-
1011
- ### Performance Options
1012
660
 
1013
- The `@part` decorator supports throttle and debounce options to optimize render performance:
1014
-
1015
- ```typescript
1016
- // Throttle: Limit renders to once per 300ms
1017
- @part('notifications', { throttle: 300 })
1018
- renderNotifications() { /* ... */ }
1019
-
1020
- // Debounce: Delay render until 150ms after last call
1021
- @part('search-results', { debounce: 150 })
1022
- renderSearchResults() { /* ... */ }
661
+ // main.ts - configure router with layout
662
+ const { page, initialize } = Router({
663
+ target: '#app',
664
+ layout: 'app-shell'
665
+ });
1023
666
  ```
1024
667
 
1025
- - **Throttle** - Limits renders to a maximum frequency (e.g., once every 300ms)
1026
- - **Debounce** - Delays renders until after calls stop for the specified time
1027
-
1028
- The `@part` decorator is ideal when you have components with multiple independent sections that update at different frequencies or from different data sources.
1029
-
1030
- ## Lifecycle Callbacks
668
+ Pages render inside `<slot name="page"></slot>`. Layout persists, only page content swaps.
1031
669
 
1032
- Snice provides decorators for advanced DOM lifecycle events that go beyond basic connected/disconnected callbacks:
670
+ ## Placards
1033
671
 
1034
- ### @moved Decorator
1035
-
1036
- The `@moved` decorator runs methods when an element is moved within the DOM using `Element.moveBefore()`. This is useful for handling position changes without full disconnection/reconnection:
672
+ Page metadata that layouts use to build navigation, breadcrumbs, and help systems:
1037
673
 
1038
674
  ```typescript
1039
- @element('my-element')
1040
- class MyElement extends HTMLElement {
1041
- @moved()
1042
- onElementMoved() {
1043
- console.log('Element moved to new position');
1044
- this.updatePosition();
1045
- }
1046
-
1047
- // With timing options
1048
- @moved({ debounce: 100 })
1049
- onMovedDebounced() {
1050
- // Only called once after moves stop for 100ms
1051
- this.recalculateLayout();
1052
- }
675
+ // pages/dashboard-page.ts
676
+ const placard: Placard<AppContext> = {
677
+ name: 'dashboard',
678
+ title: 'Dashboard',
679
+ icon: '📊',
680
+ order: 1,
681
+ searchTerms: ['home', 'overview', 'stats'],
682
+ hotkeys: ['ctrl+d'],
683
+ visibleOn: [isAuthenticated]
684
+ };
1053
685
 
1054
- @moved({ throttle: 500 })
1055
- onMovedThrottled() {
1056
- // Called at most once every 500ms during rapid moves
1057
- this.optimizePerformance();
1058
- }
1059
- }
686
+ @page({
687
+ tag: 'dashboard-page',
688
+ routes: ['/dashboard'],
689
+ placard: placard
690
+ })
691
+ class DashboardPage extends HTMLElement { }
1060
692
  ```
1061
693
 
1062
- ### @adopted Decorator
694
+ **Features:**
695
+ - **Navigation** - `title`, `icon`, `order`, `show`
696
+ - **Hierarchy** - `parent`, `group`, `breadcrumbs`
697
+ - **Discovery** - `searchTerms`, `hotkeys`, `tooltip`
698
+ - **Visibility** - `visibleOn` guards control who sees what
1063
699
 
1064
- The `@adopted` decorator runs methods when an element is moved to a new document (like iframes or document fragments):
700
+ Layouts receive placard data in `update()` and auto-build navigation. See [docs](./docs/placards.md).
1065
701
 
1066
- ```typescript
1067
- @element('portable-element')
1068
- class PortableElement extends HTMLElement {
1069
- @adopted()
1070
- onAdoptedToNewDocument() {
1071
- console.log('Element moved to new document');
1072
- this.updateDocumentReferences();
1073
- }
702
+ ## Migrating from v2.x
1074
703
 
1075
- // With timing options
1076
- @adopted({ debounce: 200 })
1077
- onAdoptedDebounced() {
1078
- // Debounced for performance during rapid document moves
1079
- this.reinitializeForNewContext();
1080
- }
1081
- }
1082
- ```
704
+ v3.0.0 introduces template-based rendering with differential updates. Key changes:
1083
705
 
1084
- ### Timing Options
706
+ - **Use `@render()` instead of `html()` method**
707
+ Return `html\`...\`` tagged template instead of string
1085
708
 
1086
- Both decorators support the same timing options as `@part`:
709
+ - **Use `@styles()` instead of `css()` method**
710
+ Return `css\`...\`` tagged template instead of string
1087
711
 
1088
- - **`debounce`** - Delays execution until after calls stop for the specified milliseconds
1089
- - **`throttle`** - Limits execution to once per specified milliseconds
712
+ - **`@on()` decorator available**
713
+ Works in both elements AND controllers with full event delegation, keyboard modifiers, and debounce/throttle support.
714
+ Template event syntax (`@click=${handler}`) is also available as an alternative.
1090
715
 
1091
- ```typescript
1092
- // Examples of timing control
1093
- @moved({ debounce: 150 }) // Wait 150ms after moves stop
1094
- @adopted({ throttle: 300 }) // Maximum once per 300ms
1095
- ```
716
+ - **`@part` decorator removed**
717
+ Differential rendering makes selective re-rendering unnecessary
1096
718
 
1097
- These lifecycle callbacks are perfect for:
1098
- - **Performance optimization** during rapid DOM changes
1099
- - **Layout recalculation** when elements move
1100
- - **Context updates** when elements move between documents
1101
- - **Resource cleanup/setup** during document adoption
719
+ See [Migration Guide](./docs/migration-v2-to-v3.md) for detailed migration guide.
1102
720
 
1103
721
  ## Documentation
1104
722
 
1105
723
  - [Elements API](./docs/elements.md) - Complete guide to creating elements with properties, queries, and styling
1106
724
  - [Controllers API](./docs/controllers.md) - Data fetching, business logic, and controller patterns
725
+ - [Routing API](./docs/routing.md) - Single-page application routing with transitions
726
+ - [Placards API](./docs/placards.md) - Rich page metadata for dynamic navigation and discovery
1107
727
  - [Events API](./docs/events.md) - Event handling, dispatching, and custom events
1108
728
  - [Request/Response API](./docs/request-response.md) - Bidirectional communication between elements and controllers
1109
- - [Routing API](./docs/routing.md) - Single-page application routing with transitions
1110
729
  - [Observe API](./docs/observe.md) - Lifecycle-managed observers for external changes
1111
730
 
1112
731
  ## License