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/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Snice
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,769 @@ cd my-app
12
12
  npm run dev
13
13
  ```
14
14
 
15
- ## Core Philosophy: Imperative, Not Reactive
15
+ ## Philosophy
16
16
 
17
- Snice takes an **imperative approach** to web components. Unlike reactive frameworks that automatically re-render when data changes, Snice components:
17
+ Without structure, you'll end up with auth checks mixed in rendering logic, business rules duplicated across components, and data fetching scattered in event handlers. **Snice provides constructs that guide you to put code where it belongs, keeping you productive as your app grows.**
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
+ - **Pages fetch data and assemble UI** - They understand what the user wants to do
20
+ - **Elements handle visuals only** - They don't know or care about business logic
21
+ - **Controllers let you swap behavior** - Same UI, different data sources or logic
22
+ - **Cross-cutting concerns stay separate** - Auth, routing, and global state don't leak into your components
23
23
 
24
- This approach gives you direct control over DOM updates without hidden complexity or automatic re-renders.
24
+ Yes, global state is bad! but you will have a little always, and it should be managed well.
25
+ Usually we see auth/principals, themes, and localization as global state examples.
25
26
 
26
- ## The Snice Way: Elements + Controllers
27
+ Each piece hints at where your code should live, preventing the mess that kills velocity on larger teams.
27
28
 
28
- Snice separates UI from data: **elements handle UI, controllers handle behavior and data**.
29
+ ## The Tools
29
30
 
30
- ```typescript
31
- import { element, controller, property, query } from 'snice';
31
+ Snice provides decorators and utilities that map directly to these architectural concerns:
32
32
 
33
- export interface IUserCard extends HTMLElement {
34
- userId: string;
35
- showUser(user: any): void;
36
- }
37
33
 
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
- }
34
+ ### Basic Building Blocks
35
+ ```typescript
64
36
 
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
- ```
37
+ @page({ tag: 'user-profile-page', routes: ['/users/:userId'], guards: [isAuthenticated] })
38
+ class UserProfilePage extends HTMLElement { ... }
80
39
 
81
- Connect them in HTML:
82
- ```html
83
- <user-card user-id="123" controller="user-loader"></user-card>
84
- ```
40
+ @element('user-stats')
41
+ class UserStats extends HTMLElement { ... }
85
42
 
86
- That's it. Element renders UI, controller fetches data, they communicate through method calls, events, and request/response channels.
43
+ @controller('real-time-user-loader')
44
+ class RealTimeUserLoader { ... }
87
45
 
88
- ## Core Concepts
46
+ // And within these classes, use decorators like:
47
+ @property() name = 'default';
48
+ @render() fn() { return html`...`; }
49
+ @styles() fn() { return css`...`; }
50
+ @ready() async fn() { ... }
51
+ @dispose() fn() { ... }
52
+ @watch('name') fn(oldVal, newVal) { ... }
53
+ @query('input') input!: HTMLInputElement;
54
+ @queryAll('.item') items!: NodeListOf<HTMLElement>;
55
+ @on('click', 'button') fn(e: Event) { ... }
56
+ @dispatch('value-changed') fn(val: string) => Event Detail
57
+ @context() fn(ctx: Context) { ... }
58
+ @request('user') fn(): () => Request;
59
+ @respond('user') fn(req) => Response;
60
+ ```
89
61
 
90
- Snice provides a clear separation of concerns through decorators:
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
+ // sample-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
+ type: 'hash'
82
+ });
114
83
 
115
- ```typescript
116
- import { element } from 'snice';
84
+ // Any page can access context
85
+ @page({ tag: 'dashboard-page', routes: ['/dashboard'] })
86
+ class DashboardPage extends HTMLElement {
87
+ private appContext?: AppContext;
117
88
 
118
- @element('my-button')
119
- class MyButton extends HTMLElement {
120
- html() {
121
- return `<button>Click me</button>`;
89
+ @context()
90
+ handleContext(ctx: Context) {
91
+ this.appContext = ctx.application;
92
+ const user = this.getUser();
122
93
  }
94
+ // ...
123
95
  }
124
96
  ```
125
97
 
126
- That's it. Your component renders when added to the DOM:
127
-
128
- ```html
129
- <my-button></my-button>
130
- ```
98
+ ### 2. Pages: Orchestrating Intent
131
99
 
132
- ## The Imperative Way
100
+ ```typescript
101
+ // pages/user-profile-page.ts
102
+ @page({ tag: 'user-profile-page', routes: ['/users/:userId'] })
103
+ class UserProfilePage extends HTMLElement {
104
+ @property()
105
+ userId = ''; // From URL parameter
133
106
 
134
- In Snice, updates are explicit. Components expose methods that controllers or other components call to update state:
107
+ @property({ type: Object })
108
+ user = null;
135
109
 
136
- ```typescript
137
- import { element, property, query } from 'snice';
110
+ @property({ type: Object })
111
+ userStats = null;
138
112
 
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>
113
+ @ready()
114
+ async loadUserData() {
115
+ // Pages handle data fetching, elements just display
116
+ const [user, stats] = await Promise.all([
117
+ fetch(`/api/users/${this.userId}`).then(r => r.json()),
118
+ fetch(`/api/users/${this.userId}/stats`).then(r => r.json())
119
+ ]);
120
+ this.user = user;
121
+ this.userStats = stats;
122
+ }
123
+
124
+ @render()
125
+ renderContent() {
126
+ return html`
127
+ <page-header .user=${this.user}></page-header>
128
+ <user-stats .stats=${this.userStats}></user-stats>
129
+ <user-activity .userId=${this.userId}></user-activity>
157
130
  `;
158
131
  }
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
132
  }
175
133
  ```
176
134
 
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.
135
+ ### 3. Elements: Pure Presentation
186
136
 
187
137
  ```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';
138
+ // elements/user-stats.ts
139
+ @element('user-stats')
140
+ class UserStats extends HTMLElement {
141
+ @property({ type: Object })
142
+ stats = null;
197
143
 
198
- @property({ type: Boolean })
199
- verified = false;
144
+ @render()
145
+ renderContent() {
146
+ if (!this.stats) return html`<div>Loading...</div>`;
200
147
 
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>' : ''}
148
+ return html`
149
+ <div class="stats">
150
+ <div class="stat">
151
+ <span class="label">Views</span>
152
+ <span class="value">${this.stats.views}</span>
153
+ </div>
154
+ <div class="stat">
155
+ <span class="label">Followers</span>
156
+ <span class="value">${this.stats.followers}</span>
157
+ </div>
208
158
  </div>
209
159
  `;
210
160
  }
161
+
162
+ @styles()
163
+ statsStyles() {
164
+ return css`
165
+ .stats { display: flex; gap: 2rem; }
166
+ .stat { text-align: center; }
167
+ `;
168
+ }
211
169
  }
212
- ```
213
170
 
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>
171
+ // Usage in parent page (which handles data fetching):
172
+ // <user-stats .stats=${this.userStats}></user-stats>
224
173
  ```
225
174
 
226
- For arrays of basic types, use `SimpleArray` for safe reflection:
175
+ ### 4. Controllers: Behavior Management
227
176
 
228
177
  ```typescript
229
- import { element, property, SimpleArray } from 'snice';
178
+ // controllers/real-time-user-loader.ts
179
+ @controller('real-time-user-loader')
180
+ class RealTimeUserLoader {
181
+ async attach(element: IUserList) {
182
+ this.socket = new WebSocket('/api/users/stream');
183
+ this.socket.onmessage = (e) => {
184
+ element.setUsers(JSON.parse(e.data));
185
+ };
186
+ }
187
+ // ...
188
+ }
230
189
 
231
- @element('tag-list')
232
- class TagList extends HTMLElement {
233
- @property({ type: SimpleArray })
234
- tags = ['javascript', 'typescript', 'web'];
190
+ // controllers/cached-user-loader.ts
191
+ @controller('cached-user-loader')
192
+ class CachedUserLoader {
193
+ async attach(element: IUserList) {
194
+ const cached = localStorage.getItem('users');
195
+ if (cached) element.setUsers(JSON.parse(cached));
196
+ }
197
+ // ...
198
+ }
199
+
200
+ // elements/user-list.ts - stays the same
201
+ @element('user-list')
202
+ class UserList extends HTMLElement {
203
+ setUsers(users: User[]) {
204
+ this.users = users;
205
+ // ...
206
+ }
235
207
 
236
- html() {
237
- return `<div>${this.tags.join(', ')}</div>`;
208
+ @render()
209
+ renderContent() {
210
+ return html`
211
+ <ul>${this.users.map(u => html`<li>${u.name}</li>`)}</ul>
212
+ `;
238
213
  }
239
214
  }
240
215
  ```
241
216
 
217
+ Usage - swap behavior without touching presentation:
218
+
242
219
  ```html
243
- <tag-list tags="react,vue,angular"></tag-list>
220
+ <user-list controller="real-time-user-loader"></user-list>
221
+ <user-list controller="cached-user-loader"></user-list>
244
222
  ```
245
223
 
246
- ## Watching Property Changes
224
+ ## Key Features
247
225
 
248
- Use `@watch` to imperatively update DOM when properties change:
226
+ **Differential Rendering** - Only updates changed parts of the DOM, not entire components
249
227
 
250
- ```typescript
251
- import { element, property, watch, query } from 'snice';
228
+ **Auto-Rendering** - Components automatically re-render when properties change
252
229
 
253
- @element('theme-toggle')
254
- class ThemeToggle extends HTMLElement {
255
- @property()
256
- theme: 'light' | 'dark' = 'light';
230
+ **Template Syntax** - Clean `html\`...\`` and `css\`...\`` tagged templates
257
231
 
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
- ```
232
+ **Type Safety** - Full TypeScript support with decorator-based APIs
280
233
 
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
234
+ **Zero Dependencies** - No external runtime dependencies
287
235
 
288
- You can watch multiple properties with a single decorator:
236
+ **Standards-Based** - Built on web components, works with any framework
289
237
 
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
- ```
238
+ ## Core APIs
298
239
 
299
- Watch all property changes with the wildcard:
240
+ ### Class Decorators
300
241
 
242
+ **`@element('tag-name')`** - Define reusable UI components
301
243
  ```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
- }
244
+ @element('my-button')
245
+ class MyButton extends HTMLElement { }
307
246
  ```
308
247
 
309
- ## Queries
310
-
311
- Query single elements with `@query`:
312
-
248
+ **`@page({ tag, routes })`** - Define routable pages
313
249
  ```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
- }
250
+ @page({ tag: 'home-page', routes: ['/'] })
251
+ class HomePage extends HTMLElement { }
252
+ ```
324
253
 
325
- getValue() {
326
- return this.input.value;
327
- }
254
+ **`@controller('controller-name')`** - Define behavior modules
255
+ ```typescript
256
+ @controller('data-loader')
257
+ class DataLoader {
258
+ async attach(element) { }
259
+ async detach(element) { }
328
260
  }
329
261
  ```
330
262
 
331
- Query multiple elements with `@queryAll`:
263
+ ### Rendering
332
264
 
265
+ **`@render(options?)`** - Define component template (auto re-renders on property changes)
333
266
  ```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
- }
267
+ @render()
268
+ renderContent() {
269
+ return html`<div>${this.data}</div>`;
354
270
  }
355
271
  ```
356
272
 
357
- ## Events
358
-
359
- Listen for events with `@on`:
360
-
273
+ **`@styles()`** - Define scoped styles
361
274
  ```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
- }
275
+ @styles()
276
+ componentStyles() {
277
+ return css`.container { padding: 1rem; }`;
392
278
  }
393
279
  ```
394
280
 
395
- ## Dispatching Events
396
-
397
- Automatically dispatch custom events with `@dispatch`:
281
+ ### Properties & State
398
282
 
283
+ **`@property(options?)`** - Reactive properties that sync with attributes
399
284
  ```typescript
400
- import { element, dispatch, on, query } from 'snice';
285
+ @property()
286
+ name = 'default';
401
287
 
402
- @element('toggle-switch')
403
- class ToggleSwitch extends HTMLElement {
404
- private isOn = false;
405
-
406
- @query('.toggle')
407
- toggleButton!: HTMLElement;
288
+ @property({ type: Boolean })
289
+ enabled = false;
290
+ ```
408
291
 
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
- }
292
+ **`@watch(...propertyNames)`** - React to property changes
293
+ ```typescript
294
+ @watch('name')
295
+ onNameChange(oldVal, newVal) {
296
+ console.log(`Name changed from ${oldVal} to ${newVal}`);
420
297
  }
421
298
  ```
422
299
 
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
300
+ ### Lifecycle
428
301
 
302
+ **`@ready()`** - Runs after initial render completes
429
303
  ```typescript
430
- // With options from EventInit
431
- @dispatch('my-event', { bubbles: false, cancelable: true })
432
-
433
- // Don't dispatch if method returns undefined
434
- @dispatch('maybe-data', { dispatchOnUndefined: false })
304
+ @ready()
305
+ async initialize() {
306
+ // Fetch data, set up listeners, etc.
307
+ }
435
308
  ```
436
309
 
437
- ## Styling
438
-
310
+ **`@dispose()`** - Runs when element is removed from DOM
439
311
  ```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
- }
312
+ @dispose()
313
+ cleanup() {
314
+ // Clean up listeners, close connections, etc.
455
315
  }
456
316
  ```
457
317
 
458
- CSS is automatically scoped to your component.
318
+ ### DOM Queries
459
319
 
460
- ## Routing
320
+ **`@query(selector)`** - Query single element from shadow DOM
321
+ ```typescript
322
+ @query('input')
323
+ input!: HTMLInputElement;
324
+ ```
461
325
 
326
+ **`@queryAll(selector)`** - Query multiple elements from shadow DOM
462
327
  ```typescript
463
- import { Router } from 'snice';
328
+ @queryAll('.item')
329
+ items!: NodeListOf<HTMLElement>;
330
+ ```
464
331
 
465
- const router = Router({ target: '#app', type: 'hash' });
332
+ ### Events & Communication
466
333
 
467
- const { page, navigate, initialize } = router;
334
+ **Template Events** - Handle events directly in templates (with keyboard modifiers!)
335
+ ```typescript
336
+ html`
337
+ <button @click=${this.handleClick}>Click</button>
338
+ <input @keydown:Enter=${this.submit} />
339
+ <input @keydown:ctrl+s=${this.save} />
340
+ `
341
+ ```
468
342
 
469
- @page({ tag: 'home-page', routes: ['/'] })
470
- class HomePage extends HTMLElement {
471
- html() {
472
- return `<h1>Home</h1>`;
473
- }
343
+ **`@on` Decorator** - Event delegation with selectors
344
+ ```typescript
345
+ // Works in both elements AND controllers
346
+ @on('click', 'button') // Event delegation
347
+ handleClick(e: Event) {
348
+ console.log('Button clicked!');
474
349
  }
475
350
 
476
- @page({ tag: 'about-page', routes: ['/about'] })
477
- class AboutPage extends HTMLElement {
478
- html() {
479
- return `<h1>About</h1>`;
480
- }
351
+ @on('keydown:Enter', 'input') // Keyboard modifiers
352
+ handleEnter(e: KeyboardEvent) {
353
+ this.submit();
481
354
  }
482
355
 
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
- }
356
+ @on('input', 'input', { debounce: 300 }) // Debounce support
357
+ handleInput(e: Event) {
358
+ this.search((e.target as HTMLInputElement).value);
494
359
  }
495
-
496
- // Start the router
497
- initialize();
498
-
499
- // Navigate programmatically
500
- navigate('/about');
501
- navigate('/users/123'); // Sets userId="123" on UserPage
502
360
  ```
503
361
 
504
- ### Route Guards
505
-
506
- Protect routes with guard functions that control access:
507
-
362
+ **`@dispatch(eventName)`** - Auto-dispatch custom events after method execution
508
363
  ```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
- });
517
-
518
- const { page, navigate, initialize } = router;
364
+ @dispatch('value-changed')
365
+ setValue(val: string) {
366
+ this.value = val;
367
+ return { value: val }; // Event detail
368
+ }
369
+ ```
519
370
 
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';
371
+ ### Global State
523
372
 
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
- }
373
+ **`@context(options?)`** - Receive router context updates (global state)
374
+ ```typescript
375
+ // Method decorator that receives context updates
376
+ @context()
377
+ handleContext(ctx: Context) {
378
+ this.appContext = ctx.application;
379
+ this.requestRender();
530
380
  }
531
381
 
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
- }
382
+ // With timing options
383
+ @context({ debounce: 300 })
384
+ handleContextDebounced(ctx: Context) {
385
+ // Called after 300ms of no updates
538
386
  }
539
387
 
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
- }
388
+ @context({ throttle: 100 })
389
+ handleContextThrottled(ctx: Context) {
390
+ // Called at most once per 100ms
566
391
  }
567
392
 
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
- }
393
+ @context({ once: true })
394
+ handleContextOnce(ctx: Context) {
395
+ // Called only once, then unregisters
576
396
  }
397
+ ```
577
398
 
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
- }
399
+ **Context Object Structure:**
400
+ ```typescript
401
+ interface Context {
402
+ application: AppContext; // Your router context
403
+ navigation: {
404
+ placards: Placard[]; // Page metadata
405
+ route: string; // Current route
406
+ params: Record<string, string>; // Route parameters
407
+ };
408
+ update(): void; // Notify all subscribers
588
409
  }
589
-
590
- initialize();
591
410
  ```
592
411
 
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)
412
+ **Triggering Context Updates:**
597
413
 
598
- ## Controllers (Data Fetching)
599
-
600
- Controllers handle server communication separately from visual components:
414
+ When you modify the application context, call `update()` to notify all subscribers:
601
415
 
602
416
  ```typescript
603
- import { controller, element } from 'snice';
604
-
605
- interface IUserElement extends HTMLElement {
606
- setUsers(users: any[]): void;
607
- }
417
+ @page({ tag: 'login-page', routes: ['/login'] })
418
+ class LoginPage extends HTMLElement {
419
+ private ctx?: Context<AppContext>;
608
420
 
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);
421
+ @context()
422
+ handleContext(ctx: Context<AppContext>) {
423
+ this.ctx = ctx;
424
+ this.requestRender();
617
425
  }
618
426
 
619
- async detach(element: IUserElement) { /* Cleanup */ }
620
- }
621
-
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
- }
427
+ login(user: User) {
428
+ // Modify the application context
429
+ this.ctx!.application.setUser(user);
633
430
 
634
- setUsers(users: any[]) {
635
- this.users = users;
636
- if (this.shadowRoot) {
637
- this.shadowRoot.innerHTML = this.html();
638
- }
431
+ // Notify all @context subscribers
432
+ this.ctx!.update();
639
433
  }
640
434
  }
641
435
  ```
642
436
 
643
- Use it:
437
+ **Note:** The router calls `update()` automatically during navigation. Only call it manually when you change application state (like login/logout, theme changes, etc.).
644
438
 
645
- ```html
646
- <user-list controller="user-controller"></user-list>
647
- ```
439
+ ### Request/Response
648
440
 
649
- ## Request/Response
441
+ For the few cases where elements need to request data from controllers (like fetching user info or current state), Snice provides a request/response pattern:
650
442
 
651
- Bidirectional communication between elements and controllers:
443
+ **`@request(channel)`** - Make requests to controllers from elements
444
+ **`@respond(channel)`** - Respond to requests from elements in controllers
652
445
 
653
- ```typescript
654
- import { element, request, type Response } from 'snice';
446
+ This pattern is useful when:
447
+ - Elements need to fetch data without direct controller access
448
+ - You want to keep elements decoupled from specific controller implementations
449
+ - Multiple elements may request the same data
655
450
 
656
- // Element makes request, controller responds
657
- @element('user-profile')
658
- class UserProfile extends HTMLElement {
451
+ **Example:**
659
452
 
660
- @request('fetch-user')
661
- async *getUser(): Response<{ name: string; email: string }> {
662
- const user = await (yield { userId: 123 });
663
- return user;
664
- }
453
+ ```typescript
454
+ // Controller responds to requests
455
+ @element('app-controller')
456
+ class AppController extends HTMLElement {
457
+ private currentUser = { name: 'Alice', role: 'admin' };
665
458
 
666
- @ready()
667
- async load() {
668
- const userData = await this.getUser();
669
- this.displayUser(userData);
459
+ @respond('user')
460
+ getUserData() {
461
+ return this.currentUser;
670
462
  }
671
-
672
463
  }
673
464
 
674
- @controller('user-controller')
675
- class UserController {
465
+ // Element makes requests
466
+ @element('user-badge')
467
+ class UserBadge extends HTMLElement {
468
+ @request('user')
469
+ getUser!: () => any;
676
470
 
677
- @respond('fetch-user')
678
- async handleFetchUser(request: { userId: number }) {
679
- const response = await fetch(`/api/users/${request.userId}`);
680
- return response.json();
471
+ @ready()
472
+ init() {
473
+ const user = this.getUser();
474
+ console.log('Current user:', user);
681
475
  }
682
476
 
477
+ @render()
478
+ renderContent() {
479
+ const user = this.getUser();
480
+ return html`<div>Welcome, ${user.name}!</div>`;
481
+ }
683
482
  }
684
483
  ```
685
484
 
686
- ## Layouts
485
+ **Usage:**
486
+ ```html
487
+ <app-controller>
488
+ <user-badge></user-badge>
489
+ </app-controller>
490
+ ```
491
+
492
+ See [Request/Response documentation](./docs/request-response.md) for details.
687
493
 
688
- Wrap your pages in shared layout components for consistent navigation, headers, and footers across your application.
494
+ ## Template Syntax
689
495
 
690
- ### Basic Layout Usage
496
+ ### Auto-Rendering with Differential Updates
691
497
 
692
498
  ```typescript
693
- import { Router, layout, page } from 'snice';
499
+ @element('counter-display')
500
+ class CounterDisplay extends HTMLElement {
501
+ @property({ type: Number })
502
+ count = 0;
694
503
 
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>
504
+ @render()
505
+ renderContent() {
506
+ return html`
507
+ <div class="counter">
508
+ <span class="count">${this.count}</span>
509
+ <button @click=${this.increment}>+</button>
510
+ </div>
710
511
  `;
711
512
  }
712
- }
713
-
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
513
 
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>`;
514
+ @styles()
515
+ counterStyles() {
516
+ return css`.counter { display: flex; gap: 1rem; }`;
728
517
  }
729
- }
730
518
 
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>`;
519
+ increment() {
520
+ this.count++;
521
+ // Auto re-renders! Only <span class="count"> updates
736
522
  }
737
523
  }
738
-
739
- initialize();
740
524
  ```
741
525
 
742
- ### Layout Features
526
+ **Key Points:**
527
+ - Properties trigger automatic re-renders
528
+ - Only changed parts update (differential rendering)
529
+ - Event handlers: `@click=${this.method}`
530
+ - Batched updates (multiple changes = single render)
743
531
 
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
532
+ ### Property Binding
749
533
 
750
- ## Router Context
534
+ Use `.property=${value}` to set element properties directly:
751
535
 
752
- Access router context in page components, nested elements, and controllers using the `@context` decorator.
536
+ ```typescript
537
+ html`
538
+ <input .value=${this.text} />
539
+ <custom-element .complexData=${this.dataObject}></custom-element>
540
+ `
541
+ ```
753
542
 
754
- ### ⚠️ Important Warning About Global State
543
+ ### Boolean Attributes
755
544
 
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.
545
+ Use `?attribute=${boolean}` for boolean attributes:
757
546
 
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
547
+ ```typescript
548
+ html`
549
+ <button ?disabled=${this.isLoading}>Submit</button>
550
+ <input type="checkbox" ?checked=${this.isChecked} />
551
+ `
552
+ ```
764
553
 
765
- ### Basic Usage in Pages
554
+ ### Conditionals
766
555
 
767
556
  ```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
- }
557
+ // Ternary operator
558
+ html`
559
+ ${this.isLoggedIn
560
+ ? html`<span>Welcome!</span>`
561
+ : html`<a href="/login">Login</a>`
562
+ }
563
+ `
564
+
565
+ // <if> conditional element
566
+ html`
567
+ <if ${this.isLoggedIn}>
568
+ <span>Welcome, ${this.user.name}!</span>
569
+ <button @click=${this.logout}>Logout</button>
570
+ </if>
571
+ <if ${!this.isLoggedIn}>
572
+ <a href="/login">Login</a>
573
+ </if>
574
+ `
575
+
576
+ // <case>/<when>/<default> for multiple branches
577
+ html`
578
+ <case ${this.status}>
579
+ <when value="loading">
580
+ <span>Loading...</span>
581
+ </when>
582
+ <when value="success">
583
+ <span>Success!</span>
584
+ </when>
585
+ <when value="error">
586
+ <span>Error occurred</span>
587
+ </when>
588
+ <default>
589
+ <span>Unknown status</span>
590
+ </default>
591
+ </case>
592
+ `
593
+ ```
594
+
595
+ ### Lists
780
596
 
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
- });
788
-
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
- }
597
+ ```typescript
598
+ html`
599
+ <ul>
600
+ ${this.items.map(item => html`
601
+ <li @click=${() => this.select(item.id)}>${item.name}</li>
602
+ `)}
603
+ </ul>
604
+ `
807
605
  ```
808
606
 
809
- ### Context in Nested Elements
810
-
811
- Nested elements within pages can also access context:
607
+ ### Keyboard Shortcuts
812
608
 
813
609
  ```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
- }
610
+ html`
611
+ <input @keydown.enter=${this.submit} />
612
+ <input @keydown.ctrl+s=${this.save} />
613
+ <input @keydown.ctrl+shift+s=${this.saveAs} />
614
+ <input @keydown.escape=${this.cancel} />
615
+ <input @keydown.~enter=${this.submitAny} />
616
+ `
839
617
  ```
840
618
 
841
- ### Context in Controllers
619
+ Keyboard syntax:
620
+ - `@keydown.enter` - Plain Enter (no modifiers)
621
+ - `@keydown.ctrl+s` - Ctrl+S combination
622
+ - `@keydown.~enter` - Enter with any modifiers
623
+ - `@keydown.down` - Arrow keys (up, down, left, right)
624
+ - `@keydown.escape` - Escape key
842
625
 
843
- Controllers attached to page elements automatically acquire context:
626
+ ## Router
844
627
 
845
628
  ```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
- }
629
+ // main.ts
630
+ const { page, navigate, initialize } = Router({
631
+ target: '#app',
632
+ context: new AppContext()
633
+ });
862
634
 
863
- @page({ tag: 'admin-page', routes: ['/admin'] })
864
- class AdminPage extends HTMLElement {
865
- html() {
866
- return `<div controller="nav-controller">Admin Panel</div>`;
635
+ // pages/home-page.ts
636
+ @page({ tag: 'home-page', routes: ['/'] })
637
+ class HomePage extends HTMLElement {
638
+ @render()
639
+ renderContent() {
640
+ return html`<h1>Home</h1>`;
867
641
  }
868
642
  }
869
- ```
870
643
 
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
644
+ // pages/user-page.ts
645
+ @page({ tag: 'user-page', routes: ['/users/:userId'] })
646
+ class UserPage extends HTMLElement {
647
+ @property()
648
+ userId = ''; // Auto-populated from URL
649
+ // ...
650
+ }
877
651
 
878
- **Remember:** With great power comes great responsibility. Global state is dangerous - use it wisely and sparingly.
652
+ // main.ts
653
+ initialize();
654
+ navigate('/users/123');
655
+ ```
879
656
 
880
- ## Observing External Changes
657
+ ### Route Guards
881
658
 
882
- The `@observe` decorator provides lifecycle-managed observation of external changes like viewport intersection, element resize, media queries, and DOM mutations:
659
+ Protect routes with guard functions:
883
660
 
884
661
  ```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
- }
662
+ const isAuthenticated: Guard<AppContext> = (ctx) => ctx.getUser() !== null;
895
663
 
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
- }
906
-
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
- }
664
+ @page({
665
+ tag: 'dashboard-page',
666
+ routes: ['/dashboard'],
667
+ guards: isAuthenticated
668
+ })
669
+ class DashboardPage extends HTMLElement { }
913
670
  ```
914
671
 
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
672
+ ## Layouts
918
673
 
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:
674
+ Layouts wrap pages with shared UI and dynamically build navigation from page metadata:
920
675
 
921
676
  ```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
- }
677
+ // layouts/app-shell.ts
678
+ @layout('app-shell')
679
+ class AppShell extends HTMLElement implements Layout {
680
+ private placards: Placard[] = [];
681
+ private currentRoute = '';
960
682
 
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('')}
683
+ @render()
684
+ renderContent() {
685
+ return html`
686
+ <header>
687
+ <nav>
688
+ ${this.placards
689
+ .filter(p => p.show !== false)
690
+ .map(p => html`
691
+ <a href="#/${p.name}"
692
+ class="${this.currentRoute === p.name ? 'active' : ''}">
693
+ ${p.icon} ${p.title}
694
+ </a>
695
+ `)}
696
+ </nav>
697
+ </header>
698
+ <main><slot name="page"></slot></main>
966
699
  `;
967
700
  }
968
701
 
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
702
+ // Called when route changes
703
+ update(appContext, placards, currentRoute, routeParams) {
704
+ this.placards = placards;
705
+ this.currentRoute = currentRoute;
706
+ // Property changes trigger re-render
1000
707
  }
1001
708
  }
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
709
 
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() { /* ... */ }
710
+ // main.ts - configure router with layout
711
+ const { page, initialize } = Router({
712
+ target: '#app',
713
+ layout: 'app-shell'
714
+ });
1023
715
  ```
1024
716
 
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
717
+ Pages render inside `<slot name="page"></slot>`. Layout persists, only page content swaps.
1031
718
 
1032
- Snice provides decorators for advanced DOM lifecycle events that go beyond basic connected/disconnected callbacks:
719
+ ## Placards
1033
720
 
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:
721
+ Page metadata that layouts use to build navigation, breadcrumbs, and help systems:
1037
722
 
1038
723
  ```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
- }
724
+ // pages/dashboard-page.ts
725
+ const placard: Placard<AppContext> = {
726
+ name: 'dashboard',
727
+ title: 'Dashboard',
728
+ icon: '📊',
729
+ order: 1,
730
+ searchTerms: ['home', 'overview', 'stats'],
731
+ hotkeys: ['ctrl+d'],
732
+ visibleOn: [isAuthenticated]
733
+ };
1053
734
 
1054
- @moved({ throttle: 500 })
1055
- onMovedThrottled() {
1056
- // Called at most once every 500ms during rapid moves
1057
- this.optimizePerformance();
1058
- }
1059
- }
735
+ @page({
736
+ tag: 'dashboard-page',
737
+ routes: ['/dashboard'],
738
+ placard: placard
739
+ })
740
+ class DashboardPage extends HTMLElement { }
1060
741
  ```
1061
742
 
1062
- ### @adopted Decorator
743
+ **Features:**
744
+ - **Navigation** - `title`, `icon`, `order`, `show`
745
+ - **Hierarchy** - `parent`, `group`, `breadcrumbs`
746
+ - **Discovery** - `searchTerms`, `hotkeys`, `tooltip`
747
+ - **Visibility** - `visibleOn` guards control who sees what
1063
748
 
1064
- The `@adopted` decorator runs methods when an element is moved to a new document (like iframes or document fragments):
749
+ Layouts receive placard data in `update()` and auto-build navigation. See [docs](./docs/placards.md).
1065
750
 
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
- }
751
+ ## Migrating from v2.x
1074
752
 
1075
- // With timing options
1076
- @adopted({ debounce: 200 })
1077
- onAdoptedDebounced() {
1078
- // Debounced for performance during rapid document moves
1079
- this.reinitializeForNewContext();
1080
- }
1081
- }
1082
- ```
753
+ v3.0.0 introduces template-based rendering with differential updates. Key changes:
1083
754
 
1084
- ### Timing Options
755
+ - **Use `@render()` instead of `html()` method**
756
+ Return `html\`...\`` tagged template instead of string
1085
757
 
1086
- Both decorators support the same timing options as `@part`:
758
+ - **Use `@styles()` instead of `css()` method**
759
+ Return `css\`...\`` tagged template instead of string
1087
760
 
1088
- - **`debounce`** - Delays execution until after calls stop for the specified milliseconds
1089
- - **`throttle`** - Limits execution to once per specified milliseconds
761
+ - **`@on()` decorator available**
762
+ Works in both elements AND controllers with full event delegation, keyboard modifiers, and debounce/throttle support.
763
+ Template event syntax (`@click=${handler}`) is also available as an alternative.
1090
764
 
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
- ```
765
+ - **`@part` decorator removed**
766
+ Differential rendering makes selective re-rendering unnecessary
1096
767
 
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
768
+ See [Migration Guide](./docs/migration-v2-to-v3.md) for detailed migration guide.
1102
769
 
1103
770
  ## Documentation
1104
771
 
1105
772
  - [Elements API](./docs/elements.md) - Complete guide to creating elements with properties, queries, and styling
1106
773
  - [Controllers API](./docs/controllers.md) - Data fetching, business logic, and controller patterns
774
+ - [Routing API](./docs/routing.md) - Single-page application routing with transitions
775
+ - [Placards API](./docs/placards.md) - Rich page metadata for dynamic navigation and discovery
1107
776
  - [Events API](./docs/events.md) - Event handling, dispatching, and custom events
1108
777
  - [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
778
  - [Observe API](./docs/observe.md) - Lifecycle-managed observers for external changes
1111
779
 
1112
780
  ## License