snice 7.2.0 → 7.4.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/.agents/skills/snice/SKILL.md +36 -13
- package/README.md +11 -6
- package/adapters/react/SniceProvider.js +1 -1
- package/adapters/react/SniceRouter.d.ts +2 -0
- package/adapters/react/SniceRouter.js +3 -2
- package/adapters/react/SniceRouter.js.map +1 -1
- package/adapters/react/matchRoute.d.ts +4 -1
- package/adapters/react/matchRoute.js +13 -3
- package/adapters/react/matchRoute.js.map +1 -1
- package/adapters/react/useRequestHandler.js +1 -1
- package/bin/analyzer-contracts.js +4 -1
- package/bin/project-analyzer.js +521 -20
- package/bin/snice.js +77 -13
- package/bin/templates/AI_GUIDANCE.md +6 -1
- package/bin/templates/default/README.md +1 -1
- package/bin/templates/default/src/components/app-header.ts +11 -16
- package/bin/templates/default/src/components/notification-badge.ts +5 -14
- package/bin/templates/default/src/components/search-bar.ts +21 -65
- package/bin/templates/default/src/pages/dashboard.ts +15 -11
- package/bin/templates/default/src/pages/data.ts +10 -13
- package/bin/templates/default/src/pages/notifications.ts +4 -9
- package/bin/templates/default/src/pages/profile.ts +2 -8
- package/bin/templates/default/src/pages/settings.ts +16 -16
- package/dist/app-context.d.ts +10 -10
- package/dist/cdn/accordion/snice-accordion.js +1 -1
- package/dist/cdn/action-bar/snice-action-bar.js +1 -1
- package/dist/cdn/activity-feed/snice-activity-feed.js +1 -1
- package/dist/cdn/alert/snice-alert.js +1 -1
- package/dist/cdn/app-tiles/snice-app-tiles.js +1 -1
- package/dist/cdn/approval-flow/snice-approval-flow.js +1 -1
- package/dist/cdn/audio-recorder/snice-audio-recorder.js +1 -1
- package/dist/cdn/availability/snice-availability.js +1 -1
- package/dist/cdn/avatar/snice-avatar.js +1 -1
- package/dist/cdn/avatar-group/snice-avatar-group.js +1 -1
- package/dist/cdn/badge/snice-badge.js +1 -1
- package/dist/cdn/banner/snice-banner.js +1 -1
- package/dist/cdn/binpack/snice-binpack.js +1 -1
- package/dist/cdn/book/snice-book.js +1 -1
- package/dist/cdn/booking/snice-booking.js +1 -1
- package/dist/cdn/breadcrumbs/snice-breadcrumbs.js +1 -1
- package/dist/cdn/button/snice-button.js +1 -1
- package/dist/cdn/calendar/snice-calendar.js +1 -1
- package/dist/cdn/camera/snice-camera.js +1 -1
- package/dist/cdn/camera-annotate/snice-camera-annotate.js +1 -1
- package/dist/cdn/candlestick/snice-candlestick.js +1 -1
- package/dist/cdn/card/snice-card.js +1 -1
- package/dist/cdn/carousel/snice-carousel.js +1 -1
- package/dist/cdn/cart/snice-cart.js +1 -1
- package/dist/cdn/chart/snice-chart.js +1 -1
- package/dist/cdn/chat/snice-chat.js +1 -1
- package/dist/cdn/checkbox/snice-checkbox.js +1 -1
- package/dist/cdn/chip/snice-chip.js +1 -1
- package/dist/cdn/code-block/snice-code-block.js +1 -1
- package/dist/cdn/color-display/snice-color-display.js +1 -1
- package/dist/cdn/color-picker/snice-color-picker.js +1 -1
- package/dist/cdn/command-palette/snice-command-palette.js +1 -1
- package/dist/cdn/comments/snice-comments.js +1 -1
- package/dist/cdn/countdown/snice-countdown.js +1 -1
- package/dist/cdn/cropper/snice-cropper.js +1 -1
- package/dist/cdn/data-card/snice-data-card.js +1 -1
- package/dist/cdn/date-picker/snice-date-picker.js +1 -1
- package/dist/cdn/date-range-picker/snice-date-range-picker.js +1 -1
- package/dist/cdn/date-time-picker/snice-date-time-picker.js +1 -1
- package/dist/cdn/diff/snice-diff.js +1 -1
- package/dist/cdn/divider/snice-divider.js +1 -1
- package/dist/cdn/doc/snice-doc.js +1 -1
- package/dist/cdn/draw/snice-draw.js +1 -1
- package/dist/cdn/drawer/snice-drawer.js +1 -1
- package/dist/cdn/empty-state/snice-empty-state.js +1 -1
- package/dist/cdn/estimate/snice-estimate.js +1 -1
- package/dist/cdn/file-gallery/snice-file-gallery.js +1 -1
- package/dist/cdn/file-upload/snice-file-upload.js +1 -1
- package/dist/cdn/flip-card/snice-flip-card.js +1 -1
- package/dist/cdn/flow/snice-flow.js +1 -1
- package/dist/cdn/form-layout/snice-form-layout.js +1 -1
- package/dist/cdn/funnel/snice-funnel.js +1 -1
- package/dist/cdn/gantt/snice-gantt.js +1 -1
- package/dist/cdn/gauge/snice-gauge.js +1 -1
- package/dist/cdn/grid/snice-grid.js +1 -1
- package/dist/cdn/heatmap/snice-heatmap.js +1 -1
- package/dist/cdn/image/snice-image.js +1 -1
- package/dist/cdn/input/snice-input.js +1 -1
- package/dist/cdn/invoice/snice-invoice.js +1 -1
- package/dist/cdn/kanban/snice-kanban.js +1 -1
- package/dist/cdn/key-value/snice-key-value.js +1 -1
- package/dist/cdn/kpi/snice-kpi.js +1 -1
- package/dist/cdn/layout/snice-layout.js +1 -1
- package/dist/cdn/leaderboard/snice-leaderboard.js +1 -1
- package/dist/cdn/link/snice-link.js +1 -1
- package/dist/cdn/link-preview/snice-link-preview.js +1 -1
- package/dist/cdn/list/snice-list.js +1 -1
- package/dist/cdn/location/snice-location.js +1 -1
- package/dist/cdn/login/snice-login.js +1 -1
- package/dist/cdn/map/snice-map.js +1 -1
- package/dist/cdn/markdown/snice-markdown.js +1 -1
- package/dist/cdn/masonry/snice-masonry.js +1 -1
- package/dist/cdn/menu/snice-menu.js +1 -1
- package/dist/cdn/message-strip/snice-message-strip.js +1 -1
- package/dist/cdn/modal/snice-modal.js +1 -1
- package/dist/cdn/music-player/snice-music-player.js +1 -1
- package/dist/cdn/nav/snice-nav.js +1 -1
- package/dist/cdn/network-graph/snice-network-graph.js +1 -1
- package/dist/cdn/notification-center/snice-notification-center.js +1 -1
- package/dist/cdn/order-tracker/snice-order-tracker.js +1 -1
- package/dist/cdn/org-chart/snice-org-chart.js +1 -1
- package/dist/cdn/pagination/snice-pagination.js +1 -1
- package/dist/cdn/paint/snice-paint.js +1 -1
- package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +1 -1
- package/dist/cdn/permission-matrix/snice-permission-matrix.js +1 -1
- package/dist/cdn/podcast-player/snice-podcast-player.js +1 -1
- package/dist/cdn/popover/snice-popover.js +1 -1
- package/dist/cdn/pricing-table/snice-pricing-table.js +1 -1
- package/dist/cdn/product-card/snice-product-card.js +1 -1
- package/dist/cdn/progress/snice-progress.js +1 -1
- package/dist/cdn/progress-ring/snice-progress-ring.js +1 -1
- package/dist/cdn/qr-code/snice-qr-code.js +1 -1
- package/dist/cdn/qr-reader/snice-qr-reader.js +1 -1
- package/dist/cdn/radio/snice-radio.js +1 -1
- package/dist/cdn/range-slider/snice-range-slider.js +1 -1
- package/dist/cdn/rating/snice-rating.js +1 -1
- package/dist/cdn/receipt/snice-receipt.js +1 -1
- package/dist/cdn/recipe/snice-recipe.js +1 -1
- package/dist/cdn/runtime/README.md +2 -2
- package/dist/cdn/runtime/snice-runtime.esm.js +333 -192
- package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.esm.min.js +1 -1
- package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.js +333 -191
- package/dist/cdn/runtime/snice-runtime.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.min.js +1 -1
- package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
- package/dist/cdn/sankey/snice-sankey.js +1 -1
- package/dist/cdn/segmented-control/snice-segmented-control.js +1 -1
- package/dist/cdn/select/snice-select.js +1 -1
- package/dist/cdn/skeleton/snice-skeleton.js +1 -1
- package/dist/cdn/slider/snice-slider.js +1 -1
- package/dist/cdn/sortable/snice-sortable.js +1 -1
- package/dist/cdn/sparkline/snice-sparkline.js +1 -1
- package/dist/cdn/spinner/snice-spinner.js +1 -1
- package/dist/cdn/split-button/snice-split-button.js +1 -1
- package/dist/cdn/split-pane/snice-split-pane.js +1 -1
- package/dist/cdn/spotlight/snice-spotlight.js +1 -1
- package/dist/cdn/stat-group/snice-stat-group.js +1 -1
- package/dist/cdn/step-input/snice-step-input.js +1 -1
- package/dist/cdn/stepper/snice-stepper.js +1 -1
- package/dist/cdn/switch/snice-switch.js +1 -1
- package/dist/cdn/table/snice-table.esm.js +1 -1
- package/dist/cdn/table/snice-table.js +1 -1
- package/dist/cdn/tabs/snice-tabs.js +1 -1
- package/dist/cdn/tag/snice-tag.js +1 -1
- package/dist/cdn/tag-input/snice-tag-input.js +1 -1
- package/dist/cdn/terminal/snice-terminal.js +1 -1
- package/dist/cdn/testimonial/snice-testimonial.js +1 -1
- package/dist/cdn/textarea/snice-textarea.js +1 -1
- package/dist/cdn/time-picker/snice-time-picker.js +1 -1
- package/dist/cdn/time-range-picker/snice-time-range-picker.js +1 -1
- package/dist/cdn/timeline/snice-timeline.js +1 -1
- package/dist/cdn/timer/snice-timer.js +1 -1
- package/dist/cdn/toast/snice-toast.js +1 -1
- package/dist/cdn/tooltip/snice-tooltip.js +1 -1
- package/dist/cdn/tree/snice-tree.js +1 -1
- package/dist/cdn/treemap/snice-treemap.js +1 -1
- package/dist/cdn/user-card/snice-user-card.js +1 -1
- package/dist/cdn/video-player/snice-video-player.js +1 -1
- package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +1 -1
- package/dist/cdn/waterfall/snice-waterfall.js +1 -1
- package/dist/cdn/weather/snice-weather.js +1 -1
- package/dist/cdn/work-order/snice-work-order.js +1 -1
- package/dist/components/pagination/snice-pagination.types.d.ts +5 -0
- package/dist/context-provider.d.ts +11 -1
- package/dist/context.d.ts +5 -6
- package/dist/index.cjs +330 -188
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.esm.js +330 -189
- package/dist/index.esm.js.map +1 -1
- package/dist/index.iife.js +330 -188
- package/dist/index.iife.js.map +1 -1
- package/dist/react/SniceProvider.js +1 -1
- package/dist/react/SniceRouter.d.ts +2 -0
- package/dist/react/SniceRouter.js +3 -2
- package/dist/react/SniceRouter.js.map +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/matchRoute.d.ts +4 -1
- package/dist/react/matchRoute.js +13 -3
- package/dist/react/matchRoute.js.map +1 -1
- package/dist/react/useRequestHandler.js +1 -1
- package/dist/symbols.cjs +1 -1
- package/dist/symbols.esm.js +1 -1
- package/dist/testing-dom.cjs +48 -0
- package/dist/testing-dom.cjs.map +1 -0
- package/dist/testing-dom.d.ts +23 -0
- package/dist/testing-dom.esm.js +45 -0
- package/dist/testing-dom.esm.js.map +1 -0
- package/dist/transitions.cjs +1 -1
- package/dist/transitions.esm.js +1 -1
- package/dist/types/context.d.ts +8 -8
- package/dist/types/page-options.d.ts +15 -1
- package/dist/types/router-instance.d.ts +2 -1
- package/dist/utils.d.ts +3 -1
- package/docs/ai/README.md +6 -4
- package/docs/ai/api.md +13 -2
- package/docs/ai/architecture.md +28 -11
- package/docs/ai/bindings.md +1 -1
- package/docs/ai/cli.md +26 -1
- package/docs/ai/components/badge.md +4 -0
- package/docs/ai/components/pagination.md +4 -2
- package/docs/ai/components/segmented-control.md +5 -1
- package/docs/ai/controllers.md +41 -12
- package/docs/ai/decorators.md +3 -3
- package/docs/ai/events.md +8 -2
- package/docs/ai/fetcher.md +32 -1
- package/docs/ai/lifecycle.md +4 -1
- package/docs/ai/patterns.md +9 -4
- package/docs/ai/react-integration.md +1 -0
- package/docs/ai/request-response.md +20 -6
- package/docs/ai/routing.md +33 -4
- package/docs/ai/testing.md +71 -0
- package/docs/bindings.md +1 -1
- package/docs/cli.md +27 -2
- package/docs/components/badge.md +4 -0
- package/docs/components/pagination.md +8 -5
- package/docs/components/segmented-control.md +5 -1
- package/docs/controllers.md +46 -15
- package/docs/events.md +11 -2
- package/docs/fetcher.md +32 -1
- package/docs/lifecycle.md +5 -1
- package/docs/react-integration.md +1 -0
- package/docs/request-response.md +25 -7
- package/docs/routing.md +39 -3
- package/docs/testing.md +71 -0
- package/package.json +6 -1
|
@@ -2,6 +2,8 @@ import { type ReactNode, type ReactElement, type ComponentType } from 'react';
|
|
|
2
2
|
import type { Placard } from './SniceProvider';
|
|
3
3
|
export interface RouteProps {
|
|
4
4
|
path: string;
|
|
5
|
+
/** Lower values win when route specificity ties. Declaration order is the fallback. */
|
|
6
|
+
order?: number;
|
|
5
7
|
/** React component OR Snice element tag name (string) */
|
|
6
8
|
page: ComponentType<any> | string;
|
|
7
9
|
/** Single guard function */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* snice v7.
|
|
2
|
+
* snice v7.3.0
|
|
3
3
|
* A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
|
|
4
4
|
* (c) 2024
|
|
5
5
|
* Released under the MIT License.
|
|
@@ -83,6 +83,7 @@ function SniceRouter({ mode, context = {}, layout: defaultLayout, loading, fallb
|
|
|
83
83
|
guards.push(...props.guards);
|
|
84
84
|
result.push({
|
|
85
85
|
path: props.path,
|
|
86
|
+
order: props.order,
|
|
86
87
|
page: props.page,
|
|
87
88
|
guards,
|
|
88
89
|
guardRedirect: props.guardRedirect,
|
|
@@ -99,7 +100,7 @@ function SniceRouter({ mode, context = {}, layout: defaultLayout, loading, fallb
|
|
|
99
100
|
.map((r) => r.placard);
|
|
100
101
|
}, [parsedRoutes]);
|
|
101
102
|
// Build route configs for matching
|
|
102
|
-
const routeConfigs = useMemo(() => parsedRoutes.map((r, i) => ({ path: r.path, index: i })), [parsedRoutes]);
|
|
103
|
+
const routeConfigs = useMemo(() => parsedRoutes.map((r, i) => ({ path: r.path, index: i, order: r.order })), [parsedRoutes]);
|
|
103
104
|
// Navigate function
|
|
104
105
|
const navigate = useCallback((path) => {
|
|
105
106
|
if (mode === 'hash') {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SniceRouter.js","sources":["../../src/react/SniceRouter.tsx"],"sourcesContent":["import {\n useState,\n useEffect,\n useCallback,\n useMemo,\n useRef,\n type ReactNode,\n type ReactElement,\n type ComponentType,\n Children,\n isValidElement,\n createElement,\n} from 'react';\nimport { SniceProvider } from './SniceProvider';\nimport { matchRoutes, type RouteConfig } from './matchRoute';\nimport type { Placard } from './SniceProvider';\n\n// ─── Route component (declarative config) ───\n\nexport interface RouteProps {\n path: string;\n /** React component OR Snice element tag name (string) */\n page: ComponentType<any> | string;\n /** Single guard function */\n guard?: (context: Record<string, any>, params: Record<string, string>) => boolean | Promise<boolean>;\n /** Multiple guards (AND logic) */\n guards?: Array<(context: Record<string, any>, params: Record<string, string>) => boolean | Promise<boolean>>;\n /** Redirect path if guard rejects */\n guardRedirect?: string;\n /** Layout override. Component, string (Snice tag), or false (no layout) */\n layout?: ComponentType<{ children: ReactNode }> | string | false;\n /** Page metadata for layouts */\n placard?: Placard;\n}\n\n/**\n * Route definition component. Child of <SniceRouter>.\n * Does not render anything — SniceRouter reads its props.\n */\nexport function Route(_props: RouteProps): ReactElement | null {\n return null;\n}\n\n// ─── SniceRouter ───\n\nexport interface SniceRouterProps {\n /** URL strategy */\n mode: 'hash' | 'history';\n /** Application context passed to guards, pages, layouts */\n context?: Record<string, any>;\n /** Default layout component or Snice tag name */\n layout?: ComponentType<{ children: ReactNode }> | string;\n /** Loading component shown during async guards. Component, string (Snice tag), or JSX. */\n loading?: ComponentType | string | ReactNode;\n /** Fallback when no route matches. Component, string (Snice tag), or JSX. */\n fallback?: ComponentType | string | ReactNode;\n children: ReactNode;\n}\n\ninterface ParsedRoute {\n path: string;\n page: ComponentType<any> | string;\n guards: Array<(ctx: Record<string, any>, params: Record<string, string>) => boolean | Promise<boolean>>;\n guardRedirect?: string;\n layout?: ComponentType<{ children: ReactNode }> | string | false;\n placard?: Placard;\n}\n\nfunction getPath(mode: 'hash' | 'history'): string {\n if (mode === 'hash') {\n return window.location.hash.slice(1) || '/';\n }\n return window.location.pathname;\n}\n\nfunction DefaultLoading() {\n return createElement('div', {\n style: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n height: '100%',\n minHeight: '200px',\n },\n },\n createElement('style', null, '@keyframes snice-spin{to{transform:rotate(360deg)}}'),\n createElement('div', {\n style: {\n width: '32px',\n height: '32px',\n border: '3px solid rgba(128,128,128,0.3)',\n borderTopColor: 'rgba(128,128,128,0.8)',\n borderRadius: '50%',\n animation: 'snice-spin 0.6s linear infinite',\n },\n }),\n );\n}\n\n/** Render a page/layout/loading/fallback prop that can be component, string tag, or JSX */\nfunction renderFlexible(\n value: ComponentType<any> | string | ReactNode | undefined,\n props?: Record<string, any>,\n): ReactNode {\n if (value === undefined || value === null) return null;\n\n // String = Snice web component tag name\n if (typeof value === 'string') {\n return createElement(value, props);\n }\n\n // Function = React component\n if (typeof value === 'function') {\n return createElement(value as ComponentType<any>, props);\n }\n\n // Already JSX/ReactNode\n return value;\n}\n\n/**\n * Root provider component. Manages URL state, route matching,\n * guard execution, layout selection, and context propagation.\n */\nexport function SniceRouter({\n mode,\n context = {},\n layout: defaultLayout,\n loading,\n fallback,\n children,\n}: SniceRouterProps) {\n const [currentPath, setCurrentPath] = useState(() => getPath(mode));\n const [guardState, setGuardState] = useState<'idle' | 'checking' | 'passed' | 'failed'>('idle');\n const contextRef = useRef(context);\n contextRef.current = context;\n\n // Parse Route children into config\n const parsedRoutes = useMemo<ParsedRoute[]>(() => {\n const result: ParsedRoute[] = [];\n Children.forEach(children, (child) => {\n if (!isValidElement(child) || child.type !== Route) return;\n const props = child.props as RouteProps;\n const guards: ParsedRoute['guards'] = [];\n if (props.guard) guards.push(props.guard);\n if (props.guards) guards.push(...props.guards);\n result.push({\n path: props.path,\n page: props.page,\n guards,\n guardRedirect: props.guardRedirect,\n layout: props.layout,\n placard: props.placard,\n });\n });\n return result;\n }, [children]);\n\n // Collect placards\n const placards = useMemo<Placard[]>(() => {\n return parsedRoutes\n .filter((r) => r.placard)\n .map((r) => r.placard!);\n }, [parsedRoutes]);\n\n // Build route configs for matching\n const routeConfigs = useMemo<RouteConfig[]>(\n () => parsedRoutes.map((r, i) => ({ path: r.path, index: i })),\n [parsedRoutes],\n );\n\n // Navigate function\n const navigate = useCallback(\n (path: string) => {\n if (mode === 'hash') {\n window.location.hash = path;\n } else {\n window.history.pushState(null, '', path);\n setCurrentPath(path);\n }\n },\n [mode],\n );\n\n // Listen for URL changes\n useEffect(() => {\n const handler = () => setCurrentPath(getPath(mode));\n const event = mode === 'hash' ? 'hashchange' : 'popstate';\n window.addEventListener(event, handler);\n return () => window.removeEventListener(event, handler);\n }, [mode]);\n\n // Match current path\n const match = useMemo(\n () => matchRoutes(routeConfigs, currentPath),\n [routeConfigs, currentPath],\n );\n\n const matchedRoute = match ? parsedRoutes[match.index] : null;\n // Stable params reference across renders so downstream `useMemo` deps\n // (SniceProvider value, guard effect) don't churn on every render when\n // nothing actually changed.\n const params = useMemo(\n () => match?.params ?? {},\n // Re-create only when the param content actually changes\n [JSON.stringify(match?.params ?? {})],\n );\n\n // Run guards\n useEffect(() => {\n if (!matchedRoute) {\n setGuardState('idle');\n return;\n }\n\n if (matchedRoute.guards.length === 0) {\n setGuardState('passed');\n return;\n }\n\n let cancelled = false;\n setGuardState('checking');\n\n (async () => {\n try {\n for (const guard of matchedRoute.guards) {\n const result = await guard(contextRef.current, params);\n if (cancelled) return;\n if (!result) {\n setGuardState('failed');\n if (matchedRoute.guardRedirect) {\n navigate(matchedRoute.guardRedirect);\n }\n return;\n }\n }\n if (!cancelled) setGuardState('passed');\n } catch {\n if (!cancelled) {\n setGuardState('failed');\n if (matchedRoute.guardRedirect) {\n navigate(matchedRoute.guardRedirect);\n }\n }\n }\n })();\n\n return () => { cancelled = true; };\n }, [matchedRoute, currentPath]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Determine what to render\n let content: ReactNode;\n\n if (!matchedRoute) {\n // No route match → fallback\n content = renderFlexible(fallback) ?? createElement('div', null, '404 — Page not found');\n } else if (guardState === 'checking') {\n // Async guards running → show loading\n content = renderFlexible(loading) ?? createElement(DefaultLoading);\n } else if (guardState === 'failed') {\n // Guard rejected (redirect may have fired) — render nothing\n content = null;\n } else {\n // Guard passed or no guards → render page\n content = renderFlexible(matchedRoute.page, params);\n }\n\n // Apply layout\n const layoutCandidate = matchedRoute?.layout !== undefined ? matchedRoute.layout : defaultLayout;\n const layoutToUse = layoutCandidate === false ? undefined : layoutCandidate;\n if (layoutToUse && content !== null) {\n content = renderFlexible(\n layoutToUse as ComponentType<{ children: ReactNode }> | string,\n { children: content },\n );\n }\n\n return (\n <SniceProvider\n context={context}\n navigate={navigate}\n route={match?.path ?? ''}\n params={params}\n placards={placards}\n >\n {content}\n </SniceProvider>\n );\n}\n"],"names":["_jsx"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SniceRouter.js","sources":["../../src/react/SniceRouter.tsx"],"sourcesContent":["import {\n useState,\n useEffect,\n useCallback,\n useMemo,\n useRef,\n type ReactNode,\n type ReactElement,\n type ComponentType,\n Children,\n isValidElement,\n createElement,\n} from 'react';\nimport { SniceProvider } from './SniceProvider';\nimport { matchRoutes, type RouteConfig } from './matchRoute';\nimport type { Placard } from './SniceProvider';\n\n// ─── Route component (declarative config) ───\n\nexport interface RouteProps {\n path: string;\n /** Lower values win when route specificity ties. Declaration order is the fallback. */\n order?: number;\n /** React component OR Snice element tag name (string) */\n page: ComponentType<any> | string;\n /** Single guard function */\n guard?: (context: Record<string, any>, params: Record<string, string>) => boolean | Promise<boolean>;\n /** Multiple guards (AND logic) */\n guards?: Array<(context: Record<string, any>, params: Record<string, string>) => boolean | Promise<boolean>>;\n /** Redirect path if guard rejects */\n guardRedirect?: string;\n /** Layout override. Component, string (Snice tag), or false (no layout) */\n layout?: ComponentType<{ children: ReactNode }> | string | false;\n /** Page metadata for layouts */\n placard?: Placard;\n}\n\n/**\n * Route definition component. Child of <SniceRouter>.\n * Does not render anything — SniceRouter reads its props.\n */\nexport function Route(_props: RouteProps): ReactElement | null {\n return null;\n}\n\n// ─── SniceRouter ───\n\nexport interface SniceRouterProps {\n /** URL strategy */\n mode: 'hash' | 'history';\n /** Application context passed to guards, pages, layouts */\n context?: Record<string, any>;\n /** Default layout component or Snice tag name */\n layout?: ComponentType<{ children: ReactNode }> | string;\n /** Loading component shown during async guards. Component, string (Snice tag), or JSX. */\n loading?: ComponentType | string | ReactNode;\n /** Fallback when no route matches. Component, string (Snice tag), or JSX. */\n fallback?: ComponentType | string | ReactNode;\n children: ReactNode;\n}\n\ninterface ParsedRoute {\n path: string;\n order?: number;\n page: ComponentType<any> | string;\n guards: Array<(ctx: Record<string, any>, params: Record<string, string>) => boolean | Promise<boolean>>;\n guardRedirect?: string;\n layout?: ComponentType<{ children: ReactNode }> | string | false;\n placard?: Placard;\n}\n\nfunction getPath(mode: 'hash' | 'history'): string {\n if (mode === 'hash') {\n return window.location.hash.slice(1) || '/';\n }\n return window.location.pathname;\n}\n\nfunction DefaultLoading() {\n return createElement('div', {\n style: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n height: '100%',\n minHeight: '200px',\n },\n },\n createElement('style', null, '@keyframes snice-spin{to{transform:rotate(360deg)}}'),\n createElement('div', {\n style: {\n width: '32px',\n height: '32px',\n border: '3px solid rgba(128,128,128,0.3)',\n borderTopColor: 'rgba(128,128,128,0.8)',\n borderRadius: '50%',\n animation: 'snice-spin 0.6s linear infinite',\n },\n }),\n );\n}\n\n/** Render a page/layout/loading/fallback prop that can be component, string tag, or JSX */\nfunction renderFlexible(\n value: ComponentType<any> | string | ReactNode | undefined,\n props?: Record<string, any>,\n): ReactNode {\n if (value === undefined || value === null) return null;\n\n // String = Snice web component tag name\n if (typeof value === 'string') {\n return createElement(value, props);\n }\n\n // Function = React component\n if (typeof value === 'function') {\n return createElement(value as ComponentType<any>, props);\n }\n\n // Already JSX/ReactNode\n return value;\n}\n\n/**\n * Root provider component. Manages URL state, route matching,\n * guard execution, layout selection, and context propagation.\n */\nexport function SniceRouter({\n mode,\n context = {},\n layout: defaultLayout,\n loading,\n fallback,\n children,\n}: SniceRouterProps) {\n const [currentPath, setCurrentPath] = useState(() => getPath(mode));\n const [guardState, setGuardState] = useState<'idle' | 'checking' | 'passed' | 'failed'>('idle');\n const contextRef = useRef(context);\n contextRef.current = context;\n\n // Parse Route children into config\n const parsedRoutes = useMemo<ParsedRoute[]>(() => {\n const result: ParsedRoute[] = [];\n Children.forEach(children, (child) => {\n if (!isValidElement(child) || child.type !== Route) return;\n const props = child.props as RouteProps;\n const guards: ParsedRoute['guards'] = [];\n if (props.guard) guards.push(props.guard);\n if (props.guards) guards.push(...props.guards);\n result.push({\n path: props.path,\n order: props.order,\n page: props.page,\n guards,\n guardRedirect: props.guardRedirect,\n layout: props.layout,\n placard: props.placard,\n });\n });\n return result;\n }, [children]);\n\n // Collect placards\n const placards = useMemo<Placard[]>(() => {\n return parsedRoutes\n .filter((r) => r.placard)\n .map((r) => r.placard!);\n }, [parsedRoutes]);\n\n // Build route configs for matching\n const routeConfigs = useMemo<RouteConfig[]>(\n () => parsedRoutes.map((r, i) => ({ path: r.path, index: i, order: r.order })),\n [parsedRoutes],\n );\n\n // Navigate function\n const navigate = useCallback(\n (path: string) => {\n if (mode === 'hash') {\n window.location.hash = path;\n } else {\n window.history.pushState(null, '', path);\n setCurrentPath(path);\n }\n },\n [mode],\n );\n\n // Listen for URL changes\n useEffect(() => {\n const handler = () => setCurrentPath(getPath(mode));\n const event = mode === 'hash' ? 'hashchange' : 'popstate';\n window.addEventListener(event, handler);\n return () => window.removeEventListener(event, handler);\n }, [mode]);\n\n // Match current path\n const match = useMemo(\n () => matchRoutes(routeConfigs, currentPath),\n [routeConfigs, currentPath],\n );\n\n const matchedRoute = match ? parsedRoutes[match.index] : null;\n // Stable params reference across renders so downstream `useMemo` deps\n // (SniceProvider value, guard effect) don't churn on every render when\n // nothing actually changed.\n const params = useMemo(\n () => match?.params ?? {},\n // Re-create only when the param content actually changes\n [JSON.stringify(match?.params ?? {})],\n );\n\n // Run guards\n useEffect(() => {\n if (!matchedRoute) {\n setGuardState('idle');\n return;\n }\n\n if (matchedRoute.guards.length === 0) {\n setGuardState('passed');\n return;\n }\n\n let cancelled = false;\n setGuardState('checking');\n\n (async () => {\n try {\n for (const guard of matchedRoute.guards) {\n const result = await guard(contextRef.current, params);\n if (cancelled) return;\n if (!result) {\n setGuardState('failed');\n if (matchedRoute.guardRedirect) {\n navigate(matchedRoute.guardRedirect);\n }\n return;\n }\n }\n if (!cancelled) setGuardState('passed');\n } catch {\n if (!cancelled) {\n setGuardState('failed');\n if (matchedRoute.guardRedirect) {\n navigate(matchedRoute.guardRedirect);\n }\n }\n }\n })();\n\n return () => { cancelled = true; };\n }, [matchedRoute, currentPath]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Determine what to render\n let content: ReactNode;\n\n if (!matchedRoute) {\n // No route match → fallback\n content = renderFlexible(fallback) ?? createElement('div', null, '404 — Page not found');\n } else if (guardState === 'checking') {\n // Async guards running → show loading\n content = renderFlexible(loading) ?? createElement(DefaultLoading);\n } else if (guardState === 'failed') {\n // Guard rejected (redirect may have fired) — render nothing\n content = null;\n } else {\n // Guard passed or no guards → render page\n content = renderFlexible(matchedRoute.page, params);\n }\n\n // Apply layout\n const layoutCandidate = matchedRoute?.layout !== undefined ? matchedRoute.layout : defaultLayout;\n const layoutToUse = layoutCandidate === false ? undefined : layoutCandidate;\n if (layoutToUse && content !== null) {\n content = renderFlexible(\n layoutToUse as ComponentType<{ children: ReactNode }> | string,\n { children: content },\n );\n }\n\n return (\n <SniceProvider\n context={context}\n navigate={navigate}\n route={match?.path ?? ''}\n params={params}\n placards={placards}\n >\n {content}\n </SniceProvider>\n );\n}\n"],"names":["_jsx"],"mappings":"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"}
|
package/dist/react/index.js
CHANGED
|
@@ -2,6 +2,8 @@ import { type RouteParams } from 'pica-route';
|
|
|
2
2
|
export interface RouteConfig {
|
|
3
3
|
path: string;
|
|
4
4
|
index: number;
|
|
5
|
+
/** Lower values win when route specificity ties. Defaults to 0. */
|
|
6
|
+
order?: number;
|
|
5
7
|
}
|
|
6
8
|
export interface MatchResult {
|
|
7
9
|
index: number;
|
|
@@ -11,6 +13,7 @@ export interface MatchResult {
|
|
|
11
13
|
/**
|
|
12
14
|
* Match a URL path against an array of route configs.
|
|
13
15
|
* Uses pica-route — same matching as vanilla Snice's Router.
|
|
14
|
-
* Routes are sorted by per-segment specificity (static > dynamic > wildcard)
|
|
16
|
+
* Routes are sorted by per-segment specificity (static > dynamic > wildcard),
|
|
17
|
+
* then optional lower-first order, then declaration order.
|
|
15
18
|
*/
|
|
16
19
|
export declare function matchRoutes(routes: RouteConfig[], pathname: string): MatchResult | null;
|
package/dist/react/matchRoute.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* snice v7.
|
|
2
|
+
* snice v7.3.0
|
|
3
3
|
* A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
|
|
4
4
|
* (c) 2024
|
|
5
5
|
* Released under the MIT License.
|
|
@@ -45,11 +45,21 @@ function routeSpecificity(spec) {
|
|
|
45
45
|
/**
|
|
46
46
|
* Match a URL path against an array of route configs.
|
|
47
47
|
* Uses pica-route — same matching as vanilla Snice's Router.
|
|
48
|
-
* Routes are sorted by per-segment specificity (static > dynamic > wildcard)
|
|
48
|
+
* Routes are sorted by per-segment specificity (static > dynamic > wildcard),
|
|
49
|
+
* then optional lower-first order, then declaration order.
|
|
49
50
|
*/
|
|
50
51
|
function matchRoutes(routes, pathname) {
|
|
52
|
+
for (const route of routes) {
|
|
53
|
+
if (route.order !== undefined && !Number.isFinite(route.order)) {
|
|
54
|
+
throw new TypeError(`Route order for "${route.path}" must be a finite number.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
51
57
|
// Sort by specificity (most specific first), same model as the vanilla Router.
|
|
52
|
-
const sorted =
|
|
58
|
+
const sorted = routes
|
|
59
|
+
.map((route, registrationOrder) => ({ ...route, registrationOrder }))
|
|
60
|
+
.sort((a, b) => routeSpecificity(b.path) - routeSpecificity(a.path)
|
|
61
|
+
|| (a.order ?? 0) - (b.order ?? 0)
|
|
62
|
+
|| a.registrationOrder - b.registrationOrder);
|
|
53
63
|
for (const route of sorted) {
|
|
54
64
|
const matcher = new Route(route.path);
|
|
55
65
|
const params = matcher.match(pathname);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"matchRoute.js","sources":["../../src/react/route-specificity.ts","../../src/react/matchRoute.ts"],"sourcesContent":["/**\n * Route specificity scorer for the React binding.\n *\n * IMPORTANT: keep this in exact sync with `src/route-specificity.ts`. The React\n * integration is bundled in isolation (rootDir: src/react) and cannot import\n * across that boundary, so the scorer is mirrored here. The `matchRoutes`\n * resolution tests exercise this copy, so behavioral drift is caught.\n *\n * Scoring is per-segment by ROLE, not string length: a static segment outweighs\n * a dynamic (`:param`) segment, which outweighs a wildcard (`*` / `*name`). So\n * `/post/new` beats `/post/:id`, and a splat always ranks last. Sort callers\n * descending by this score (highest = most specific = first).\n */\nconst STATIC_SEGMENT = 10;\nconst DYNAMIC_SEGMENT = 3;\nconst WILDCARD_SEGMENT = -2;\n\nexport function routeSpecificity(spec: string): number {\n const segments = spec.split('/').filter(Boolean);\n // Base on segment count so a deeper path outranks a shallower one on ties.\n let score = segments.length;\n\n for (const segment of segments) {\n if (segment.startsWith('*')) {\n score += WILDCARD_SEGMENT;\n } else if (segment.startsWith(':')) {\n score += DYNAMIC_SEGMENT;\n } else {\n score += STATIC_SEGMENT;\n }\n }\n\n return score;\n}\n","import { Route, type RouteParams } from 'pica-route';\nimport { routeSpecificity } from './route-specificity';\n\nexport interface RouteConfig {\n path: string;\n index: number;\n}\n\nexport interface MatchResult {\n index: number;\n params: RouteParams;\n path: string;\n}\n\n/**\n * Match a URL path against an array of route configs.\n * Uses pica-route — same matching as vanilla Snice's Router.\n * Routes are sorted by per-segment specificity (static > dynamic > wildcard).\n */\nexport function matchRoutes(routes: RouteConfig[], pathname: string): MatchResult | null {\n // Sort by specificity (most specific first), same model as the vanilla Router.\n const sorted =
|
|
1
|
+
{"version":3,"file":"matchRoute.js","sources":["../../src/react/route-specificity.ts","../../src/react/matchRoute.ts"],"sourcesContent":["/**\n * Route specificity scorer for the React binding.\n *\n * IMPORTANT: keep this in exact sync with `src/route-specificity.ts`. The React\n * integration is bundled in isolation (rootDir: src/react) and cannot import\n * across that boundary, so the scorer is mirrored here. The `matchRoutes`\n * resolution tests exercise this copy, so behavioral drift is caught.\n *\n * Scoring is per-segment by ROLE, not string length: a static segment outweighs\n * a dynamic (`:param`) segment, which outweighs a wildcard (`*` / `*name`). So\n * `/post/new` beats `/post/:id`, and a splat always ranks last. Sort callers\n * descending by this score (highest = most specific = first).\n */\nconst STATIC_SEGMENT = 10;\nconst DYNAMIC_SEGMENT = 3;\nconst WILDCARD_SEGMENT = -2;\n\nexport function routeSpecificity(spec: string): number {\n const segments = spec.split('/').filter(Boolean);\n // Base on segment count so a deeper path outranks a shallower one on ties.\n let score = segments.length;\n\n for (const segment of segments) {\n if (segment.startsWith('*')) {\n score += WILDCARD_SEGMENT;\n } else if (segment.startsWith(':')) {\n score += DYNAMIC_SEGMENT;\n } else {\n score += STATIC_SEGMENT;\n }\n }\n\n return score;\n}\n","import { Route, type RouteParams } from 'pica-route';\nimport { routeSpecificity } from './route-specificity';\n\nexport interface RouteConfig {\n path: string;\n index: number;\n /** Lower values win when route specificity ties. Defaults to 0. */\n order?: number;\n}\n\nexport interface MatchResult {\n index: number;\n params: RouteParams;\n path: string;\n}\n\n/**\n * Match a URL path against an array of route configs.\n * Uses pica-route — same matching as vanilla Snice's Router.\n * Routes are sorted by per-segment specificity (static > dynamic > wildcard),\n * then optional lower-first order, then declaration order.\n */\nexport function matchRoutes(routes: RouteConfig[], pathname: string): MatchResult | null {\n for (const route of routes) {\n if (route.order !== undefined && !Number.isFinite(route.order)) {\n throw new TypeError(`Route order for \"${route.path}\" must be a finite number.`);\n }\n }\n // Sort by specificity (most specific first), same model as the vanilla Router.\n const sorted = routes\n .map((route, registrationOrder) => ({ ...route, registrationOrder }))\n .sort((a, b) =>\n routeSpecificity(b.path) - routeSpecificity(a.path)\n || (a.order ?? 0) - (b.order ?? 0)\n || a.registrationOrder - b.registrationOrder\n );\n\n for (const route of sorted) {\n const matcher = new Route(route.path);\n const params = matcher.match(pathname);\n if (params !== false) {\n return {\n index: route.index,\n params: params as RouteParams,\n path: route.path,\n };\n }\n }\n\n return null;\n}\n"],"names":[],"mappings":";;;;;;;;;;AAAA;;;;;;;;;;;;AAYG;AACH,MAAM,cAAc,GAAG,EAAE;AACzB,MAAM,eAAe,GAAG,CAAC;AACzB,MAAM,gBAAgB,GAAG,EAAE;AAErB,SAAU,gBAAgB,CAAC,IAAY,EAAA;AAC3C,IAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;;AAEhD,IAAA,IAAI,KAAK,GAAG,QAAQ,CAAC,MAAM;AAE3B,IAAA,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE;AAC9B,QAAA,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE;YAC3B,KAAK,IAAI,gBAAgB;QAC3B;AAAO,aAAA,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE;YAClC,KAAK,IAAI,eAAe;QAC1B;aAAO;YACL,KAAK,IAAI,cAAc;QACzB;IACF;AAEA,IAAA,OAAO,KAAK;AACd;;ACjBA;;;;;AAKG;AACG,SAAU,WAAW,CAAC,MAAqB,EAAE,QAAgB,EAAA;AACjE,IAAA,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE;AAC1B,QAAA,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;YAC9D,MAAM,IAAI,SAAS,CAAC,CAAA,iBAAA,EAAoB,KAAK,CAAC,IAAI,CAAA,0BAAA,CAA4B,CAAC;QACjF;IACF;;IAEA,MAAM,MAAM,GAAG;AACZ,SAAA,GAAG,CAAC,CAAC,KAAK,EAAE,iBAAiB,MAAM,EAAE,GAAG,KAAK,EAAE,iBAAiB,EAAE,CAAC;SACnE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,KACT,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC,IAAI;AAC/C,WAAA,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,CAAC;AAC9B,WAAA,CAAC,CAAC,iBAAiB,GAAG,CAAC,CAAC,iBAAiB,CAC7C;AAEH,IAAA,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE;QAC1B,MAAM,OAAO,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC;QACrC,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC;AACtC,QAAA,IAAI,MAAM,KAAK,KAAK,EAAE;YACpB,OAAO;gBACL,KAAK,EAAE,KAAK,CAAC,KAAK;AAClB,gBAAA,MAAM,EAAE,MAAqB;gBAC7B,IAAI,EAAE,KAAK,CAAC,IAAI;aACjB;QACH;IACF;AAEA,IAAA,OAAO,IAAI;AACb;;;;"}
|
package/dist/symbols.cjs
CHANGED
package/dist/symbols.esm.js
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* snice v7.3.0
|
|
3
|
+
* A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
|
|
4
|
+
* (c) 2024
|
|
5
|
+
* Released under the MIT License.
|
|
6
|
+
*
|
|
7
|
+
* GENERATED FILE — DO NOT EDIT. Source: src/. Rebuild: npm run build:core
|
|
8
|
+
*/
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Standards-shaped DOM compatibility for non-browser test runners.
|
|
13
|
+
*
|
|
14
|
+
* This module only fills missing IDL surface. It never replaces an existing
|
|
15
|
+
* implementation and does not attempt to simulate layout, paint, or browser
|
|
16
|
+
* focus navigation.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Fill proven DOM-runner IDL gaps without replacing native implementations.
|
|
20
|
+
* Safe to call more than once.
|
|
21
|
+
*/
|
|
22
|
+
function installDOMTestingCompatibility(scope = globalThis) {
|
|
23
|
+
const installed = [];
|
|
24
|
+
const HTMLElementConstructor = scope.HTMLElement;
|
|
25
|
+
if (!HTMLElementConstructor)
|
|
26
|
+
return { installed };
|
|
27
|
+
const prototype = HTMLElementConstructor.prototype;
|
|
28
|
+
if (!('autofocus' in prototype)) {
|
|
29
|
+
Object.defineProperty(prototype, 'autofocus', {
|
|
30
|
+
configurable: true,
|
|
31
|
+
enumerable: true,
|
|
32
|
+
get() {
|
|
33
|
+
return this.hasAttribute('autofocus');
|
|
34
|
+
},
|
|
35
|
+
set(value) {
|
|
36
|
+
this.toggleAttribute('autofocus', Boolean(value));
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
installed.push('HTMLElement.autofocus');
|
|
40
|
+
}
|
|
41
|
+
return { installed };
|
|
42
|
+
}
|
|
43
|
+
/** Result of the side-effect installation performed by `snice/testing/dom`. */
|
|
44
|
+
const domTestingCompatibility = installDOMTestingCompatibility();
|
|
45
|
+
|
|
46
|
+
exports.domTestingCompatibility = domTestingCompatibility;
|
|
47
|
+
exports.installDOMTestingCompatibility = installDOMTestingCompatibility;
|
|
48
|
+
//# sourceMappingURL=testing-dom.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing-dom.cjs","sources":["../src/testing-dom.ts"],"sourcesContent":["/**\n * Standards-shaped DOM compatibility for non-browser test runners.\n *\n * This module only fills missing IDL surface. It never replaces an existing\n * implementation and does not attempt to simulate layout, paint, or browser\n * focus navigation.\n */\n\nexport type DOMTestingCompatibilityFeature = 'HTMLElement.autofocus';\n\nexport interface DOMTestingCompatibilityReport {\n /** Standards features installed because the current DOM omitted them. */\n installed: readonly DOMTestingCompatibilityFeature[];\n}\n\ninterface DOMTestingGlobal {\n HTMLElement?: typeof HTMLElement;\n}\n\n/**\n * Fill proven DOM-runner IDL gaps without replacing native implementations.\n * Safe to call more than once.\n */\nexport function installDOMTestingCompatibility(\n scope: DOMTestingGlobal = globalThis as DOMTestingGlobal\n): DOMTestingCompatibilityReport {\n const installed: DOMTestingCompatibilityFeature[] = [];\n const HTMLElementConstructor = scope.HTMLElement;\n if (!HTMLElementConstructor) return { installed };\n\n const prototype = HTMLElementConstructor.prototype;\n if (!('autofocus' in prototype)) {\n Object.defineProperty(prototype, 'autofocus', {\n configurable: true,\n enumerable: true,\n get(this: HTMLElement): boolean {\n return this.hasAttribute('autofocus');\n },\n set(this: HTMLElement, value: boolean) {\n this.toggleAttribute('autofocus', Boolean(value));\n },\n });\n installed.push('HTMLElement.autofocus');\n }\n\n return { installed };\n}\n\n/** Result of the side-effect installation performed by `snice/testing/dom`. */\nexport const domTestingCompatibility = installDOMTestingCompatibility();\n"],"names":[],"mappings":";;;;;;;;;;AAAA;;;;;;AAMG;AAaH;;;AAGG;AACG,SAAU,8BAA8B,CAC5C,KAAA,GAA0B,UAA8B,EAAA;IAExD,MAAM,SAAS,GAAqC,EAAE;AACtD,IAAA,MAAM,sBAAsB,GAAG,KAAK,CAAC,WAAW;AAChD,IAAA,IAAI,CAAC,sBAAsB;QAAE,OAAO,EAAE,SAAS,EAAE;AAEjD,IAAA,MAAM,SAAS,GAAG,sBAAsB,CAAC,SAAS;AAClD,IAAA,IAAI,EAAE,WAAW,IAAI,SAAS,CAAC,EAAE;AAC/B,QAAA,MAAM,CAAC,cAAc,CAAC,SAAS,EAAE,WAAW,EAAE;AAC5C,YAAA,YAAY,EAAE,IAAI;AAClB,YAAA,UAAU,EAAE,IAAI;YAChB,GAAG,GAAA;AACD,gBAAA,OAAO,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC;YACvC,CAAC;AACD,YAAA,GAAG,CAAoB,KAAc,EAAA;gBACnC,IAAI,CAAC,eAAe,CAAC,WAAW,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;YACnD,CAAC;AACF,SAAA,CAAC;AACF,QAAA,SAAS,CAAC,IAAI,CAAC,uBAAuB,CAAC;IACzC;IAEA,OAAO,EAAE,SAAS,EAAE;AACtB;AAEA;AACO,MAAM,uBAAuB,GAAG,8BAA8B;;;;;"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standards-shaped DOM compatibility for non-browser test runners.
|
|
3
|
+
*
|
|
4
|
+
* This module only fills missing IDL surface. It never replaces an existing
|
|
5
|
+
* implementation and does not attempt to simulate layout, paint, or browser
|
|
6
|
+
* focus navigation.
|
|
7
|
+
*/
|
|
8
|
+
export type DOMTestingCompatibilityFeature = 'HTMLElement.autofocus';
|
|
9
|
+
export interface DOMTestingCompatibilityReport {
|
|
10
|
+
/** Standards features installed because the current DOM omitted them. */
|
|
11
|
+
installed: readonly DOMTestingCompatibilityFeature[];
|
|
12
|
+
}
|
|
13
|
+
interface DOMTestingGlobal {
|
|
14
|
+
HTMLElement?: typeof HTMLElement;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Fill proven DOM-runner IDL gaps without replacing native implementations.
|
|
18
|
+
* Safe to call more than once.
|
|
19
|
+
*/
|
|
20
|
+
export declare function installDOMTestingCompatibility(scope?: DOMTestingGlobal): DOMTestingCompatibilityReport;
|
|
21
|
+
/** Result of the side-effect installation performed by `snice/testing/dom`. */
|
|
22
|
+
export declare const domTestingCompatibility: DOMTestingCompatibilityReport;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* snice v7.3.0
|
|
3
|
+
* A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
|
|
4
|
+
* (c) 2024
|
|
5
|
+
* Released under the MIT License.
|
|
6
|
+
*
|
|
7
|
+
* GENERATED FILE — DO NOT EDIT. Source: src/. Rebuild: npm run build:core
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Standards-shaped DOM compatibility for non-browser test runners.
|
|
11
|
+
*
|
|
12
|
+
* This module only fills missing IDL surface. It never replaces an existing
|
|
13
|
+
* implementation and does not attempt to simulate layout, paint, or browser
|
|
14
|
+
* focus navigation.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Fill proven DOM-runner IDL gaps without replacing native implementations.
|
|
18
|
+
* Safe to call more than once.
|
|
19
|
+
*/
|
|
20
|
+
function installDOMTestingCompatibility(scope = globalThis) {
|
|
21
|
+
const installed = [];
|
|
22
|
+
const HTMLElementConstructor = scope.HTMLElement;
|
|
23
|
+
if (!HTMLElementConstructor)
|
|
24
|
+
return { installed };
|
|
25
|
+
const prototype = HTMLElementConstructor.prototype;
|
|
26
|
+
if (!('autofocus' in prototype)) {
|
|
27
|
+
Object.defineProperty(prototype, 'autofocus', {
|
|
28
|
+
configurable: true,
|
|
29
|
+
enumerable: true,
|
|
30
|
+
get() {
|
|
31
|
+
return this.hasAttribute('autofocus');
|
|
32
|
+
},
|
|
33
|
+
set(value) {
|
|
34
|
+
this.toggleAttribute('autofocus', Boolean(value));
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
installed.push('HTMLElement.autofocus');
|
|
38
|
+
}
|
|
39
|
+
return { installed };
|
|
40
|
+
}
|
|
41
|
+
/** Result of the side-effect installation performed by `snice/testing/dom`. */
|
|
42
|
+
const domTestingCompatibility = installDOMTestingCompatibility();
|
|
43
|
+
|
|
44
|
+
export { domTestingCompatibility, installDOMTestingCompatibility };
|
|
45
|
+
//# sourceMappingURL=testing-dom.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing-dom.esm.js","sources":["../src/testing-dom.ts"],"sourcesContent":["/**\n * Standards-shaped DOM compatibility for non-browser test runners.\n *\n * This module only fills missing IDL surface. It never replaces an existing\n * implementation and does not attempt to simulate layout, paint, or browser\n * focus navigation.\n */\n\nexport type DOMTestingCompatibilityFeature = 'HTMLElement.autofocus';\n\nexport interface DOMTestingCompatibilityReport {\n /** Standards features installed because the current DOM omitted them. */\n installed: readonly DOMTestingCompatibilityFeature[];\n}\n\ninterface DOMTestingGlobal {\n HTMLElement?: typeof HTMLElement;\n}\n\n/**\n * Fill proven DOM-runner IDL gaps without replacing native implementations.\n * Safe to call more than once.\n */\nexport function installDOMTestingCompatibility(\n scope: DOMTestingGlobal = globalThis as DOMTestingGlobal\n): DOMTestingCompatibilityReport {\n const installed: DOMTestingCompatibilityFeature[] = [];\n const HTMLElementConstructor = scope.HTMLElement;\n if (!HTMLElementConstructor) return { installed };\n\n const prototype = HTMLElementConstructor.prototype;\n if (!('autofocus' in prototype)) {\n Object.defineProperty(prototype, 'autofocus', {\n configurable: true,\n enumerable: true,\n get(this: HTMLElement): boolean {\n return this.hasAttribute('autofocus');\n },\n set(this: HTMLElement, value: boolean) {\n this.toggleAttribute('autofocus', Boolean(value));\n },\n });\n installed.push('HTMLElement.autofocus');\n }\n\n return { installed };\n}\n\n/** Result of the side-effect installation performed by `snice/testing/dom`. */\nexport const domTestingCompatibility = installDOMTestingCompatibility();\n"],"names":[],"mappings":";;;;;;;;AAAA;;;;;;AAMG;AAaH;;;AAGG;AACG,SAAU,8BAA8B,CAC5C,KAAA,GAA0B,UAA8B,EAAA;IAExD,MAAM,SAAS,GAAqC,EAAE;AACtD,IAAA,MAAM,sBAAsB,GAAG,KAAK,CAAC,WAAW;AAChD,IAAA,IAAI,CAAC,sBAAsB;QAAE,OAAO,EAAE,SAAS,EAAE;AAEjD,IAAA,MAAM,SAAS,GAAG,sBAAsB,CAAC,SAAS;AAClD,IAAA,IAAI,EAAE,WAAW,IAAI,SAAS,CAAC,EAAE;AAC/B,QAAA,MAAM,CAAC,cAAc,CAAC,SAAS,EAAE,WAAW,EAAE;AAC5C,YAAA,YAAY,EAAE,IAAI;AAClB,YAAA,UAAU,EAAE,IAAI;YAChB,GAAG,GAAA;AACD,gBAAA,OAAO,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC;YACvC,CAAC;AACD,YAAA,GAAG,CAAoB,KAAc,EAAA;gBACnC,IAAI,CAAC,eAAe,CAAC,WAAW,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;YACnD,CAAC;AACF,SAAA,CAAC;AACF,QAAA,SAAS,CAAC,IAAI,CAAC,uBAAuB,CAAC;IACzC;IAEA,OAAO,EAAE,SAAS,EAAE;AACtB;AAEA;AACO,MAAM,uBAAuB,GAAG,8BAA8B;;;;"}
|
package/dist/transitions.cjs
CHANGED
package/dist/transitions.esm.js
CHANGED
package/dist/types/context.d.ts
CHANGED
|
@@ -9,21 +9,21 @@ declare const REGISTERED_ELEMENTS_SET: unique symbol;
|
|
|
9
9
|
* Represents the bundled router state that can notify registered elements of changes
|
|
10
10
|
*/
|
|
11
11
|
export declare class Context {
|
|
12
|
-
[REGISTERED_ELEMENTS]: WeakMap<
|
|
12
|
+
[REGISTERED_ELEMENTS]: WeakMap<object, Set<string>>;
|
|
13
13
|
[IS_UPDATING]: boolean;
|
|
14
14
|
/**
|
|
15
|
-
* Register an element to receive context updates
|
|
15
|
+
* Register an element or controller to receive context updates
|
|
16
16
|
* @internal Used by @context decorator
|
|
17
17
|
*/
|
|
18
|
-
[CONTEXT_REGISTER]: (element:
|
|
18
|
+
[CONTEXT_REGISTER]: (element: object, methodName: string) => void;
|
|
19
19
|
/**
|
|
20
|
-
* Unregister
|
|
20
|
+
* Unregister a subscriber from context updates. With a methodName,
|
|
21
21
|
* removes only that one handler (so a `once` handler tearing itself down
|
|
22
22
|
* doesn't kill the element's other @context handlers); without one, removes
|
|
23
|
-
* the
|
|
23
|
+
* the subscriber entirely (used on disconnect/detach).
|
|
24
24
|
* @internal Used by @context decorator cleanup
|
|
25
25
|
*/
|
|
26
|
-
[CONTEXT_UNREGISTER]: (element:
|
|
26
|
+
[CONTEXT_UNREGISTER]: (element: object, methodName?: string) => void;
|
|
27
27
|
/**
|
|
28
28
|
* Update the context and notify all registered elements
|
|
29
29
|
* Prevents infinite loops by tracking update state
|
|
@@ -31,10 +31,10 @@ export declare class Context {
|
|
|
31
31
|
*/
|
|
32
32
|
[CONTEXT_UPDATE]: (context: AppContext, placards: Placard[], currentRoute: string, routeParams: RouteParams) => void;
|
|
33
33
|
/**
|
|
34
|
-
* Notify a specific
|
|
34
|
+
* Notify a specific subscriber of the current context state
|
|
35
35
|
* @internal Used by @context decorator
|
|
36
36
|
*/
|
|
37
|
-
[CONTEXT_NOTIFY_ELEMENT]: (element:
|
|
37
|
+
[CONTEXT_NOTIFY_ELEMENT]: (element: object) => void;
|
|
38
38
|
private [REGISTERED_ELEMENTS_SET];
|
|
39
39
|
/**
|
|
40
40
|
* Unique immutable identifier for this context instance
|
|
@@ -2,6 +2,16 @@ import { Transition } from './transition';
|
|
|
2
2
|
import { Placard } from './placard';
|
|
3
3
|
import { Guard } from './guard';
|
|
4
4
|
import { AppContext } from './app-context';
|
|
5
|
+
export interface PageRouteOptions {
|
|
6
|
+
/** Route pattern matched by the router. */
|
|
7
|
+
path: string;
|
|
8
|
+
/**
|
|
9
|
+
* Optional priority used only when route specificity ties.
|
|
10
|
+
* Lower numbers match first. Equal or omitted values preserve registration
|
|
11
|
+
* order, including the order of entries in PageOptions.routes.
|
|
12
|
+
*/
|
|
13
|
+
order?: number;
|
|
14
|
+
}
|
|
5
15
|
export interface PageOptions {
|
|
6
16
|
/**
|
|
7
17
|
* The tag name of the custom element.
|
|
@@ -11,9 +21,13 @@ export interface PageOptions {
|
|
|
11
21
|
tag: string;
|
|
12
22
|
/**
|
|
13
23
|
* The routes that will trigger the page element.
|
|
24
|
+
* String entries are the normal form. Their array order breaks specificity
|
|
25
|
+
* ties. Use an object entry only when an explicit order is needed across
|
|
26
|
+
* registrations.
|
|
14
27
|
* @example { routes: ['/login', '/login/:id'] }
|
|
28
|
+
* @example { routes: [{ path: '/login', order: 10 }] }
|
|
15
29
|
*/
|
|
16
|
-
routes: string
|
|
30
|
+
routes: Array<string | PageRouteOptions>;
|
|
17
31
|
/**
|
|
18
32
|
* Optional per-page transition override
|
|
19
33
|
*/
|
|
@@ -2,11 +2,12 @@ import { PageOptions } from './page-options';
|
|
|
2
2
|
import { Guard } from './guard';
|
|
3
3
|
import { Transition } from './transition';
|
|
4
4
|
import { AppContext } from './app-context';
|
|
5
|
+
import { Placard } from './placard';
|
|
5
6
|
export interface RouterInstance {
|
|
6
7
|
page: (pageOptions: PageOptions) => <C extends {
|
|
7
8
|
new (...args: any[]): HTMLElement;
|
|
8
9
|
}>(constructor: C, context: ClassDecoratorContext) => C;
|
|
9
10
|
initialize: () => void;
|
|
10
11
|
navigate: (path: string) => Promise<void>;
|
|
11
|
-
register: (route: string, tag: string, transition?: Transition, guards?: Guard<AppContext> | Guard<AppContext>[]) => void;
|
|
12
|
+
register: (route: string, tag: string, transition?: Transition, guards?: Guard<AppContext> | Guard<AppContext>[], layout?: string | false, placard?: Placard | ((context: AppContext) => Placard), order?: number) => void;
|
|
12
13
|
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -101,7 +101,9 @@ export declare function invokeImmediateWatchers(instance: any, constructor: any)
|
|
|
101
101
|
/**
|
|
102
102
|
* Create a debounced version of a function
|
|
103
103
|
*/
|
|
104
|
-
export declare function createDebounced<T extends (...args: any[]) => any>(fn: T, delay: number): T
|
|
104
|
+
export declare function createDebounced<T extends (...args: any[]) => any>(fn: T, delay: number): T & {
|
|
105
|
+
cancel(): void;
|
|
106
|
+
};
|
|
105
107
|
/**
|
|
106
108
|
* Create a throttled version of a function (leading + trailing edge)
|
|
107
109
|
*/
|
package/docs/ai/README.md
CHANGED
|
@@ -101,13 +101,15 @@ Runtime first, then one bundle per component (any order after runtime):
|
|
|
101
101
|
- Components re-render on any property change (decorated or not)
|
|
102
102
|
|
|
103
103
|
**Architecture:**
|
|
104
|
-
- **Elements
|
|
104
|
+
- **Elements own visual behavior** - no fetch(), no API calls, no backend logic
|
|
105
105
|
- Elements receive data via properties, emit events for actions
|
|
106
|
-
- **
|
|
107
|
-
- **
|
|
106
|
+
- **Controllers own application behavior specific to a set of elements** - data, APIs, storage, business rules, and app-specific reactions that exist for those elements; bind by class in templates (`controller=${MyController}`, preferred) or by name in raw HTML (`controller="name"`)
|
|
107
|
+
- **Pages own element orchestration** - compose elements, pass properties, handle events, bind controllers, and coordinate the screen; routing is one concern, not the definition of the role
|
|
108
108
|
- **Daemons hold app-owned state/lifecycle** - construct explicitly, provide through `context.daemons`, communicate by address without importing implementations
|
|
109
109
|
- Conventional folders: `src/pages`, `src/components`, `src/controllers`, `src/daemons`; construct the Router in `src/router.ts` and initialize it from `src/main.ts`
|
|
110
|
-
-
|
|
110
|
+
- Do not attach a controller to a page host; keep the ownership rules distinct: visual behavior in elements, element-specific application behavior in controllers, element orchestration in pages
|
|
111
|
+
- A host-free reusable function may stay a plain module wherever the project keeps it; state plus lifecycle belongs in a daemon
|
|
112
|
+
- Declare query parameters in `@page({ routes: ['/search?q=:query'] })`; do not build a URL-parsing controller for a page
|
|
111
113
|
- Put API calls in pages/controllers/services, not in elements
|
|
112
114
|
|
|
113
115
|
**Properties:**
|
package/docs/ai/api.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
shadow?: 'open' | 'closed' | false,
|
|
10
10
|
delegatesFocus?: boolean
|
|
11
11
|
}) // Define custom element
|
|
12
|
-
@page({ tag, routes, guards?, placard? }) //
|
|
12
|
+
@page({ tag, routes, guards?, placard? }) // Ordered string routes; optional { path, order? } tie-break entries
|
|
13
13
|
@controller('name') // Define behavior module (required on every controller class)
|
|
14
14
|
// Attach: html`<x-el controller=${MyController}></x-el>` (class, preferred; custom + native els)
|
|
15
15
|
// attachController(el, MyController) | el.controller = MyController
|
|
@@ -122,7 +122,7 @@ html`
|
|
|
122
122
|
@dispatch(eventName: string, options?: { debounce?, throttle?, dispatchOnUndefined?, scope?, daemon?, ...EventInit })
|
|
123
123
|
// Fires CustomEvent after method, detail = return value
|
|
124
124
|
// Supports async methods (dispatches after promise resolves)
|
|
125
|
-
// dispatchOnUndefined: false
|
|
125
|
+
// dispatchOnUndefined: false — skips when return is undefined; omitted/true still dispatches
|
|
126
126
|
// scope: 'global' | selector | EventTarget | () => EventTarget | null — redirects dispatch target
|
|
127
127
|
```
|
|
128
128
|
|
|
@@ -178,6 +178,8 @@ html`
|
|
|
178
178
|
// Example:
|
|
179
179
|
// @respond('fetch-user')
|
|
180
180
|
// async handleFetchUser(payload: { id: string }) {
|
|
181
|
+
// const fetch = getContextFetch(this);
|
|
182
|
+
// if (!fetch) throw new Error('Controller requires context fetch');
|
|
181
183
|
// const user = await fetch(`/api/users/${payload.id}`).then(r => r.json());
|
|
182
184
|
// return user;
|
|
183
185
|
// }
|
|
@@ -191,6 +193,14 @@ const context = getContext(elementOrController);
|
|
|
191
193
|
// Provide before connect/attach. Call release() during app/test teardown.
|
|
192
194
|
```
|
|
193
195
|
|
|
196
|
+
Context transport:
|
|
197
|
+
```typescript
|
|
198
|
+
const release = provideContext(appRoot, appContext, { fetch: appFetch });
|
|
199
|
+
const fetch = getContextFetch(elementOrController);
|
|
200
|
+
// Router supplies its ContextAwareFetcher-bound Context.fetch automatically.
|
|
201
|
+
// AppContext is not mutated with a reserved transport key.
|
|
202
|
+
```
|
|
203
|
+
|
|
194
204
|
## Observers
|
|
195
205
|
|
|
196
206
|
```typescript
|
|
@@ -222,6 +232,7 @@ const context = getContext(elementOrController);
|
|
|
222
232
|
|
|
223
233
|
@context(options?: { debounce?, throttle?, once? })
|
|
224
234
|
// Method decorator: receives Context on navigation and ctx.update() calls
|
|
235
|
+
// Supported on pages, descendant elements, and attached controllers.
|
|
225
236
|
// Called on: initial load, route change, ctx.update()
|
|
226
237
|
// Example:
|
|
227
238
|
// @context() handleContext(ctx: Context) {
|
package/docs/ai/architecture.md
CHANGED
|
@@ -4,11 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
**Separation of concerns:**
|
|
6
6
|
- **Cross-cutting:** Router + global context
|
|
7
|
-
- **
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
7
|
+
- **Elements:** Visual behavior and semantic UI contracts — rendering, internal DOM, focus, keyboard behavior, and visual state
|
|
8
|
+
- **Controllers:** Application behavior specific to a set of elements — data, APIs, storage, business rules, and app-specific reactions that exist for those elements
|
|
9
|
+
- **Pages:** Element orchestration — compose elements, pass properties, handle events, bind controllers, and coordinate the screen
|
|
10
|
+
- **Plain modules:** Host-free reusable functions with explicit inputs; use the project's own folder convention
|
|
10
11
|
- **Daemons:** Explicitly constructed app-owned state/lifecycle objects — addressable through context, never global singletons
|
|
11
12
|
|
|
13
|
+
These are ownership rules, not synonyms: visual behavior belongs in the element,
|
|
14
|
+
application behavior specific to a set of elements belongs in a controller, and
|
|
15
|
+
element orchestration belongs in the page. Routing is a page concern, but it does
|
|
16
|
+
not define the page role.
|
|
17
|
+
|
|
12
18
|
**Generic vs Specific:** Elements say *what* they need, controllers decide *how*.
|
|
13
19
|
Swap controllers to change behavior without touching the component.
|
|
14
20
|
Mock controller for tests, real API controller in production — same element.
|
|
@@ -27,16 +33,23 @@ Conventional application layout:
|
|
|
27
33
|
src/
|
|
28
34
|
main.ts
|
|
29
35
|
router.ts
|
|
30
|
-
pages/ # @page classes;
|
|
36
|
+
pages/ # @page classes; element orchestration and routing
|
|
31
37
|
components/ # reusable @element visual components
|
|
32
38
|
controllers/ # @controller data, API, and reusable business behavior
|
|
33
39
|
daemons/ # explicitly constructed @daemon state/lifecycle services
|
|
34
40
|
```
|
|
35
41
|
|
|
36
|
-
Pages
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
Pages orchestrate elements. Do not call `attachController(this, ...)` or assign
|
|
43
|
+
`this.controller` on a page; that creates a second owner instead of preserving
|
|
44
|
+
the page's orchestration role. Keep visual behavior in the element. Put
|
|
45
|
+
application behavior specific to a set of elements in a controller and bind it
|
|
46
|
+
where the page composes those elements. A host-free reusable function may stay a
|
|
47
|
+
plain module in the project's chosen location.
|
|
48
|
+
|
|
49
|
+
Declare route and query state in `@page({ routes })` rather than giving a
|
|
50
|
+
controller `URLSearchParams`, `location`, or `history` responsibilities.
|
|
51
|
+
`snice check` reports these architecture mistakes and conservative page
|
|
52
|
+
decomposition suggestions.
|
|
40
53
|
|
|
41
54
|
## Rendering System
|
|
42
55
|
|
|
@@ -90,8 +103,9 @@ as architecture suggestions, not errors.
|
|
|
90
103
|
2. ControllerPart commit / attributeChangedCallback detects change
|
|
91
104
|
3. Detach old controller (if any)
|
|
92
105
|
4. Attach new controller (class refs skip the registry; reference-deduped)
|
|
93
|
-
5.
|
|
94
|
-
6.
|
|
106
|
+
5. Register `@context` and catch up with the current Router context
|
|
107
|
+
6. Set up `@observe`, `@respond`, and `@on` handlers
|
|
108
|
+
7. On detach: clean up every managed decorator
|
|
95
109
|
- Class bindings own the element; their decorator name is reflected as a diagnostic-only `controller="name"` marker
|
|
96
110
|
- Native elements: class bindings attach via ControllerPart; string attrs via MutationObserver
|
|
97
111
|
|
|
@@ -171,9 +185,12 @@ async *fetchData(): Response<Data> {
|
|
|
171
185
|
}
|
|
172
186
|
|
|
173
187
|
// Controller responds (receives payload, returns result directly)
|
|
188
|
+
private ctx!: Context;
|
|
189
|
+
@context() receiveContext(ctx: Context) { this.ctx = ctx; }
|
|
190
|
+
|
|
174
191
|
@respond('fetch-data')
|
|
175
192
|
async handleFetch(payload: { id: string }) {
|
|
176
|
-
return await fetch(`/api/${payload.id}`).then(r => r.json());
|
|
193
|
+
return await this.ctx.fetch(`/api/${payload.id}`).then(r => r.json());
|
|
177
194
|
}
|
|
178
195
|
// Wiring: html`<my-element controller=${MyController}></my-element>` (or controller="my-controller" in raw HTML)
|
|
179
196
|
```
|