snice 2.5.4 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +501 -882
- 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/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/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/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/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/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/snice-cell-BLFVdxPp.js +4 -0
- package/dist/components/snice-cell-BLFVdxPp.js.map +1 -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/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 +14 -7
- 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 +2589 -605
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +21 -0
- package/dist/index.esm.js +2568 -604
- package/dist/index.esm.js.map +1 -1
- package/dist/index.iife.js +2589 -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 +159 -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 +100 -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 +17 -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/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/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/input.md +111 -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/switch.md +53 -0
- package/docs/ai/components/table.md +227 -0
- package/docs/ai/components/tabs.md +83 -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/drawer.md +602 -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/switch.md +354 -0
- package/docs/components/tabs.md +546 -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 +10 -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/docs/routing.md
ADDED
|
@@ -0,0 +1,1186 @@
|
|
|
1
|
+
# Routing API Documentation
|
|
2
|
+
|
|
3
|
+
Snice provides a powerful routing system for single-page applications with support for hash and pushstate routing, page transitions, route parameters, guards, and layouts.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
- [Router Setup](#router-setup)
|
|
7
|
+
- [Page Components](#page-components)
|
|
8
|
+
- [Route Configuration](#route-configuration)
|
|
9
|
+
- [Navigation](#navigation)
|
|
10
|
+
- [Route Parameters](#route-parameters)
|
|
11
|
+
- [Page Transitions](#page-transitions)
|
|
12
|
+
- [Route Guards](#route-guards)
|
|
13
|
+
- [Layouts](#layouts)
|
|
14
|
+
- [Advanced Patterns](#advanced-patterns)
|
|
15
|
+
|
|
16
|
+
## Router Setup
|
|
17
|
+
|
|
18
|
+
### Creating a Router
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { Router } from 'snice';
|
|
22
|
+
|
|
23
|
+
const router = Router({
|
|
24
|
+
target: '#app', // Target element selector
|
|
25
|
+
type: 'hash' // 'hash' or 'pushstate'
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Destructure router methods
|
|
29
|
+
const { page, initialize, navigate } = router;
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Router Options
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
interface RouterOptions<T = any> {
|
|
36
|
+
target: string; // Target element selector
|
|
37
|
+
type: 'hash' | 'pushstate'; // Routing type
|
|
38
|
+
window?: Window; // Override window object (for testing)
|
|
39
|
+
document?: Document; // Override document object (for testing)
|
|
40
|
+
transition?: Transition; // Global transition config
|
|
41
|
+
layout?: string; // Default layout for all pages
|
|
42
|
+
context?: T; // Router context object (shared state)
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Router Context
|
|
47
|
+
|
|
48
|
+
The context object provides shared state across all pages and layouts:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// app-context.ts
|
|
52
|
+
class AppContext {
|
|
53
|
+
user: User | null = null;
|
|
54
|
+
theme: 'light' | 'dark' = 'light';
|
|
55
|
+
|
|
56
|
+
setUser(user: User) {
|
|
57
|
+
this.user = user;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getUser() {
|
|
61
|
+
return this.user;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// main.ts
|
|
66
|
+
const { page, initialize } = Router({
|
|
67
|
+
target: '#app',
|
|
68
|
+
type: 'hash',
|
|
69
|
+
context: new AppContext()
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Page Components
|
|
74
|
+
|
|
75
|
+
### Basic Page
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { page, render, html, styles, css } from 'snice';
|
|
79
|
+
|
|
80
|
+
@page({ tag: 'home-page', routes: ['/'] })
|
|
81
|
+
class HomePage extends HTMLElement {
|
|
82
|
+
@render()
|
|
83
|
+
renderContent() {
|
|
84
|
+
return html`
|
|
85
|
+
<div class="home">
|
|
86
|
+
<h1>Welcome Home</h1>
|
|
87
|
+
<nav>
|
|
88
|
+
<a href="#/about">About</a>
|
|
89
|
+
<a href="#/contact">Contact</a>
|
|
90
|
+
</nav>
|
|
91
|
+
</div>
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@styles()
|
|
96
|
+
homeStyles() {
|
|
97
|
+
return css`
|
|
98
|
+
.home {
|
|
99
|
+
padding: 20px;
|
|
100
|
+
text-align: center;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
nav a {
|
|
104
|
+
margin: 0 10px;
|
|
105
|
+
color: blue;
|
|
106
|
+
text-decoration: none;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
nav a:hover {
|
|
110
|
+
text-decoration: underline;
|
|
111
|
+
}
|
|
112
|
+
`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Page with Context
|
|
118
|
+
|
|
119
|
+
The `@context()` decorator is a **method decorator** that receives context updates from the router. The method is called whenever navigation occurs, with a Context object containing application state and navigation data.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { page, context, render, html, Context } from 'snice';
|
|
123
|
+
|
|
124
|
+
@page({ tag: 'profile-page', routes: ['/profile'] })
|
|
125
|
+
class ProfilePage extends HTMLElement {
|
|
126
|
+
private appContext?: AppContext;
|
|
127
|
+
|
|
128
|
+
@context()
|
|
129
|
+
handleContextUpdate(ctx: Context) {
|
|
130
|
+
// ctx.application is your router context (AppContext)
|
|
131
|
+
this.appContext = ctx.application;
|
|
132
|
+
// ctx.navigation contains { placards, route, params }
|
|
133
|
+
this.requestRender();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@render()
|
|
137
|
+
renderContent() {
|
|
138
|
+
const user = this.appContext?.getUser();
|
|
139
|
+
|
|
140
|
+
if (!user) {
|
|
141
|
+
return html`
|
|
142
|
+
<div>
|
|
143
|
+
<p>Please log in to view your profile</p>
|
|
144
|
+
<a href="#/login">Login</a>
|
|
145
|
+
</div>
|
|
146
|
+
`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return html`
|
|
150
|
+
<div class="profile">
|
|
151
|
+
<h1>Profile: ${user.name}</h1>
|
|
152
|
+
<p>Email: ${user.email}</p>
|
|
153
|
+
<button @click=${this.logout}>Logout</button>
|
|
154
|
+
</div>
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
logout() {
|
|
159
|
+
this.appContext?.setUser(null);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Context Options
|
|
165
|
+
|
|
166
|
+
The `@context()` decorator accepts optional timing and behavior controls:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
@page({ tag: 'dashboard-page', routes: ['/dashboard'] })
|
|
170
|
+
class DashboardPage extends HTMLElement {
|
|
171
|
+
private appContext?: AppContext;
|
|
172
|
+
|
|
173
|
+
// Called immediately on every navigation
|
|
174
|
+
@context()
|
|
175
|
+
handleContext(ctx: Context) {
|
|
176
|
+
this.appContext = ctx.application;
|
|
177
|
+
this.requestRender();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Debounce: Wait 300ms after last update before calling
|
|
181
|
+
@context({ debounce: 300 })
|
|
182
|
+
handleContextDebounced(ctx: Context) {
|
|
183
|
+
// Useful for expensive operations
|
|
184
|
+
this.updateExpensiveCalculation(ctx);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Throttle: Call at most once per 100ms
|
|
188
|
+
@context({ throttle: 100 })
|
|
189
|
+
handleContextThrottled(ctx: Context) {
|
|
190
|
+
// Useful for frequent updates
|
|
191
|
+
this.updateAnimation(ctx);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Once: Call only once, then unregister
|
|
195
|
+
@context({ once: true })
|
|
196
|
+
handleContextOnce(ctx: Context) {
|
|
197
|
+
// Useful for one-time initialization
|
|
198
|
+
this.initializeFromContext(ctx);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Context Object Structure
|
|
204
|
+
|
|
205
|
+
The Context object passed to `@context()` methods has the following structure:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
interface Context<T = any> {
|
|
209
|
+
application: T; // Your router context (e.g., AppContext)
|
|
210
|
+
navigation: {
|
|
211
|
+
placards: Placard[]; // All page placards
|
|
212
|
+
route: string; // Current route name
|
|
213
|
+
params: Record<string, string>; // Route parameters
|
|
214
|
+
};
|
|
215
|
+
update(): void; // Notify all subscribers of changes
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Example:**
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
@page({ tag: 'user-page', routes: ['/users/:userId'] })
|
|
223
|
+
class UserPage extends HTMLElement {
|
|
224
|
+
private ctx?: Context<AppContext>;
|
|
225
|
+
|
|
226
|
+
@context()
|
|
227
|
+
handleContext(ctx: Context<AppContext>) {
|
|
228
|
+
this.ctx = ctx;
|
|
229
|
+
|
|
230
|
+
// Access application state
|
|
231
|
+
const currentUser = ctx.application.getUser();
|
|
232
|
+
|
|
233
|
+
// Access navigation data
|
|
234
|
+
const userId = ctx.navigation.params.userId;
|
|
235
|
+
const currentRoute = ctx.navigation.route;
|
|
236
|
+
const allPlacards = ctx.navigation.placards;
|
|
237
|
+
|
|
238
|
+
// Use this data
|
|
239
|
+
this.loadUserData(userId, currentUser);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Triggering Context Updates
|
|
245
|
+
|
|
246
|
+
When you modify the application context, call `update()` to notify all subscribers:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
@page({ tag: 'settings-page', routes: ['/settings'] })
|
|
250
|
+
class SettingsPage extends HTMLElement {
|
|
251
|
+
private ctx?: Context<AppContext>;
|
|
252
|
+
|
|
253
|
+
@context()
|
|
254
|
+
handleContext(ctx: Context<AppContext>) {
|
|
255
|
+
this.ctx = ctx;
|
|
256
|
+
this.requestRender();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
changeTheme(theme: 'light' | 'dark') {
|
|
260
|
+
// Modify the application context
|
|
261
|
+
this.ctx!.application.theme = theme;
|
|
262
|
+
|
|
263
|
+
// Notify all @context subscribers
|
|
264
|
+
this.ctx!.update();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Note:** The router automatically calls `update()` during navigation. Only call it manually when changing application state outside of navigation (login, logout, theme changes, etc.).
|
|
270
|
+
|
|
271
|
+
## Route Configuration
|
|
272
|
+
|
|
273
|
+
### @page Decorator Options
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
interface PageOptions<T = any> {
|
|
277
|
+
tag: string; // Custom element tag name
|
|
278
|
+
routes: string[]; // Route patterns
|
|
279
|
+
transition?: Transition; // Page-specific transition
|
|
280
|
+
guards?: Guard<T> | Guard<T>[]; // Route guards
|
|
281
|
+
placard?: Placard<T>; // Page metadata
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Multiple Routes
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
@page({
|
|
289
|
+
tag: 'user-page',
|
|
290
|
+
routes: ['/user', '/users', '/profile']
|
|
291
|
+
})
|
|
292
|
+
class UserPage extends HTMLElement {
|
|
293
|
+
@render()
|
|
294
|
+
renderContent() {
|
|
295
|
+
return html`<h1>User Page</h1>`;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Route with Parameters
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
@page({
|
|
304
|
+
tag: 'user-detail-page',
|
|
305
|
+
routes: ['/users/:userId']
|
|
306
|
+
})
|
|
307
|
+
class UserDetailPage extends HTMLElement {
|
|
308
|
+
@property()
|
|
309
|
+
userId = '';
|
|
310
|
+
|
|
311
|
+
@render()
|
|
312
|
+
renderContent() {
|
|
313
|
+
return html`
|
|
314
|
+
<div>
|
|
315
|
+
<h1>User Details</h1>
|
|
316
|
+
<p>Viewing user: ${this.userId}</p>
|
|
317
|
+
</div>
|
|
318
|
+
`;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Multiple Parameters
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
@page({
|
|
327
|
+
tag: 'post-detail-page',
|
|
328
|
+
routes: ['/users/:userId/posts/:postId']
|
|
329
|
+
})
|
|
330
|
+
class PostDetailPage extends HTMLElement {
|
|
331
|
+
@property()
|
|
332
|
+
userId = '';
|
|
333
|
+
|
|
334
|
+
@property()
|
|
335
|
+
postId = '';
|
|
336
|
+
|
|
337
|
+
@render()
|
|
338
|
+
renderContent() {
|
|
339
|
+
return html`
|
|
340
|
+
<h1>Post ${this.postId} by User ${this.userId}</h1>
|
|
341
|
+
`;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Navigation
|
|
347
|
+
|
|
348
|
+
### Hash Navigation
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
// In templates
|
|
352
|
+
html`<a href="#/about">About</a>`
|
|
353
|
+
|
|
354
|
+
// Programmatic navigation
|
|
355
|
+
navigate('/about');
|
|
356
|
+
|
|
357
|
+
// With parameters
|
|
358
|
+
navigate('/users/123');
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Pushstate Navigation
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
// In templates
|
|
365
|
+
html`<a href="/about">About</a>`
|
|
366
|
+
|
|
367
|
+
// Programmatic navigation using the router instance
|
|
368
|
+
const { navigate } = Router({
|
|
369
|
+
target: '#app',
|
|
370
|
+
type: 'pushstate'
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
navigate('/about');
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Back/Forward Navigation
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
// Browser back
|
|
380
|
+
window.history.back();
|
|
381
|
+
|
|
382
|
+
// Browser forward
|
|
383
|
+
window.history.forward();
|
|
384
|
+
|
|
385
|
+
// Go back 2 pages
|
|
386
|
+
window.history.go(-2);
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
## Route Parameters
|
|
390
|
+
|
|
391
|
+
### Accessing Parameters
|
|
392
|
+
|
|
393
|
+
Route parameters are automatically mapped to element properties:
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
@page({
|
|
397
|
+
tag: 'article-page',
|
|
398
|
+
routes: ['/articles/:articleId']
|
|
399
|
+
})
|
|
400
|
+
class ArticlePage extends HTMLElement {
|
|
401
|
+
@property()
|
|
402
|
+
articleId = '';
|
|
403
|
+
|
|
404
|
+
@ready()
|
|
405
|
+
async loadArticle() {
|
|
406
|
+
// articleId is automatically set from URL
|
|
407
|
+
const article = await fetch(`/api/articles/${this.articleId}`);
|
|
408
|
+
this.article = await article.json();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
@render()
|
|
412
|
+
renderContent() {
|
|
413
|
+
return html`<h1>Article ${this.articleId}</h1>`;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Multiple Parameters
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
@page({
|
|
422
|
+
tag: 'comment-page',
|
|
423
|
+
routes: ['/posts/:postId/comments/:commentId']
|
|
424
|
+
})
|
|
425
|
+
class CommentPage extends HTMLElement {
|
|
426
|
+
@property()
|
|
427
|
+
postId = '';
|
|
428
|
+
|
|
429
|
+
@property()
|
|
430
|
+
commentId = '';
|
|
431
|
+
|
|
432
|
+
@ready()
|
|
433
|
+
async loadData() {
|
|
434
|
+
// Both postId and commentId are set from URL
|
|
435
|
+
const [post, comment] = await Promise.all([
|
|
436
|
+
fetch(`/api/posts/${this.postId}`).then(r => r.json()),
|
|
437
|
+
fetch(`/api/comments/${this.commentId}`).then(r => r.json())
|
|
438
|
+
]);
|
|
439
|
+
|
|
440
|
+
this.post = post;
|
|
441
|
+
this.comment = comment;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
@render()
|
|
445
|
+
renderContent() {
|
|
446
|
+
return html`
|
|
447
|
+
<div>
|
|
448
|
+
<h2>Comment on Post ${this.postId}</h2>
|
|
449
|
+
<p>Comment ID: ${this.commentId}</p>
|
|
450
|
+
</div>
|
|
451
|
+
`;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Query Parameters
|
|
457
|
+
|
|
458
|
+
Query parameters are not automatically parsed but can be accessed via URL:
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
@page({
|
|
462
|
+
tag: 'search-page',
|
|
463
|
+
routes: ['/search']
|
|
464
|
+
})
|
|
465
|
+
class SearchPage extends HTMLElement {
|
|
466
|
+
@property()
|
|
467
|
+
query = '';
|
|
468
|
+
|
|
469
|
+
@property()
|
|
470
|
+
page = 1;
|
|
471
|
+
|
|
472
|
+
@ready()
|
|
473
|
+
parseQueryParams() {
|
|
474
|
+
const params = new URLSearchParams(window.location.search);
|
|
475
|
+
this.query = params.get('q') || '';
|
|
476
|
+
this.page = parseInt(params.get('page') || '1');
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
@render()
|
|
480
|
+
renderContent() {
|
|
481
|
+
return html`
|
|
482
|
+
<div>
|
|
483
|
+
<h1>Search Results for: ${this.query}</h1>
|
|
484
|
+
<p>Page: ${this.page}</p>
|
|
485
|
+
</div>
|
|
486
|
+
`;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
## Page Transitions
|
|
492
|
+
|
|
493
|
+
### Global Transitions
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
import { fadeTransition } from 'snice';
|
|
497
|
+
|
|
498
|
+
const router = Router({
|
|
499
|
+
target: '#app',
|
|
500
|
+
type: 'hash',
|
|
501
|
+
transition: fadeTransition
|
|
502
|
+
});
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Page-Specific Transitions
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
import { slideTransition } from 'snice';
|
|
509
|
+
|
|
510
|
+
@page({
|
|
511
|
+
tag: 'about-page',
|
|
512
|
+
routes: ['/about'],
|
|
513
|
+
transition: slideTransition
|
|
514
|
+
})
|
|
515
|
+
class AboutPage extends HTMLElement {
|
|
516
|
+
@render()
|
|
517
|
+
renderContent() {
|
|
518
|
+
return html`<h1>About</h1>`;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Built-in Transitions
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
import {
|
|
527
|
+
fadeTransition,
|
|
528
|
+
slideTransition,
|
|
529
|
+
slideLeftTransition,
|
|
530
|
+
slideRightTransition,
|
|
531
|
+
slideUpTransition,
|
|
532
|
+
slideDownTransition,
|
|
533
|
+
scaleTransition
|
|
534
|
+
} from 'snice';
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### Custom Transitions
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
import { Transition } from 'snice';
|
|
541
|
+
|
|
542
|
+
const customTransition: Transition = {
|
|
543
|
+
name: 'custom',
|
|
544
|
+
duration: 500,
|
|
545
|
+
enterClass: 'page-enter',
|
|
546
|
+
enterActiveClass: 'page-enter-active',
|
|
547
|
+
leaveClass: 'page-leave',
|
|
548
|
+
leaveActiveClass: 'page-leave-active'
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
// CSS for custom transition
|
|
552
|
+
/*
|
|
553
|
+
.page-enter {
|
|
554
|
+
opacity: 0;
|
|
555
|
+
transform: translateY(20px);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
.page-enter-active {
|
|
559
|
+
transition: all 500ms ease-out;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.page-leave {
|
|
563
|
+
opacity: 1;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.page-leave-active {
|
|
567
|
+
opacity: 0;
|
|
568
|
+
transition: all 500ms ease-in;
|
|
569
|
+
}
|
|
570
|
+
*/
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
## Route Guards
|
|
574
|
+
|
|
575
|
+
Guards protect routes and can redirect unauthorized access:
|
|
576
|
+
|
|
577
|
+
### Basic Guard
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
import { Guard } from 'snice';
|
|
581
|
+
|
|
582
|
+
const isAuthenticated: Guard<AppContext> = (ctx) => {
|
|
583
|
+
return ctx.getUser() !== null;
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
@page({
|
|
587
|
+
tag: 'dashboard-page',
|
|
588
|
+
routes: ['/dashboard'],
|
|
589
|
+
guards: isAuthenticated
|
|
590
|
+
})
|
|
591
|
+
class DashboardPage extends HTMLElement {
|
|
592
|
+
@render()
|
|
593
|
+
renderContent() {
|
|
594
|
+
return html`<h1>Dashboard</h1>`;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### Multiple Guards
|
|
600
|
+
|
|
601
|
+
```typescript
|
|
602
|
+
const hasAdminRole: Guard<AppContext> = (ctx) => {
|
|
603
|
+
const user = ctx.getUser();
|
|
604
|
+
return user?.role === 'admin';
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
@page({
|
|
608
|
+
tag: 'admin-page',
|
|
609
|
+
routes: ['/admin'],
|
|
610
|
+
guards: [isAuthenticated, hasAdminRole]
|
|
611
|
+
})
|
|
612
|
+
class AdminPage extends HTMLElement {
|
|
613
|
+
@render()
|
|
614
|
+
renderContent() {
|
|
615
|
+
return html`<h1>Admin Dashboard</h1>`;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
### Guard with Redirect
|
|
621
|
+
|
|
622
|
+
```typescript
|
|
623
|
+
const requiresAuth: Guard<AppContext> = (ctx) => {
|
|
624
|
+
const isAuth = ctx.getUser() !== null;
|
|
625
|
+
|
|
626
|
+
if (!isAuth) {
|
|
627
|
+
// Redirect to login page
|
|
628
|
+
setTimeout(() => {
|
|
629
|
+
window.location.hash = '#/login';
|
|
630
|
+
}, 0);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return isAuth;
|
|
634
|
+
};
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
### Async Guards
|
|
638
|
+
|
|
639
|
+
```typescript
|
|
640
|
+
const checkPermission: Guard<AppContext> = async (ctx) => {
|
|
641
|
+
const user = ctx.getUser();
|
|
642
|
+
if (!user) return false;
|
|
643
|
+
|
|
644
|
+
// Check with API
|
|
645
|
+
const response = await fetch(`/api/permissions/${user.id}`);
|
|
646
|
+
const permissions = await response.json();
|
|
647
|
+
|
|
648
|
+
return permissions.includes('access_dashboard');
|
|
649
|
+
};
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
## Layouts
|
|
653
|
+
|
|
654
|
+
Layouts wrap pages with shared UI like headers, footers, and navigation:
|
|
655
|
+
|
|
656
|
+
### Creating a Layout
|
|
657
|
+
|
|
658
|
+
```typescript
|
|
659
|
+
import { layout, render, html, styles, css, Layout } from 'snice';
|
|
660
|
+
|
|
661
|
+
@layout('app-shell')
|
|
662
|
+
class AppShell extends HTMLElement implements Layout {
|
|
663
|
+
private placards: Placard[] = [];
|
|
664
|
+
private currentRoute = '';
|
|
665
|
+
|
|
666
|
+
@render()
|
|
667
|
+
renderContent() {
|
|
668
|
+
return html`
|
|
669
|
+
<div class="app-shell">
|
|
670
|
+
<header>
|
|
671
|
+
<h1>My App</h1>
|
|
672
|
+
<nav>
|
|
673
|
+
${this.placards
|
|
674
|
+
.filter(p => p.show !== false)
|
|
675
|
+
.map(p => html`
|
|
676
|
+
<a
|
|
677
|
+
href="#/${p.name}"
|
|
678
|
+
class="${this.currentRoute === p.name ? 'active' : ''}"
|
|
679
|
+
>
|
|
680
|
+
${p.icon || ''} ${p.title}
|
|
681
|
+
</a>
|
|
682
|
+
`)}
|
|
683
|
+
</nav>
|
|
684
|
+
</header>
|
|
685
|
+
|
|
686
|
+
<main>
|
|
687
|
+
<slot name="page"></slot>
|
|
688
|
+
</main>
|
|
689
|
+
|
|
690
|
+
<footer>
|
|
691
|
+
<p>© 2024 My App</p>
|
|
692
|
+
</footer>
|
|
693
|
+
</div>
|
|
694
|
+
`;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
@styles()
|
|
698
|
+
shellStyles() {
|
|
699
|
+
return css`
|
|
700
|
+
.app-shell {
|
|
701
|
+
display: flex;
|
|
702
|
+
flex-direction: column;
|
|
703
|
+
min-height: 100vh;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
header {
|
|
707
|
+
background: #333;
|
|
708
|
+
color: white;
|
|
709
|
+
padding: 1rem;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
nav a {
|
|
713
|
+
color: white;
|
|
714
|
+
margin: 0 1rem;
|
|
715
|
+
text-decoration: none;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
nav a.active {
|
|
719
|
+
font-weight: bold;
|
|
720
|
+
text-decoration: underline;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
main {
|
|
724
|
+
flex: 1;
|
|
725
|
+
padding: 2rem;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
footer {
|
|
729
|
+
background: #f0f0f0;
|
|
730
|
+
padding: 1rem;
|
|
731
|
+
text-align: center;
|
|
732
|
+
}
|
|
733
|
+
`;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Called by router when route changes
|
|
737
|
+
update(appContext: any, placards: Placard[], currentRoute: string, routeParams: any) {
|
|
738
|
+
this.placards = placards;
|
|
739
|
+
this.currentRoute = currentRoute;
|
|
740
|
+
// Property changes trigger re-render
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
### Using a Layout
|
|
746
|
+
|
|
747
|
+
```typescript
|
|
748
|
+
const router = Router({
|
|
749
|
+
target: '#app',
|
|
750
|
+
type: 'hash',
|
|
751
|
+
layout: 'app-shell', // Layout tag name
|
|
752
|
+
context: new AppContext()
|
|
753
|
+
});
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
### Layout Interface
|
|
757
|
+
|
|
758
|
+
```typescript
|
|
759
|
+
interface Layout {
|
|
760
|
+
update(
|
|
761
|
+
appContext: any,
|
|
762
|
+
placards: Placard[],
|
|
763
|
+
currentRoute: string,
|
|
764
|
+
routeParams: Record<string, string>
|
|
765
|
+
): void;
|
|
766
|
+
}
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
### Conditional Layout
|
|
770
|
+
|
|
771
|
+
Different pages can use different layouts or no layout:
|
|
772
|
+
|
|
773
|
+
```typescript
|
|
774
|
+
// Router with default layout
|
|
775
|
+
const router = Router({
|
|
776
|
+
target: '#app',
|
|
777
|
+
layout: 'app-shell'
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
// Page without layout
|
|
781
|
+
@page({
|
|
782
|
+
tag: 'fullscreen-page',
|
|
783
|
+
routes: ['/fullscreen'],
|
|
784
|
+
layout: null // Disable layout for this page
|
|
785
|
+
})
|
|
786
|
+
class FullscreenPage extends HTMLElement {
|
|
787
|
+
@render()
|
|
788
|
+
renderContent() {
|
|
789
|
+
return html`<div>Fullscreen content</div>`;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
## Advanced Patterns
|
|
795
|
+
|
|
796
|
+
### Lazy Loading Pages
|
|
797
|
+
|
|
798
|
+
```typescript
|
|
799
|
+
@page({
|
|
800
|
+
tag: 'lazy-page',
|
|
801
|
+
routes: ['/lazy']
|
|
802
|
+
})
|
|
803
|
+
class LazyPage extends HTMLElement {
|
|
804
|
+
@property({ type: Boolean })
|
|
805
|
+
loaded = false;
|
|
806
|
+
|
|
807
|
+
@ready()
|
|
808
|
+
async loadContent() {
|
|
809
|
+
// Simulate loading external content
|
|
810
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
811
|
+
|
|
812
|
+
// Dynamically import module
|
|
813
|
+
const module = await import('./lazy-content.js');
|
|
814
|
+
module.initialize(this);
|
|
815
|
+
|
|
816
|
+
this.loaded = true;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
@render()
|
|
820
|
+
renderContent() {
|
|
821
|
+
if (!this.loaded) {
|
|
822
|
+
return html`<div>Loading...</div>`;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return html`<div>Loaded content</div>`;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
### Nested Routing
|
|
831
|
+
|
|
832
|
+
```typescript
|
|
833
|
+
// Parent page with sub-navigation
|
|
834
|
+
@page({
|
|
835
|
+
tag: 'settings-page',
|
|
836
|
+
routes: ['/settings', '/settings/:section']
|
|
837
|
+
})
|
|
838
|
+
class SettingsPage extends HTMLElement {
|
|
839
|
+
@property()
|
|
840
|
+
section = 'general';
|
|
841
|
+
|
|
842
|
+
@render()
|
|
843
|
+
renderContent() {
|
|
844
|
+
return html`
|
|
845
|
+
<div class="settings">
|
|
846
|
+
<nav>
|
|
847
|
+
<a href="#/settings/general">General</a>
|
|
848
|
+
<a href="#/settings/privacy">Privacy</a>
|
|
849
|
+
<a href="#/settings/security">Security</a>
|
|
850
|
+
</nav>
|
|
851
|
+
|
|
852
|
+
<div class="content">
|
|
853
|
+
<case ${this.section}>
|
|
854
|
+
<when value="general">
|
|
855
|
+
<div>General settings</div>
|
|
856
|
+
</when>
|
|
857
|
+
<when value="privacy">
|
|
858
|
+
<div>Privacy settings</div>
|
|
859
|
+
</when>
|
|
860
|
+
<when value="security">
|
|
861
|
+
<div>Security settings</div>
|
|
862
|
+
</when>
|
|
863
|
+
<default>
|
|
864
|
+
<div>Unknown section</div>
|
|
865
|
+
</default>
|
|
866
|
+
</case>
|
|
867
|
+
</div>
|
|
868
|
+
</div>
|
|
869
|
+
`;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
### Route-Based Data Loading
|
|
875
|
+
|
|
876
|
+
```typescript
|
|
877
|
+
@page({
|
|
878
|
+
tag: 'product-page',
|
|
879
|
+
routes: ['/products/:productId']
|
|
880
|
+
})
|
|
881
|
+
class ProductPage extends HTMLElement {
|
|
882
|
+
@property()
|
|
883
|
+
productId = '';
|
|
884
|
+
|
|
885
|
+
@property()
|
|
886
|
+
product: any = null;
|
|
887
|
+
|
|
888
|
+
@property({ type: Boolean })
|
|
889
|
+
loading = true;
|
|
890
|
+
|
|
891
|
+
@ready()
|
|
892
|
+
loadProduct() {
|
|
893
|
+
this.fetchProduct();
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
@watch('productId')
|
|
897
|
+
onProductIdChange() {
|
|
898
|
+
// Reload when productId changes
|
|
899
|
+
this.fetchProduct();
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
async fetchProduct() {
|
|
903
|
+
this.loading = true;
|
|
904
|
+
|
|
905
|
+
try {
|
|
906
|
+
const response = await fetch(`/api/products/${this.productId}`);
|
|
907
|
+
this.product = await response.json();
|
|
908
|
+
} catch (error) {
|
|
909
|
+
console.error('Failed to load product:', error);
|
|
910
|
+
} finally {
|
|
911
|
+
this.loading = false;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
@render()
|
|
916
|
+
renderContent() {
|
|
917
|
+
if (this.loading) {
|
|
918
|
+
return html`<div>Loading product...</div>`;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (!this.product) {
|
|
922
|
+
return html`<div>Product not found</div>`;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
return html`
|
|
926
|
+
<div class="product">
|
|
927
|
+
<h1>${this.product.name}</h1>
|
|
928
|
+
<p>${this.product.description}</p>
|
|
929
|
+
<span class="price">$${this.product.price}</span>
|
|
930
|
+
</div>
|
|
931
|
+
`;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
### Breadcrumb Navigation
|
|
937
|
+
|
|
938
|
+
```typescript
|
|
939
|
+
@page({
|
|
940
|
+
tag: 'breadcrumb-page',
|
|
941
|
+
routes: ['/categories/:category/products/:productId'],
|
|
942
|
+
placard: {
|
|
943
|
+
name: 'product-detail',
|
|
944
|
+
title: 'Product Details',
|
|
945
|
+
breadcrumbs: ['home', 'categories', 'products', 'product-detail']
|
|
946
|
+
}
|
|
947
|
+
})
|
|
948
|
+
class BreadcrumbPage extends HTMLElement {
|
|
949
|
+
@property()
|
|
950
|
+
category = '';
|
|
951
|
+
|
|
952
|
+
@property()
|
|
953
|
+
productId = '';
|
|
954
|
+
|
|
955
|
+
@render()
|
|
956
|
+
renderContent() {
|
|
957
|
+
return html`
|
|
958
|
+
<nav class="breadcrumbs">
|
|
959
|
+
<a href="#/">Home</a>
|
|
960
|
+
<span>/</span>
|
|
961
|
+
<a href="#/categories">Categories</a>
|
|
962
|
+
<span>/</span>
|
|
963
|
+
<a href="#/categories/${this.category}">
|
|
964
|
+
${this.category}
|
|
965
|
+
</a>
|
|
966
|
+
<span>/</span>
|
|
967
|
+
<span>${this.productId}</span>
|
|
968
|
+
</nav>
|
|
969
|
+
<div class="content">
|
|
970
|
+
<h1>Product ${this.productId} in ${this.category}</h1>
|
|
971
|
+
</div>
|
|
972
|
+
`;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
### Error Page (404)
|
|
978
|
+
|
|
979
|
+
```typescript
|
|
980
|
+
@page({
|
|
981
|
+
tag: 'not-found-page',
|
|
982
|
+
routes: ['/404', '*'] // Catch-all route
|
|
983
|
+
})
|
|
984
|
+
class NotFoundPage extends HTMLElement {
|
|
985
|
+
@render()
|
|
986
|
+
renderContent() {
|
|
987
|
+
return html`
|
|
988
|
+
<div class="not-found">
|
|
989
|
+
<h1>404 - Page Not Found</h1>
|
|
990
|
+
<p>The page you're looking for doesn't exist.</p>
|
|
991
|
+
<a href="#/">Go Home</a>
|
|
992
|
+
</div>
|
|
993
|
+
`;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
@styles()
|
|
997
|
+
errorStyles() {
|
|
998
|
+
return css`
|
|
999
|
+
.not-found {
|
|
1000
|
+
text-align: center;
|
|
1001
|
+
padding: 4rem;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
h1 {
|
|
1005
|
+
color: #e74c3c;
|
|
1006
|
+
font-size: 3rem;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
a {
|
|
1010
|
+
display: inline-block;
|
|
1011
|
+
margin-top: 2rem;
|
|
1012
|
+
padding: 0.5rem 2rem;
|
|
1013
|
+
background: #3498db;
|
|
1014
|
+
color: white;
|
|
1015
|
+
text-decoration: none;
|
|
1016
|
+
border-radius: 4px;
|
|
1017
|
+
}
|
|
1018
|
+
`;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
### Protected Route Pattern
|
|
1024
|
+
|
|
1025
|
+
```typescript
|
|
1026
|
+
// Context with auth state
|
|
1027
|
+
class AppContext {
|
|
1028
|
+
private user: User | null = null;
|
|
1029
|
+
|
|
1030
|
+
setUser(user: User | null) {
|
|
1031
|
+
this.user = user;
|
|
1032
|
+
|
|
1033
|
+
// Redirect if logged out
|
|
1034
|
+
if (!user && window.location.hash.includes('/dashboard')) {
|
|
1035
|
+
window.location.hash = '#/login';
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
getUser() {
|
|
1040
|
+
return this.user;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
isAuthenticated() {
|
|
1044
|
+
return this.user !== null;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Auth guard
|
|
1049
|
+
const requireAuth: Guard<AppContext> = (ctx) => {
|
|
1050
|
+
if (!ctx.isAuthenticated()) {
|
|
1051
|
+
window.location.hash = '#/login';
|
|
1052
|
+
return false;
|
|
1053
|
+
}
|
|
1054
|
+
return true;
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
// Protected page
|
|
1058
|
+
@page({
|
|
1059
|
+
tag: 'dashboard-page',
|
|
1060
|
+
routes: ['/dashboard'],
|
|
1061
|
+
guards: requireAuth
|
|
1062
|
+
})
|
|
1063
|
+
class DashboardPage extends HTMLElement {
|
|
1064
|
+
private appContext?: AppContext;
|
|
1065
|
+
|
|
1066
|
+
@context()
|
|
1067
|
+
handleContext(ctx: Context<AppContext>) {
|
|
1068
|
+
this.appContext = ctx.application;
|
|
1069
|
+
this.requestRender();
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
@render()
|
|
1073
|
+
renderContent() {
|
|
1074
|
+
const user = this.appContext?.getUser();
|
|
1075
|
+
|
|
1076
|
+
return html`
|
|
1077
|
+
<div>
|
|
1078
|
+
<h1>Welcome, ${user?.name}!</h1>
|
|
1079
|
+
<p>This is your dashboard</p>
|
|
1080
|
+
</div>
|
|
1081
|
+
`;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// Login page
|
|
1086
|
+
@page({
|
|
1087
|
+
tag: 'login-page',
|
|
1088
|
+
routes: ['/login']
|
|
1089
|
+
})
|
|
1090
|
+
class LoginPage extends HTMLElement {
|
|
1091
|
+
private appContext?: AppContext;
|
|
1092
|
+
|
|
1093
|
+
@context()
|
|
1094
|
+
handleContext(ctx: Context<AppContext>) {
|
|
1095
|
+
this.appContext = ctx.application;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
@render()
|
|
1099
|
+
renderContent() {
|
|
1100
|
+
return html`
|
|
1101
|
+
<form @submit=${this.handleLogin}>
|
|
1102
|
+
<input type="text" name="username" placeholder="Username" required>
|
|
1103
|
+
<input type="password" name="password" placeholder="Password" required>
|
|
1104
|
+
<button type="submit">Login</button>
|
|
1105
|
+
</form>
|
|
1106
|
+
`;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
handleLogin(e: Event) {
|
|
1110
|
+
e.preventDefault();
|
|
1111
|
+
|
|
1112
|
+
const form = e.target as HTMLFormElement;
|
|
1113
|
+
const formData = new FormData(form);
|
|
1114
|
+
|
|
1115
|
+
// Simulate login
|
|
1116
|
+
const user = {
|
|
1117
|
+
id: 1,
|
|
1118
|
+
name: formData.get('username') as string
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1121
|
+
this.appContext?.setUser(user);
|
|
1122
|
+
|
|
1123
|
+
// Redirect to dashboard
|
|
1124
|
+
window.location.hash = '#/dashboard';
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
```
|
|
1128
|
+
|
|
1129
|
+
## Best Practices
|
|
1130
|
+
|
|
1131
|
+
1. **Use semantic routes**: `/users/123` instead of `/page?id=123`
|
|
1132
|
+
2. **Leverage route parameters**: Automatically mapped to properties
|
|
1133
|
+
3. **Use guards for protection**: Keep auth logic separate from pages
|
|
1134
|
+
4. **Implement transitions**: Smooth user experience between pages
|
|
1135
|
+
5. **Use layouts efficiently**: Share common UI without duplication
|
|
1136
|
+
6. **Handle 404s**: Always include a catch-all route
|
|
1137
|
+
7. **Use context for shared state**: Avoid prop drilling
|
|
1138
|
+
8. **Lazy load when needed**: Improve initial load time
|
|
1139
|
+
9. **Type your guards**: Use TypeScript generics for context
|
|
1140
|
+
10. **Test navigation**: Ensure all routes work correctly
|
|
1141
|
+
|
|
1142
|
+
## Router API Reference
|
|
1143
|
+
|
|
1144
|
+
### Router()
|
|
1145
|
+
|
|
1146
|
+
```typescript
|
|
1147
|
+
function Router<T = any>(options: RouterOptions<T>): {
|
|
1148
|
+
page: PropertyDecorator;
|
|
1149
|
+
navigate: (path: string) => void;
|
|
1150
|
+
initialize: () => void;
|
|
1151
|
+
getCurrentRoute: () => string;
|
|
1152
|
+
getRouteParams: () => Record<string, string>;
|
|
1153
|
+
}
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
### navigate()
|
|
1157
|
+
|
|
1158
|
+
```typescript
|
|
1159
|
+
navigate(path: string): void
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
Navigates to the specified path. Uses hash (#) or pushstate depending on router type.
|
|
1163
|
+
|
|
1164
|
+
### initialize()
|
|
1165
|
+
|
|
1166
|
+
```typescript
|
|
1167
|
+
initialize(): void
|
|
1168
|
+
```
|
|
1169
|
+
|
|
1170
|
+
Initializes the router and starts listening for route changes. Must be called after all pages are defined.
|
|
1171
|
+
|
|
1172
|
+
### getCurrentRoute()
|
|
1173
|
+
|
|
1174
|
+
```typescript
|
|
1175
|
+
getCurrentRoute(): string
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
Returns the current route path.
|
|
1179
|
+
|
|
1180
|
+
### getRouteParams()
|
|
1181
|
+
|
|
1182
|
+
```typescript
|
|
1183
|
+
getRouteParams(): Record<string, string>
|
|
1184
|
+
```
|
|
1185
|
+
|
|
1186
|
+
Returns current route parameters as an object.
|