snice 7.2.0 → 7.3.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 +40 -13
- package/README.md +6 -5
- 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 +2 -1
- package/bin/project-analyzer.js +302 -14
- 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/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 +28 -8
- 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 +28 -8
- 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/index.cjs +25 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +25 -5
- package/dist/index.esm.js.map +1 -1
- package/dist/index.iife.js +25 -5
- 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/transitions.cjs +1 -1
- package/dist/transitions.esm.js +1 -1
- package/dist/types/page-options.d.ts +15 -1
- package/dist/types/router-instance.d.ts +2 -1
- package/docs/ai/README.md +6 -4
- package/docs/ai/api.md +1 -1
- package/docs/ai/architecture.md +21 -8
- package/docs/ai/cli.md +4 -1
- package/docs/ai/controllers.md +11 -1
- package/docs/ai/decorators.md +1 -1
- package/docs/ai/react-integration.md +1 -0
- package/docs/ai/routing.md +25 -3
- package/docs/cli.md +4 -2
- package/docs/controllers.md +16 -4
- package/docs/react-integration.md +1 -0
- package/docs/routing.md +29 -2
- package/package.json +1 -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.2.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.2.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
package/dist/transitions.cjs
CHANGED
package/dist/transitions.esm.js
CHANGED
|
@@ -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/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
|
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
|
|
package/docs/ai/cli.md
CHANGED
|
@@ -52,7 +52,10 @@ Non-blocking architecture suggestions:
|
|
|
52
52
|
- `@element` -> `src/components/`
|
|
53
53
|
- `@controller` -> `src/controllers/`
|
|
54
54
|
- `@daemon` -> `src/daemons/`
|
|
55
|
-
-
|
|
55
|
+
- visual behavior -> element
|
|
56
|
+
- application behavior specific to a set of elements -> controller
|
|
57
|
+
- element orchestration -> page; do not attach a controller to the page host
|
|
58
|
+
- host-free reusable function -> plain module in the project's chosen location
|
|
56
59
|
|
|
57
60
|
Both `doctor` and `validate` accept `--json`.
|
|
58
61
|
|
package/docs/ai/controllers.md
CHANGED
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Mirrors `docs/controllers.md`.
|
|
4
4
|
|
|
5
|
-
Controllers
|
|
5
|
+
Controllers hold application behavior specific to a set of elements, including
|
|
6
|
+
their data fetching, business rules, and server communication. Attach to any
|
|
7
|
+
HTML element, including native elements.
|
|
8
|
+
|
|
9
|
+
Visual behavior belongs in elements, application behavior specific to a set of
|
|
10
|
+
elements belongs in a controller, and element orchestration belongs in pages.
|
|
11
|
+
Do not attach a controller to the page host. A host-free reusable function may
|
|
12
|
+
stay a plain module wherever the project keeps it. URL/query parsing belongs in
|
|
13
|
+
`@page({ routes })`, not in a controller.
|
|
6
14
|
|
|
7
15
|
## Basic Usage
|
|
8
16
|
|
|
@@ -79,6 +87,8 @@ Exception to step 4: `await attachController(this, ControllerClass)` inside the
|
|
|
79
87
|
host's own `@ready` handler attaches immediately because initial render is
|
|
80
88
|
already complete. Awaiting that same host's `ready` would self-deadlock. An
|
|
81
89
|
attachment targeting any other element still waits for the target's `ready`.
|
|
90
|
+
This runtime safeguard does not make attaching a controller to a routed page a
|
|
91
|
+
good architecture; pages should orchestrate directly.
|
|
82
92
|
|
|
83
93
|
**Detachment flow:**
|
|
84
94
|
1. `detach()` called
|
package/docs/ai/decorators.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Class
|
|
4
4
|
- `@element('tag-name', options?)` - Custom element. Options: `{ formAssociated?, renderRoot?: 'shadow'|'light', shadow?: 'open'|'closed'|false, delegatesFocus? }`
|
|
5
|
-
- `@page({ tag, routes, guards?, placard? })` - Routable page
|
|
5
|
+
- `@page({ tag, routes, guards?, placard? })` - Routable page. `routes` normally uses ordered strings; optional `{ path, order? }` entries explicitly break specificity ties.
|
|
6
6
|
- `@controller('name')` - Swappable behavior. Required on every controller class. Attach by class (preferred): `controller=${MyController}` in templates, `attachController(el, MyController)`, or `el.controller = MyController`; by string in raw HTML: `controller="name"`. A class attachment reflects this name in the DOM as a diagnostic marker without attaching through the registry.
|
|
7
7
|
- `@daemon` - Marks an explicitly constructed app-context daemon. No name argument; context key is address. Never constructs/registers globally.
|
|
8
8
|
- `@layout('tag-name')` - Page wrapper for routing system
|
|
@@ -33,6 +33,7 @@ Deep import: `import { useRequestHandler } from 'snice/react/useRequestHandler'`
|
|
|
33
33
|
## Route Props
|
|
34
34
|
|
|
35
35
|
- `path`: Route pattern (e.g., `/users/:id`)
|
|
36
|
+
- `order`: Optional numeric specificity tie-break; lower values match first. Equal/omitted values preserve declaration order.
|
|
36
37
|
- `page`: React component (receives params as props) OR Snice tag name string (params set as attributes)
|
|
37
38
|
- `guard`: `(ctx, params) => boolean | Promise<boolean>`
|
|
38
39
|
- `guards`: Array of guard functions (AND logic, sequential, short-circuit)
|
package/docs/ai/routing.md
CHANGED
|
@@ -144,7 +144,10 @@ changeTheme(theme: 'light' | 'dark') {
|
|
|
144
144
|
```typescript
|
|
145
145
|
interface PageOptions {
|
|
146
146
|
tag: string; // custom element tag name
|
|
147
|
-
routes: string
|
|
147
|
+
routes: Array<string | { // strings are the normal form
|
|
148
|
+
path: string;
|
|
149
|
+
order?: number; // lower wins on a specificity tie
|
|
150
|
+
}>;
|
|
148
151
|
transition?: Transition; // page-specific transition
|
|
149
152
|
guards?: Guard | Guard[]; // route guards
|
|
150
153
|
layout?: string | false; // layout tag, or false to disable
|
|
@@ -154,6 +157,25 @@ interface PageOptions {
|
|
|
154
157
|
|
|
155
158
|
- Multiple routes: `routes: ['/user', '/users', '/profile']` — all resolve to the same page.
|
|
156
159
|
- Route params: `:name` segments inside a route pattern (see Route Parameters below).
|
|
160
|
+
- Routes are sorted by specificity first. When specificity ties, string entries
|
|
161
|
+
keep registration order, including their order in one `routes` array.
|
|
162
|
+
- Object notation is optional. Use `{ path, order }` only for an explicit tie-break
|
|
163
|
+
across registrations; lower `order` values match first. Equal or omitted values
|
|
164
|
+
still preserve registration order.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
@page({
|
|
168
|
+
tag: 'work-orders-page',
|
|
169
|
+
routes: ['/work-orders?status=:status', '/work-orders']
|
|
170
|
+
})
|
|
171
|
+
class WorkOrdersPage extends HTMLElement {}
|
|
172
|
+
|
|
173
|
+
@page({
|
|
174
|
+
tag: 'override-page',
|
|
175
|
+
routes: [{ path: '/:section/:item', order: -10 }]
|
|
176
|
+
})
|
|
177
|
+
class OverridePage extends HTMLElement {}
|
|
178
|
+
```
|
|
157
179
|
|
|
158
180
|
## Navigation
|
|
159
181
|
|
|
@@ -183,7 +205,7 @@ function Router(options: RouterOptions): {
|
|
|
183
205
|
page: (pageOptions: PageOptions) => ClassDecorator;
|
|
184
206
|
initialize: () => void;
|
|
185
207
|
navigate: (path: string) => Promise<void>;
|
|
186
|
-
register: (route: string, tag: string, transition?: Transition, guards?: Guard | Guard[]) => void;
|
|
208
|
+
register: (route: string, tag: string, transition?: Transition, guards?: Guard | Guard[], layout?: string | false, placard?: Placard | ((ctx: AppContext) => Placard), order?: number) => void;
|
|
187
209
|
}
|
|
188
210
|
```
|
|
189
211
|
|
|
@@ -191,4 +213,4 @@ function Router(options: RouterOptions): {
|
|
|
191
213
|
- `initialize(): void` — starts listening for route changes; must be called after all pages are defined.
|
|
192
214
|
- Do not add `@element` to an `@page` class. The Router-returned page decorator
|
|
193
215
|
already registers the custom element and applies Snice element behavior.
|
|
194
|
-
- `register(route
|
|
216
|
+
- `register(route, tag, transition?, guards?, layout?, placard?, order?): void` — manually registers a route without `@page`. Lower `order` values win only after specificity ties; omitted/equal order preserves registration order.
|
package/docs/cli.md
CHANGED
|
@@ -55,8 +55,10 @@ npx snice validate # source analyzer only
|
|
|
55
55
|
|
|
56
56
|
It also gives non-blocking architecture suggestions: keep `@page`, `@element`,
|
|
57
57
|
`@controller`, and `@daemon` classes under `src/pages`, `src/components`,
|
|
58
|
-
`src/controllers`, and `src/daemons`;
|
|
59
|
-
|
|
58
|
+
`src/controllers`, and `src/daemons`; keep visual behavior in elements,
|
|
59
|
+
application behavior specific to a set of elements in controllers, and element
|
|
60
|
+
orchestration in pages. A host-free reusable function may remain a plain module
|
|
61
|
+
wherever the project convention places it.
|
|
60
62
|
|
|
61
63
|
Both accept `--json` for CI.
|
|
62
64
|
|
package/docs/controllers.md
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
<!-- AI: For the AI-optimized version of this doc, see docs/ai/api.md -->
|
|
2
2
|
# Controllers API Documentation
|
|
3
3
|
|
|
4
|
-
Controllers
|
|
4
|
+
Controllers hold application behavior specific to a set of elements, including
|
|
5
|
+
their data fetching, business rules, and server communication. They can be
|
|
6
|
+
attached to any HTML element, including native elements.
|
|
7
|
+
|
|
8
|
+
Visual behavior belongs in elements, application behavior specific to a set of
|
|
9
|
+
elements belongs in a controller, and element orchestration belongs in pages.
|
|
10
|
+
Do not attach a controller to the page host. A host-free reusable function may
|
|
11
|
+
stay a plain module wherever the project keeps it. URL/query parsing belongs in
|
|
12
|
+
`@page({ routes })`, not in a controller.
|
|
5
13
|
|
|
6
14
|
## Table of Contents
|
|
7
15
|
- [Basic Usage](#basic-usage)
|
|
@@ -125,6 +133,8 @@ Snice attaches immediately. Initial rendering has already completed at that
|
|
|
125
133
|
point, and waiting for `ready` would otherwise create a self-deadlock because
|
|
126
134
|
`ready` cannot settle until the current handler returns. Attaching to any
|
|
127
135
|
other element still awaits that element's `ready` promise.
|
|
136
|
+
This runtime safeguard does not make attaching a controller to a routed page a
|
|
137
|
+
good architecture; pages should orchestrate directly.
|
|
128
138
|
|
|
129
139
|
### Detachment Flow
|
|
130
140
|
|
|
@@ -355,9 +365,11 @@ class FormValidationController implements IController<HTMLFormElement> {
|
|
|
355
365
|
|
|
356
366
|
### Data Fetching Controller
|
|
357
367
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
368
|
+
A data-fetching controller is appropriate when the fetch is application behavior
|
|
369
|
+
specific to the elements it controls. Pass state through the element's public
|
|
370
|
+
API and dispatch outcome events — do not manipulate its rendered DOM. A
|
|
371
|
+
production controller should also prevent an older response from overwriting a
|
|
372
|
+
newer one:
|
|
361
373
|
|
|
362
374
|
```typescript
|
|
363
375
|
interface Order { id: string; total: number }
|
|
@@ -147,6 +147,7 @@ Defines a route within `<SniceRouter>`. The `<Route>` component itself renders n
|
|
|
147
147
|
| Prop | Type | Description |
|
|
148
148
|
|------|------|-------------|
|
|
149
149
|
| `path` | `string` | URL pattern. Supports dynamic segments: `/users/:id`, `/posts/:slug`. |
|
|
150
|
+
| `order` | `number` | Optional specificity tie-break. Lower values match first; equal or omitted values preserve declaration order. |
|
|
150
151
|
| `page` | `Component \| string` | What to render. React component receives route params as props. String = Snice web component tag name (params set as attributes). |
|
|
151
152
|
| `guard` | `(ctx, params) => boolean \| Promise<boolean>` | Single guard function. |
|
|
152
153
|
| `guards` | `function[]` | Multiple guards — all must pass (AND logic). |
|
package/docs/routing.md
CHANGED
|
@@ -294,7 +294,10 @@ class SettingsPage extends HTMLElement {
|
|
|
294
294
|
```typescript
|
|
295
295
|
interface PageOptions {
|
|
296
296
|
tag: string; // Custom element tag name
|
|
297
|
-
routes: string
|
|
297
|
+
routes: Array<string | { // Strings are the normal form
|
|
298
|
+
path: string;
|
|
299
|
+
order?: number; // Lower wins on a specificity tie
|
|
300
|
+
}>;
|
|
298
301
|
transition?: Transition; // Page-specific transition
|
|
299
302
|
guards?: Guard | Guard[]; // Route guards
|
|
300
303
|
layout?: string | false; // Layout tag, or false to disable
|
|
@@ -317,6 +320,30 @@ class UserPage extends HTMLElement {
|
|
|
317
320
|
}
|
|
318
321
|
```
|
|
319
322
|
|
|
323
|
+
Routes are sorted by specificity first. If specificity ties, registration
|
|
324
|
+
order wins, including the order of plain strings in one `routes` array. Keep
|
|
325
|
+
that compact syntax for normal pages:
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
@page({
|
|
329
|
+
tag: 'work-orders-page',
|
|
330
|
+
routes: ['/work-orders?status=:status', '/work-orders']
|
|
331
|
+
})
|
|
332
|
+
class WorkOrdersPage extends HTMLElement {}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Object notation is optional. Use it only when a route needs an explicit
|
|
336
|
+
tie-break across registrations; lower `order` values match first. Equal or
|
|
337
|
+
omitted values still preserve registration order.
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
@page({
|
|
341
|
+
tag: 'override-page',
|
|
342
|
+
routes: [{ path: '/:section/:item', order: -10 }]
|
|
343
|
+
})
|
|
344
|
+
class OverridePage extends HTMLElement {}
|
|
345
|
+
```
|
|
346
|
+
|
|
320
347
|
### Route with Parameters
|
|
321
348
|
|
|
322
349
|
```typescript
|
|
@@ -815,7 +842,7 @@ function Router(options: RouterOptions): {
|
|
|
815
842
|
page: (pageOptions: PageOptions) => ClassDecorator;
|
|
816
843
|
initialize: () => void;
|
|
817
844
|
navigate: (path: string) => Promise<void>;
|
|
818
|
-
register: (route: string, tag: string, transition?: Transition, guards?: Guard | Guard[]) => void;
|
|
845
|
+
register: (route: string, tag: string, transition?: Transition, guards?: Guard | Guard[], layout?: string | false, placard?: Placard | ((ctx: AppContext) => Placard), order?: number) => void;
|
|
819
846
|
}
|
|
820
847
|
```
|
|
821
848
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snice",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.",
|
|
6
6
|
"main": "dist/index.cjs",
|