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.
- package/README.md +537 -869
- package/bin/templates/base/src/components/counter-button.ts +13 -26
- package/bin/templates/base/src/controllers/counter-controller.ts +3 -3
- package/dist/components/accordion/snice-accordion-item.d.ts +4 -5
- package/dist/components/accordion/snice-accordion-item.js +37 -39
- package/dist/components/accordion/snice-accordion-item.js.map +1 -1
- package/dist/components/accordion/snice-accordion.d.ts +5 -11
- package/dist/components/accordion/snice-accordion.js +51 -52
- package/dist/components/accordion/snice-accordion.js.map +1 -1
- package/dist/components/alert/snice-alert.d.ts +2 -6
- package/dist/components/alert/snice-alert.js +41 -56
- package/dist/components/alert/snice-alert.js.map +1 -1
- package/dist/components/avatar/snice-avatar.d.ts +2 -6
- package/dist/components/avatar/snice-avatar.js +64 -71
- package/dist/components/avatar/snice-avatar.js.map +1 -1
- package/dist/components/badge/snice-badge.d.ts +2 -3
- package/dist/components/badge/snice-badge.js +22 -23
- package/dist/components/badge/snice-badge.js.map +1 -1
- package/dist/components/banner/snice-banner.d.ts +22 -0
- package/dist/components/banner/snice-banner.js +180 -0
- package/dist/components/banner/snice-banner.js.map +1 -0
- package/dist/components/banner/snice-banner.types.d.ts +14 -0
- package/dist/components/breadcrumbs/snice-breadcrumbs.d.ts +5 -12
- package/dist/components/breadcrumbs/snice-breadcrumbs.js +88 -89
- package/dist/components/breadcrumbs/snice-breadcrumbs.js.map +1 -1
- package/dist/components/button/snice-button.d.ts +3 -7
- package/dist/components/button/snice-button.js +37 -58
- package/dist/components/button/snice-button.js.map +1 -1
- package/dist/components/card/snice-card.d.ts +5 -8
- package/dist/components/card/snice-card.js +71 -56
- package/dist/components/card/snice-card.js.map +1 -1
- package/dist/components/checkbox/snice-checkbox.d.ts +4 -13
- package/dist/components/checkbox/snice-checkbox.js +66 -137
- package/dist/components/checkbox/snice-checkbox.js.map +1 -1
- package/dist/components/chip/snice-chip.d.ts +5 -11
- package/dist/components/chip/snice-chip.js +44 -47
- package/dist/components/chip/snice-chip.js.map +1 -1
- package/dist/components/color-display/snice-color-display.d.ts +14 -0
- package/dist/components/color-display/snice-color-display.js +151 -0
- package/dist/components/color-display/snice-color-display.js.map +1 -0
- package/dist/components/color-display/snice-color-display.types.d.ts +10 -0
- package/dist/components/color-picker/snice-color-picker.d.ts +50 -0
- package/dist/components/color-picker/snice-color-picker.js +489 -0
- package/dist/components/color-picker/snice-color-picker.js.map +1 -0
- package/dist/components/color-picker/snice-color-picker.types.d.ts +19 -0
- package/dist/components/date-picker/snice-date-picker.d.ts +11 -11
- package/dist/components/date-picker/snice-date-picker.js +134 -133
- package/dist/components/date-picker/snice-date-picker.js.map +1 -1
- package/dist/components/divider/snice-divider.d.ts +2 -4
- package/dist/components/divider/snice-divider.js +14 -22
- package/dist/components/divider/snice-divider.js.map +1 -1
- package/dist/components/drawer/snice-drawer.d.ts +4 -4
- package/dist/components/drawer/snice-drawer.js +25 -19
- package/dist/components/drawer/snice-drawer.js.map +1 -1
- package/dist/components/empty-state/snice-empty-state.d.ts +13 -0
- package/dist/components/empty-state/snice-empty-state.js +121 -0
- package/dist/components/empty-state/snice-empty-state.js.map +1 -0
- package/dist/components/empty-state/snice-empty-state.types.d.ts +9 -0
- package/dist/components/file-upload/snice-file-upload.d.ts +45 -0
- package/dist/components/file-upload/snice-file-upload.js +394 -0
- package/dist/components/file-upload/snice-file-upload.js.map +1 -0
- package/dist/components/file-upload/snice-file-upload.types.d.ts +22 -0
- package/dist/components/image/snice-image.d.ts +22 -0
- package/dist/components/image/snice-image.js +201 -0
- package/dist/components/image/snice-image.js.map +1 -0
- package/dist/components/image/snice-image.types.d.ts +17 -0
- package/dist/components/input/snice-input.d.ts +8 -6
- package/dist/components/input/snice-input.js +122 -105
- package/dist/components/input/snice-input.js.map +1 -1
- package/dist/components/kpi/snice-kpi.d.ts +16 -0
- package/dist/components/kpi/snice-kpi.js +162 -0
- package/dist/components/kpi/snice-kpi.js.map +1 -0
- package/dist/components/kpi/snice-kpi.types.d.ts +12 -0
- package/dist/components/layout/snice-layout-blog.d.ts +4 -4
- package/dist/components/layout/snice-layout-blog.js +21 -19
- package/dist/components/layout/snice-layout-blog.js.map +1 -1
- package/dist/components/layout/snice-layout-card.d.ts +2 -2
- package/dist/components/layout/snice-layout-card.js +16 -9
- package/dist/components/layout/snice-layout-card.js.map +1 -1
- package/dist/components/layout/snice-layout-centered.d.ts +2 -2
- package/dist/components/layout/snice-layout-centered.js +14 -7
- package/dist/components/layout/snice-layout-centered.js.map +1 -1
- package/dist/components/layout/snice-layout-dashboard.d.ts +5 -5
- package/dist/components/layout/snice-layout-dashboard.js +38 -30
- package/dist/components/layout/snice-layout-dashboard.js.map +1 -1
- package/dist/components/layout/snice-layout-fullscreen.d.ts +2 -2
- package/dist/components/layout/snice-layout-fullscreen.js +17 -10
- package/dist/components/layout/snice-layout-fullscreen.js.map +1 -1
- package/dist/components/layout/snice-layout-landing.d.ts +4 -4
- package/dist/components/layout/snice-layout-landing.js +21 -19
- package/dist/components/layout/snice-layout-landing.js.map +1 -1
- package/dist/components/layout/snice-layout-minimal.d.ts +2 -2
- package/dist/components/layout/snice-layout-minimal.js +17 -6
- package/dist/components/layout/snice-layout-minimal.js.map +1 -1
- package/dist/components/layout/snice-layout-sidebar.d.ts +5 -4
- package/dist/components/layout/snice-layout-sidebar.js +42 -20
- package/dist/components/layout/snice-layout-sidebar.js.map +1 -1
- package/dist/components/layout/snice-layout-split.d.ts +2 -2
- package/dist/components/layout/snice-layout-split.js +14 -7
- package/dist/components/layout/snice-layout-split.js.map +1 -1
- package/dist/components/layout/snice-layout.d.ts +4 -4
- package/dist/components/layout/snice-layout.js +16 -10
- package/dist/components/layout/snice-layout.js.map +1 -1
- package/dist/components/link/snice-link.d.ts +13 -0
- package/dist/components/link/snice-link.js +137 -0
- package/dist/components/link/snice-link.js.map +1 -0
- package/dist/components/link/snice-link.types.d.ts +11 -0
- package/dist/components/login/snice-login.d.ts +6 -11
- package/dist/components/login/snice-login.js +97 -71
- package/dist/components/login/snice-login.js.map +1 -1
- package/dist/components/modal/snice-modal.d.ts +5 -9
- package/dist/components/modal/snice-modal.js +47 -78
- package/dist/components/modal/snice-modal.js.map +1 -1
- package/dist/components/nav/snice-nav.d.ts +13 -7
- package/dist/components/nav/snice-nav.js +191 -100
- package/dist/components/nav/snice-nav.js.map +1 -1
- package/dist/components/nav/snice-nav.types.d.ts +3 -3
- package/dist/components/pagination/snice-pagination.d.ts +6 -7
- package/dist/components/pagination/snice-pagination.js +94 -81
- package/dist/components/pagination/snice-pagination.js.map +1 -1
- package/dist/components/progress/snice-progress.d.ts +2 -7
- package/dist/components/progress/snice-progress.js +41 -98
- package/dist/components/progress/snice-progress.js.map +1 -1
- package/dist/components/radio/snice-radio.d.ts +4 -4
- package/dist/components/radio/snice-radio.js +52 -44
- package/dist/components/radio/snice-radio.js.map +1 -1
- package/dist/components/select/snice-option.d.ts +2 -1
- package/dist/components/select/snice-option.js +12 -5
- package/dist/components/select/snice-option.js.map +1 -1
- package/dist/components/select/snice-select.d.ts +9 -21
- package/dist/components/select/snice-select.js +98 -170
- package/dist/components/select/snice-select.js.map +1 -1
- package/dist/components/skeleton/snice-skeleton.d.ts +2 -6
- package/dist/components/skeleton/snice-skeleton.js +18 -49
- package/dist/components/skeleton/snice-skeleton.js.map +1 -1
- package/dist/components/slider/snice-slider.d.ts +53 -0
- package/dist/components/slider/snice-slider.js +479 -0
- package/dist/components/slider/snice-slider.js.map +1 -0
- package/dist/components/slider/snice-slider.types.d.ts +26 -0
- package/dist/components/snice-cell-C0slgOpe.js +4 -0
- package/dist/components/snice-cell-C0slgOpe.js.map +1 -0
- package/dist/components/sparkline/snice-sparkline.d.ts +21 -0
- package/dist/components/sparkline/snice-sparkline.js +228 -0
- package/dist/components/sparkline/snice-sparkline.js.map +1 -0
- package/dist/components/sparkline/snice-sparkline.types.d.ts +16 -0
- package/dist/components/spinner/snice-spinner.d.ts +10 -0
- package/dist/components/spinner/snice-spinner.js +109 -0
- package/dist/components/spinner/snice-spinner.js.map +1 -0
- package/dist/components/spinner/snice-spinner.types.d.ts +8 -0
- package/dist/components/stepper/snice-stepper-panel.d.ts +8 -0
- package/dist/components/stepper/snice-stepper-panel.js +70 -0
- package/dist/components/stepper/snice-stepper-panel.js.map +1 -0
- package/dist/components/stepper/snice-stepper-panel.types.d.ts +4 -0
- package/dist/components/stepper/snice-stepper.d.ts +15 -0
- package/dist/components/stepper/snice-stepper.js +163 -0
- package/dist/components/stepper/snice-stepper.js.map +1 -0
- package/dist/components/stepper/snice-stepper.types.d.ts +13 -0
- package/dist/components/switch/snice-switch.d.ts +2 -2
- package/dist/components/switch/snice-switch.js +38 -26
- package/dist/components/switch/snice-switch.js.map +1 -1
- package/dist/components/table/snice-cell-actions.d.ts +24 -0
- package/dist/components/table/snice-cell-actions.js +149 -0
- package/dist/components/table/snice-cell-actions.js.map +1 -0
- package/dist/components/table/snice-cell-boolean.d.ts +2 -2
- package/dist/components/table/snice-cell-boolean.js +13 -7
- package/dist/components/table/snice-cell-boolean.js.map +1 -1
- package/dist/components/table/snice-cell-color.d.ts +18 -0
- package/dist/components/table/snice-cell-color.js +149 -0
- package/dist/components/table/snice-cell-color.js.map +1 -0
- package/dist/components/table/snice-cell-currency.d.ts +24 -0
- package/dist/components/table/snice-cell-currency.js +235 -0
- package/dist/components/table/snice-cell-currency.js.map +1 -0
- package/dist/components/table/snice-cell-date.d.ts +2 -2
- package/dist/components/table/snice-cell-date.js +14 -8
- package/dist/components/table/snice-cell-date.js.map +1 -1
- package/dist/components/table/snice-cell-duration.d.ts +2 -2
- package/dist/components/table/snice-cell-duration.js +12 -6
- package/dist/components/table/snice-cell-duration.js.map +1 -1
- package/dist/components/table/snice-cell-email.d.ts +15 -0
- package/dist/components/table/snice-cell-email.js +125 -0
- package/dist/components/table/snice-cell-email.js.map +1 -0
- package/dist/components/table/snice-cell-filesize.d.ts +2 -2
- package/dist/components/table/snice-cell-filesize.js +12 -6
- package/dist/components/table/snice-cell-filesize.js.map +1 -1
- package/dist/components/table/snice-cell-image.d.ts +20 -0
- package/dist/components/table/snice-cell-image.js +162 -0
- package/dist/components/table/snice-cell-image.js.map +1 -0
- package/dist/components/table/snice-cell-json.d.ts +20 -0
- package/dist/components/table/snice-cell-json.js +186 -0
- package/dist/components/table/snice-cell-json.js.map +1 -0
- package/dist/components/table/snice-cell-link.d.ts +17 -0
- package/dist/components/table/snice-cell-link.js +142 -0
- package/dist/components/table/snice-cell-link.js.map +1 -0
- package/dist/components/table/snice-cell-location.d.ts +19 -0
- package/dist/components/table/snice-cell-location.js +185 -0
- package/dist/components/table/snice-cell-location.js.map +1 -0
- package/dist/components/table/snice-cell-number.d.ts +2 -2
- package/dist/components/table/snice-cell-number.js +12 -6
- package/dist/components/table/snice-cell-number.js.map +1 -1
- package/dist/components/table/snice-cell-percentage.d.ts +22 -0
- package/dist/components/table/snice-cell-percentage.js +208 -0
- package/dist/components/table/snice-cell-percentage.js.map +1 -0
- package/dist/components/table/snice-cell-phone.d.ts +18 -0
- package/dist/components/table/snice-cell-phone.js +153 -0
- package/dist/components/table/snice-cell-phone.js.map +1 -0
- package/dist/components/table/snice-cell-progress.d.ts +2 -2
- package/dist/components/table/snice-cell-progress.js +12 -6
- package/dist/components/table/snice-cell-progress.js.map +1 -1
- package/dist/components/table/snice-cell-rating.d.ts +2 -2
- package/dist/components/table/snice-cell-rating.js +12 -6
- package/dist/components/table/snice-cell-rating.js.map +1 -1
- package/dist/components/table/snice-cell-sparkline.d.ts +2 -2
- package/dist/components/table/snice-cell-sparkline.js +13 -7
- package/dist/components/table/snice-cell-sparkline.js.map +1 -1
- package/dist/components/table/snice-cell-status.d.ts +17 -0
- package/dist/components/table/snice-cell-status.js +144 -0
- package/dist/components/table/snice-cell-status.js.map +1 -0
- package/dist/components/table/snice-cell-tag.d.ts +16 -0
- package/dist/components/table/snice-cell-tag.js +131 -0
- package/dist/components/table/snice-cell-tag.js.map +1 -0
- package/dist/components/table/snice-cell-text.d.ts +2 -2
- package/dist/components/table/snice-cell-text.js +14 -8
- package/dist/components/table/snice-cell-text.js.map +1 -1
- package/dist/components/table/snice-cell.d.ts +2 -2
- package/dist/components/table/snice-cell.js +12 -6
- package/dist/components/table/snice-cell.js.map +1 -1
- package/dist/components/table/snice-column.d.ts +1 -1
- package/dist/components/table/snice-column.js +6 -3
- package/dist/components/table/snice-column.js.map +1 -1
- package/dist/components/table/snice-header.d.ts +5 -5
- package/dist/components/table/snice-header.js +60 -50
- package/dist/components/table/snice-header.js.map +1 -1
- package/dist/components/table/snice-progress.d.ts +2 -2
- package/dist/components/table/snice-progress.js +18 -11
- package/dist/components/table/snice-progress.js.map +1 -1
- package/dist/components/table/snice-rating.d.ts +2 -2
- package/dist/components/table/snice-rating.js +15 -8
- package/dist/components/table/snice-rating.js.map +1 -1
- package/dist/components/table/snice-row.d.ts +17 -6
- package/dist/components/table/snice-row.js +95 -44
- package/dist/components/table/snice-row.js.map +1 -1
- package/dist/components/table/snice-table.d.ts +18 -10
- package/dist/components/table/snice-table.js +355 -173
- package/dist/components/table/snice-table.js.map +1 -1
- package/dist/components/table/snice-table.types.d.ts +101 -2
- package/dist/components/tabs/snice-tab-panel.d.ts +2 -2
- package/dist/components/tabs/snice-tab-panel.js +12 -6
- package/dist/components/tabs/snice-tab-panel.js.map +1 -1
- package/dist/components/tabs/snice-tab.d.ts +6 -5
- package/dist/components/tabs/snice-tab.js +36 -19
- package/dist/components/tabs/snice-tab.js.map +1 -1
- package/dist/components/tabs/snice-tabs.d.ts +5 -5
- package/dist/components/tabs/snice-tabs.js +38 -28
- package/dist/components/tabs/snice-tabs.js.map +1 -1
- package/dist/components/textarea/snice-textarea.d.ts +52 -0
- package/dist/components/textarea/snice-textarea.js +407 -0
- package/dist/components/textarea/snice-textarea.js.map +1 -0
- package/dist/components/textarea/snice-textarea.types.d.ts +30 -0
- package/dist/components/timeline/snice-timeline.d.ts +11 -0
- package/dist/components/timeline/snice-timeline.js +112 -0
- package/dist/components/timeline/snice-timeline.js.map +1 -0
- package/dist/components/timeline/snice-timeline.types.d.ts +16 -0
- package/dist/components/toast/snice-toast-container.d.ts +7 -7
- package/dist/components/toast/snice-toast-container.js +19 -12
- package/dist/components/toast/snice-toast-container.js.map +1 -1
- package/dist/components/toast/snice-toast.d.ts +3 -15
- package/dist/components/toast/snice-toast.js +49 -108
- package/dist/components/toast/snice-toast.js.map +1 -1
- package/dist/components/tooltip/snice-tooltip.d.ts +2 -2
- package/dist/components/tooltip/snice-tooltip.js +15 -8
- package/dist/components/tooltip/snice-tooltip.js.map +1 -1
- package/dist/context.d.ts +44 -0
- package/dist/element-ready.d.ts +40 -0
- package/dist/{types/element.d.ts → element.d.ts} +2 -8
- package/dist/{types/events.d.ts → events.d.ts} +0 -4
- package/dist/index.cjs +2556 -605
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +21 -0
- package/dist/index.esm.js +2535 -604
- package/dist/index.esm.js.map +1 -1
- package/dist/index.iife.js +2556 -605
- package/dist/index.iife.js.map +1 -1
- package/dist/method-decorators.d.ts +121 -0
- package/dist/on.d.ts +59 -0
- package/dist/parts.d.ts +156 -0
- package/dist/render-debug.d.ts +27 -0
- package/dist/render-tracker.d.ts +14 -0
- package/dist/render.d.ts +96 -0
- package/dist/symbols.cjs +163 -0
- package/dist/symbols.cjs.map +1 -1
- package/dist/{types/symbols.d.ts → symbols.d.ts} +22 -0
- package/dist/symbols.esm.js +27 -3
- package/dist/symbols.esm.js.map +1 -1
- package/dist/template.d.ts +99 -0
- package/dist/transitions.cjs +219 -0
- package/dist/transitions.esm.js +2 -2
- package/dist/types/context.d.ts +48 -0
- package/dist/types/element-options.d.ts +26 -0
- package/dist/types/index.d.ts +25 -9
- package/dist/types/nav-context.d.ts +19 -0
- package/dist/types/{types/on-options.d.ts → on-options.d.ts} +2 -0
- package/dist/types/{types/placard.d.ts → placard.d.ts} +0 -1
- package/docs/ai/README.md +26 -0
- package/docs/ai/api.md +175 -0
- package/docs/ai/architecture.md +160 -0
- package/docs/ai/components/accordion.md +174 -0
- package/docs/ai/components/alert.md +77 -0
- package/docs/ai/components/avatar.md +61 -0
- package/docs/ai/components/badge.md +69 -0
- package/docs/ai/components/banner.md +84 -0
- package/docs/ai/components/breadcrumbs.md +74 -0
- package/docs/ai/components/button.md +75 -0
- package/docs/ai/components/card.md +61 -0
- package/docs/ai/components/checkbox.md +74 -0
- package/docs/ai/components/chip.md +73 -0
- package/docs/ai/components/color-display.md +48 -0
- package/docs/ai/components/color-picker.md +75 -0
- package/docs/ai/components/date-picker.md +75 -0
- package/docs/ai/components/divider.md +66 -0
- package/docs/ai/components/drawer.md +80 -0
- package/docs/ai/components/empty-state.md +72 -0
- package/docs/ai/components/file-upload.md +93 -0
- package/docs/ai/components/image.md +60 -0
- package/docs/ai/components/input.md +111 -0
- package/docs/ai/components/kpi.md +158 -0
- package/docs/ai/components/link.md +77 -0
- package/docs/ai/components/login.md +109 -0
- package/docs/ai/components/modal.md +67 -0
- package/docs/ai/components/nav.md +76 -0
- package/docs/ai/components/pagination.md +55 -0
- package/docs/ai/components/progress.md +72 -0
- package/docs/ai/components/radio.md +79 -0
- package/docs/ai/components/select.md +92 -0
- package/docs/ai/components/skeleton.md +57 -0
- package/docs/ai/components/slider.md +87 -0
- package/docs/ai/components/sparkline.md +168 -0
- package/docs/ai/components/spinner.md +47 -0
- package/docs/ai/components/stepper.md +216 -0
- package/docs/ai/components/switch.md +53 -0
- package/docs/ai/components/table.md +227 -0
- package/docs/ai/components/tabs.md +83 -0
- package/docs/ai/components/textarea.md +87 -0
- package/docs/ai/components/timeline.md +77 -0
- package/docs/ai/components/toast.md +140 -0
- package/docs/ai/components/tooltip.md +146 -0
- package/docs/ai/patterns.md +244 -0
- package/docs/components/accordion.md +558 -0
- package/docs/components/banner.md +106 -0
- package/docs/components/color-display.md +96 -0
- package/docs/components/color-picker.md +81 -0
- package/docs/components/drawer.md +602 -0
- package/docs/components/empty-state.md +79 -0
- package/docs/components/file-upload.md +263 -0
- package/docs/components/image.md +110 -0
- package/docs/components/kpi.md +251 -0
- package/docs/components/link.md +229 -0
- package/docs/components/modal.md +558 -0
- package/docs/components/nav.md +239 -0
- package/docs/components/pagination.md +289 -0
- package/docs/components/select.md +599 -0
- package/docs/components/slider.md +297 -0
- package/docs/components/sparkline.md +293 -0
- package/docs/components/spinner.md +63 -0
- package/docs/components/stepper.md +410 -0
- package/docs/components/switch.md +354 -0
- package/docs/components/tabs.md +546 -0
- package/docs/components/textarea.md +235 -0
- package/docs/components/timeline.md +192 -0
- package/docs/components/toast.md +506 -0
- package/docs/components/tooltip.md +523 -0
- package/docs/controllers.md +744 -0
- package/docs/elements.md +855 -0
- package/docs/events.md +807 -0
- package/docs/migration-v2-to-v3.md +569 -0
- package/docs/observe.md +588 -0
- package/docs/placards.md +401 -0
- package/docs/request-response.md +852 -0
- package/docs/routing.md +1186 -0
- package/package.json +11 -11
- package/dist/components/snice-cell-C9N6yGxQ.js +0 -4
- package/dist/components/snice-cell-C9N6yGxQ.js.map +0 -1
- package/dist/types/types/index.d.ts +0 -23
- /package/dist/{types/controller.d.ts → controller.d.ts} +0 -0
- /package/dist/{types/global.d.ts → global.d.ts} +0 -0
- /package/dist/{types/observe.d.ts → observe.d.ts} +0 -0
- /package/dist/{types/request-response.d.ts → request-response.d.ts} +0 -0
- /package/dist/{types/router.d.ts → router.d.ts} +0 -0
- /package/dist/{types/testing.d.ts → testing.d.ts} +0 -0
- /package/dist/{types/transitions.d.ts → transitions.d.ts} +0 -0
- /package/dist/types/{types/adopted-options.d.ts → adopted-options.d.ts} +0 -0
- /package/dist/types/{types/app-context.d.ts → app-context.d.ts} +0 -0
- /package/dist/types/{types/dispatch-options.d.ts → dispatch-options.d.ts} +0 -0
- /package/dist/types/{types/guard.d.ts → guard.d.ts} +0 -0
- /package/dist/types/{types/i-controller.d.ts → i-controller.d.ts} +0 -0
- /package/dist/types/{types/moved-options.d.ts → moved-options.d.ts} +0 -0
- /package/dist/types/{types/observe-options.d.ts → observe-options.d.ts} +0 -0
- /package/dist/types/{types/page-options.d.ts → page-options.d.ts} +0 -0
- /package/dist/types/{types/part-options.d.ts → part-options.d.ts} +0 -0
- /package/dist/types/{types/property-converter.d.ts → property-converter.d.ts} +0 -0
- /package/dist/types/{types/property-options.d.ts → property-options.d.ts} +0 -0
- /package/dist/types/{types/query-options.d.ts → query-options.d.ts} +0 -0
- /package/dist/types/{types/request-options.d.ts → request-options.d.ts} +0 -0
- /package/dist/types/{types/respond-options.d.ts → respond-options.d.ts} +0 -0
- /package/dist/types/{types/route-params.d.ts → route-params.d.ts} +0 -0
- /package/dist/types/{types/router-instance.d.ts → router-instance.d.ts} +0 -0
- /package/dist/types/{types/router-options.d.ts → router-options.d.ts} +0 -0
- /package/dist/types/{types/simple-array.d.ts → simple-array.d.ts} +0 -0
- /package/dist/types/{types/snice-element.d.ts → snice-element.d.ts} +0 -0
- /package/dist/types/{types/snice-global.d.ts → snice-global.d.ts} +0 -0
- /package/dist/types/{types/transition.d.ts → transition.d.ts} +0 -0
- /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
|
-
|
|
3
|
+
> **AI Assistants:** For token-efficient documentation, read [docs/ai/](./docs/ai/) instead. Same content, 70% fewer tokens.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A TypeScript framework for building sustainable web applications through clear separation of governance.
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
##
|
|
15
|
+
## Philosophy
|
|
16
16
|
|
|
17
|
-
|
|
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
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
-
|
|
22
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
+
Each piece hints at where your code should live, preventing the mess that kills velocity on larger teams.
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
## The Tools
|
|
29
30
|
|
|
30
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
<user-card user-id="123" controller="user-loader"></user-card>
|
|
84
|
-
```
|
|
40
|
+
@element('user-stats')
|
|
41
|
+
class UserStats extends HTMLElement { ... }
|
|
85
42
|
|
|
86
|
-
|
|
43
|
+
@controller('real-time-user-loader')
|
|
44
|
+
class RealTimeUserLoader { ... }
|
|
87
45
|
|
|
88
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
75
|
+
// main.ts
|
|
76
|
+
import { Router } from 'snice';
|
|
112
77
|
|
|
113
|
-
|
|
78
|
+
const { page, navigate, initialize } = Router({
|
|
79
|
+
target: '#app',
|
|
80
|
+
context: new AppContext(), // Global state
|
|
81
|
+
type: 'hash'
|
|
82
|
+
});
|
|
114
83
|
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
@
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
```html
|
|
129
|
-
<my-button></my-button>
|
|
130
|
-
```
|
|
98
|
+
### 2. Pages: Orchestrating Intent
|
|
131
99
|
|
|
132
|
-
|
|
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
|
-
|
|
107
|
+
@property({ type: Object })
|
|
108
|
+
user = null;
|
|
135
109
|
|
|
136
|
-
|
|
137
|
-
|
|
110
|
+
@property({ type: Object })
|
|
111
|
+
userStats = null;
|
|
138
112
|
|
|
139
|
-
@
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return `
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
@
|
|
199
|
-
|
|
144
|
+
@render()
|
|
145
|
+
renderContent() {
|
|
146
|
+
if (!this.stats) return html`<div>Loading...</div>`;
|
|
200
147
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
|
|
175
|
+
### 4. Controllers: Behavior Management
|
|
227
176
|
|
|
228
177
|
```typescript
|
|
229
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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
|
-
<
|
|
220
|
+
<user-list controller="real-time-user-loader"></user-list>
|
|
221
|
+
<user-list controller="cached-user-loader"></user-list>
|
|
244
222
|
```
|
|
245
223
|
|
|
246
|
-
##
|
|
224
|
+
## Key Features
|
|
247
225
|
|
|
248
|
-
|
|
226
|
+
**Differential Rendering** - Only updates changed parts of the DOM, not entire components
|
|
249
227
|
|
|
250
|
-
|
|
251
|
-
import { element, property, watch, query } from 'snice';
|
|
228
|
+
**Auto-Rendering** - Components automatically re-render when properties change
|
|
252
229
|
|
|
253
|
-
|
|
254
|
-
class ThemeToggle extends HTMLElement {
|
|
255
|
-
@property()
|
|
256
|
-
theme: 'light' | 'dark' = 'light';
|
|
230
|
+
**Template Syntax** - Clean `html\`...\`` and `css\`...\`` tagged templates
|
|
257
231
|
|
|
258
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
236
|
+
**Standards-Based** - Built on web components, works with any framework
|
|
289
237
|
|
|
290
|
-
|
|
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
|
-
|
|
240
|
+
### Class Decorators
|
|
300
241
|
|
|
242
|
+
**`@element('tag-name')`** - Define reusable UI components
|
|
301
243
|
```typescript
|
|
302
|
-
@
|
|
303
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
Query single elements with `@query`:
|
|
312
|
-
|
|
248
|
+
**`@page({ tag, routes })`** - Define routable pages
|
|
313
249
|
```typescript
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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
|
-
|
|
263
|
+
### Rendering
|
|
332
264
|
|
|
265
|
+
**`@render(options?)`** - Define component template (auto re-renders on property changes)
|
|
333
266
|
```typescript
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
Listen for events with `@on`:
|
|
360
|
-
|
|
273
|
+
**`@styles()`** - Define scoped styles
|
|
361
274
|
```typescript
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
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
|
-
|
|
285
|
+
@property()
|
|
286
|
+
name = 'default';
|
|
401
287
|
|
|
402
|
-
@
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
@query('.toggle')
|
|
407
|
-
toggleButton!: HTMLElement;
|
|
288
|
+
@property({ type: Boolean })
|
|
289
|
+
enabled = false;
|
|
290
|
+
```
|
|
408
291
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
@dispatch('maybe-data', { dispatchOnUndefined: false })
|
|
304
|
+
@ready()
|
|
305
|
+
async initialize() {
|
|
306
|
+
// Fetch data, set up listeners, etc.
|
|
307
|
+
}
|
|
435
308
|
```
|
|
436
309
|
|
|
437
|
-
|
|
438
|
-
|
|
310
|
+
**`@dispose()`** - Runs when element is removed from DOM
|
|
439
311
|
```typescript
|
|
440
|
-
@
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
318
|
+
### DOM Queries
|
|
459
319
|
|
|
460
|
-
|
|
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
|
-
|
|
328
|
+
@queryAll('.item')
|
|
329
|
+
items!: NodeListOf<HTMLElement>;
|
|
330
|
+
```
|
|
464
331
|
|
|
465
|
-
|
|
332
|
+
### Events & Communication
|
|
466
333
|
|
|
467
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
@
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
484
|
-
|
|
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
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
//
|
|
533
|
-
@
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
@
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
@
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
|
|
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
|
-
|
|
635
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
646
|
-
<user-list controller="user-controller"></user-list>
|
|
647
|
-
```
|
|
439
|
+
### Request/Response
|
|
648
440
|
|
|
649
|
-
|
|
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
|
-
|
|
443
|
+
**`@request(channel)`** - Make requests to controllers from elements
|
|
444
|
+
**`@respond(channel)`** - Respond to requests from elements in controllers
|
|
652
445
|
|
|
653
|
-
|
|
654
|
-
|
|
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
|
-
|
|
657
|
-
@element('user-profile')
|
|
658
|
-
class UserProfile extends HTMLElement {
|
|
451
|
+
**Example:**
|
|
659
452
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
-
@
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
this.displayUser(userData);
|
|
459
|
+
@respond('user')
|
|
460
|
+
getUserData() {
|
|
461
|
+
return this.currentUser;
|
|
670
462
|
}
|
|
671
|
-
|
|
672
463
|
}
|
|
673
464
|
|
|
674
|
-
|
|
675
|
-
|
|
465
|
+
// Element makes requests
|
|
466
|
+
@element('user-badge')
|
|
467
|
+
class UserBadge extends HTMLElement {
|
|
468
|
+
@request('user')
|
|
469
|
+
getUser!: () => any;
|
|
676
470
|
|
|
677
|
-
@
|
|
678
|
-
|
|
679
|
-
const
|
|
680
|
-
|
|
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
|
-
|
|
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
|
-
|
|
494
|
+
## Template Syntax
|
|
689
495
|
|
|
690
|
-
###
|
|
496
|
+
### Auto-Rendering with Differential Updates
|
|
691
497
|
|
|
692
498
|
```typescript
|
|
693
|
-
|
|
499
|
+
@element('counter-display')
|
|
500
|
+
class CounterDisplay extends HTMLElement {
|
|
501
|
+
@property({ type: Number })
|
|
502
|
+
count = 0;
|
|
694
503
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
534
|
+
Use `.property=${value}` to set element properties directly:
|
|
751
535
|
|
|
752
|
-
|
|
536
|
+
```typescript
|
|
537
|
+
html`
|
|
538
|
+
<input .value=${this.text} />
|
|
539
|
+
<custom-element .complexData=${this.dataObject}></custom-element>
|
|
540
|
+
`
|
|
541
|
+
```
|
|
753
542
|
|
|
754
|
-
###
|
|
543
|
+
### Boolean Attributes
|
|
755
544
|
|
|
756
|
-
|
|
545
|
+
Use `?attribute=${boolean}` for boolean attributes:
|
|
757
546
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
547
|
+
```typescript
|
|
548
|
+
html`
|
|
549
|
+
<button ?disabled=${this.isLoading}>Submit</button>
|
|
550
|
+
<input type="checkbox" ?checked=${this.isChecked} />
|
|
551
|
+
`
|
|
552
|
+
```
|
|
764
553
|
|
|
765
|
-
###
|
|
554
|
+
### Conditionals
|
|
766
555
|
|
|
767
556
|
```typescript
|
|
768
|
-
//
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
###
|
|
810
|
-
|
|
811
|
-
Nested elements within pages can also access context:
|
|
607
|
+
### Keyboard Shortcuts
|
|
812
608
|
|
|
813
609
|
```typescript
|
|
814
|
-
|
|
815
|
-
@
|
|
816
|
-
|
|
817
|
-
@
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
|
-
|
|
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
|
-
|
|
626
|
+
## Router
|
|
844
627
|
|
|
845
628
|
```typescript
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
-
|
|
652
|
+
// main.ts
|
|
653
|
+
initialize();
|
|
654
|
+
navigate('/users/123');
|
|
655
|
+
```
|
|
879
656
|
|
|
880
|
-
|
|
657
|
+
### Route Guards
|
|
881
658
|
|
|
882
|
-
|
|
659
|
+
Protect routes with guard functions:
|
|
883
660
|
|
|
884
661
|
```typescript
|
|
885
|
-
|
|
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
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
-
|
|
916
|
-
|
|
917
|
-
## Parts - Selective Re-rendering
|
|
672
|
+
## Layouts
|
|
918
673
|
|
|
919
|
-
|
|
674
|
+
Layouts wrap pages with shared UI and dynamically build navigation from page metadata:
|
|
920
675
|
|
|
921
676
|
```typescript
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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
|
-
@
|
|
962
|
-
|
|
963
|
-
return `
|
|
964
|
-
<
|
|
965
|
-
|
|
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
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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
|
-
|
|
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
|
-
|
|
719
|
+
## Placards
|
|
1033
720
|
|
|
1034
|
-
|
|
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
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
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
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
-
|
|
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
|
-
|
|
749
|
+
Layouts receive placard data in `update()` and auto-build navigation. See [docs](./docs/placards.md).
|
|
1065
750
|
|
|
1066
|
-
|
|
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
|
-
|
|
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
|
-
|
|
755
|
+
- **Use `@render()` instead of `html()` method**
|
|
756
|
+
Return `html\`...\`` tagged template instead of string
|
|
1085
757
|
|
|
1086
|
-
|
|
758
|
+
- **Use `@styles()` instead of `css()` method**
|
|
759
|
+
Return `css\`...\`` tagged template instead of string
|
|
1087
760
|
|
|
1088
|
-
-
|
|
1089
|
-
|
|
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
|
-
|
|
1092
|
-
|
|
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
|
-
|
|
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
|