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.
Files changed (200) hide show
  1. package/.agents/skills/snice/SKILL.md +40 -13
  2. package/README.md +6 -5
  3. package/adapters/react/SniceProvider.js +1 -1
  4. package/adapters/react/SniceRouter.d.ts +2 -0
  5. package/adapters/react/SniceRouter.js +3 -2
  6. package/adapters/react/SniceRouter.js.map +1 -1
  7. package/adapters/react/matchRoute.d.ts +4 -1
  8. package/adapters/react/matchRoute.js +13 -3
  9. package/adapters/react/matchRoute.js.map +1 -1
  10. package/adapters/react/useRequestHandler.js +1 -1
  11. package/bin/analyzer-contracts.js +2 -1
  12. package/bin/project-analyzer.js +302 -14
  13. package/bin/templates/AI_GUIDANCE.md +6 -1
  14. package/bin/templates/default/README.md +1 -1
  15. package/bin/templates/default/src/components/app-header.ts +11 -16
  16. package/bin/templates/default/src/components/notification-badge.ts +5 -14
  17. package/bin/templates/default/src/components/search-bar.ts +21 -65
  18. package/bin/templates/default/src/pages/dashboard.ts +15 -11
  19. package/bin/templates/default/src/pages/data.ts +10 -13
  20. package/bin/templates/default/src/pages/notifications.ts +4 -9
  21. package/bin/templates/default/src/pages/profile.ts +2 -8
  22. package/bin/templates/default/src/pages/settings.ts +16 -16
  23. package/dist/cdn/accordion/snice-accordion.js +1 -1
  24. package/dist/cdn/action-bar/snice-action-bar.js +1 -1
  25. package/dist/cdn/activity-feed/snice-activity-feed.js +1 -1
  26. package/dist/cdn/alert/snice-alert.js +1 -1
  27. package/dist/cdn/app-tiles/snice-app-tiles.js +1 -1
  28. package/dist/cdn/approval-flow/snice-approval-flow.js +1 -1
  29. package/dist/cdn/audio-recorder/snice-audio-recorder.js +1 -1
  30. package/dist/cdn/availability/snice-availability.js +1 -1
  31. package/dist/cdn/avatar/snice-avatar.js +1 -1
  32. package/dist/cdn/avatar-group/snice-avatar-group.js +1 -1
  33. package/dist/cdn/badge/snice-badge.js +1 -1
  34. package/dist/cdn/banner/snice-banner.js +1 -1
  35. package/dist/cdn/binpack/snice-binpack.js +1 -1
  36. package/dist/cdn/book/snice-book.js +1 -1
  37. package/dist/cdn/booking/snice-booking.js +1 -1
  38. package/dist/cdn/breadcrumbs/snice-breadcrumbs.js +1 -1
  39. package/dist/cdn/button/snice-button.js +1 -1
  40. package/dist/cdn/calendar/snice-calendar.js +1 -1
  41. package/dist/cdn/camera/snice-camera.js +1 -1
  42. package/dist/cdn/camera-annotate/snice-camera-annotate.js +1 -1
  43. package/dist/cdn/candlestick/snice-candlestick.js +1 -1
  44. package/dist/cdn/card/snice-card.js +1 -1
  45. package/dist/cdn/carousel/snice-carousel.js +1 -1
  46. package/dist/cdn/cart/snice-cart.js +1 -1
  47. package/dist/cdn/chart/snice-chart.js +1 -1
  48. package/dist/cdn/chat/snice-chat.js +1 -1
  49. package/dist/cdn/checkbox/snice-checkbox.js +1 -1
  50. package/dist/cdn/chip/snice-chip.js +1 -1
  51. package/dist/cdn/code-block/snice-code-block.js +1 -1
  52. package/dist/cdn/color-display/snice-color-display.js +1 -1
  53. package/dist/cdn/color-picker/snice-color-picker.js +1 -1
  54. package/dist/cdn/command-palette/snice-command-palette.js +1 -1
  55. package/dist/cdn/comments/snice-comments.js +1 -1
  56. package/dist/cdn/countdown/snice-countdown.js +1 -1
  57. package/dist/cdn/cropper/snice-cropper.js +1 -1
  58. package/dist/cdn/data-card/snice-data-card.js +1 -1
  59. package/dist/cdn/date-picker/snice-date-picker.js +1 -1
  60. package/dist/cdn/date-range-picker/snice-date-range-picker.js +1 -1
  61. package/dist/cdn/date-time-picker/snice-date-time-picker.js +1 -1
  62. package/dist/cdn/diff/snice-diff.js +1 -1
  63. package/dist/cdn/divider/snice-divider.js +1 -1
  64. package/dist/cdn/doc/snice-doc.js +1 -1
  65. package/dist/cdn/draw/snice-draw.js +1 -1
  66. package/dist/cdn/drawer/snice-drawer.js +1 -1
  67. package/dist/cdn/empty-state/snice-empty-state.js +1 -1
  68. package/dist/cdn/estimate/snice-estimate.js +1 -1
  69. package/dist/cdn/file-gallery/snice-file-gallery.js +1 -1
  70. package/dist/cdn/file-upload/snice-file-upload.js +1 -1
  71. package/dist/cdn/flip-card/snice-flip-card.js +1 -1
  72. package/dist/cdn/flow/snice-flow.js +1 -1
  73. package/dist/cdn/form-layout/snice-form-layout.js +1 -1
  74. package/dist/cdn/funnel/snice-funnel.js +1 -1
  75. package/dist/cdn/gantt/snice-gantt.js +1 -1
  76. package/dist/cdn/gauge/snice-gauge.js +1 -1
  77. package/dist/cdn/grid/snice-grid.js +1 -1
  78. package/dist/cdn/heatmap/snice-heatmap.js +1 -1
  79. package/dist/cdn/image/snice-image.js +1 -1
  80. package/dist/cdn/input/snice-input.js +1 -1
  81. package/dist/cdn/invoice/snice-invoice.js +1 -1
  82. package/dist/cdn/kanban/snice-kanban.js +1 -1
  83. package/dist/cdn/key-value/snice-key-value.js +1 -1
  84. package/dist/cdn/kpi/snice-kpi.js +1 -1
  85. package/dist/cdn/layout/snice-layout.js +1 -1
  86. package/dist/cdn/leaderboard/snice-leaderboard.js +1 -1
  87. package/dist/cdn/link/snice-link.js +1 -1
  88. package/dist/cdn/link-preview/snice-link-preview.js +1 -1
  89. package/dist/cdn/list/snice-list.js +1 -1
  90. package/dist/cdn/location/snice-location.js +1 -1
  91. package/dist/cdn/login/snice-login.js +1 -1
  92. package/dist/cdn/map/snice-map.js +1 -1
  93. package/dist/cdn/markdown/snice-markdown.js +1 -1
  94. package/dist/cdn/masonry/snice-masonry.js +1 -1
  95. package/dist/cdn/menu/snice-menu.js +1 -1
  96. package/dist/cdn/message-strip/snice-message-strip.js +1 -1
  97. package/dist/cdn/modal/snice-modal.js +1 -1
  98. package/dist/cdn/music-player/snice-music-player.js +1 -1
  99. package/dist/cdn/nav/snice-nav.js +1 -1
  100. package/dist/cdn/network-graph/snice-network-graph.js +1 -1
  101. package/dist/cdn/notification-center/snice-notification-center.js +1 -1
  102. package/dist/cdn/order-tracker/snice-order-tracker.js +1 -1
  103. package/dist/cdn/org-chart/snice-org-chart.js +1 -1
  104. package/dist/cdn/pagination/snice-pagination.js +1 -1
  105. package/dist/cdn/paint/snice-paint.js +1 -1
  106. package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +1 -1
  107. package/dist/cdn/permission-matrix/snice-permission-matrix.js +1 -1
  108. package/dist/cdn/podcast-player/snice-podcast-player.js +1 -1
  109. package/dist/cdn/popover/snice-popover.js +1 -1
  110. package/dist/cdn/pricing-table/snice-pricing-table.js +1 -1
  111. package/dist/cdn/product-card/snice-product-card.js +1 -1
  112. package/dist/cdn/progress/snice-progress.js +1 -1
  113. package/dist/cdn/progress-ring/snice-progress-ring.js +1 -1
  114. package/dist/cdn/qr-code/snice-qr-code.js +1 -1
  115. package/dist/cdn/qr-reader/snice-qr-reader.js +1 -1
  116. package/dist/cdn/radio/snice-radio.js +1 -1
  117. package/dist/cdn/range-slider/snice-range-slider.js +1 -1
  118. package/dist/cdn/rating/snice-rating.js +1 -1
  119. package/dist/cdn/receipt/snice-receipt.js +1 -1
  120. package/dist/cdn/recipe/snice-recipe.js +1 -1
  121. package/dist/cdn/runtime/README.md +2 -2
  122. package/dist/cdn/runtime/snice-runtime.esm.js +28 -8
  123. package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
  124. package/dist/cdn/runtime/snice-runtime.esm.min.js +1 -1
  125. package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
  126. package/dist/cdn/runtime/snice-runtime.js +28 -8
  127. package/dist/cdn/runtime/snice-runtime.js.map +1 -1
  128. package/dist/cdn/runtime/snice-runtime.min.js +1 -1
  129. package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
  130. package/dist/cdn/sankey/snice-sankey.js +1 -1
  131. package/dist/cdn/segmented-control/snice-segmented-control.js +1 -1
  132. package/dist/cdn/select/snice-select.js +1 -1
  133. package/dist/cdn/skeleton/snice-skeleton.js +1 -1
  134. package/dist/cdn/slider/snice-slider.js +1 -1
  135. package/dist/cdn/sortable/snice-sortable.js +1 -1
  136. package/dist/cdn/sparkline/snice-sparkline.js +1 -1
  137. package/dist/cdn/spinner/snice-spinner.js +1 -1
  138. package/dist/cdn/split-button/snice-split-button.js +1 -1
  139. package/dist/cdn/split-pane/snice-split-pane.js +1 -1
  140. package/dist/cdn/spotlight/snice-spotlight.js +1 -1
  141. package/dist/cdn/stat-group/snice-stat-group.js +1 -1
  142. package/dist/cdn/step-input/snice-step-input.js +1 -1
  143. package/dist/cdn/stepper/snice-stepper.js +1 -1
  144. package/dist/cdn/switch/snice-switch.js +1 -1
  145. package/dist/cdn/table/snice-table.esm.js +1 -1
  146. package/dist/cdn/table/snice-table.js +1 -1
  147. package/dist/cdn/tabs/snice-tabs.js +1 -1
  148. package/dist/cdn/tag/snice-tag.js +1 -1
  149. package/dist/cdn/tag-input/snice-tag-input.js +1 -1
  150. package/dist/cdn/terminal/snice-terminal.js +1 -1
  151. package/dist/cdn/testimonial/snice-testimonial.js +1 -1
  152. package/dist/cdn/textarea/snice-textarea.js +1 -1
  153. package/dist/cdn/time-picker/snice-time-picker.js +1 -1
  154. package/dist/cdn/time-range-picker/snice-time-range-picker.js +1 -1
  155. package/dist/cdn/timeline/snice-timeline.js +1 -1
  156. package/dist/cdn/timer/snice-timer.js +1 -1
  157. package/dist/cdn/toast/snice-toast.js +1 -1
  158. package/dist/cdn/tooltip/snice-tooltip.js +1 -1
  159. package/dist/cdn/tree/snice-tree.js +1 -1
  160. package/dist/cdn/treemap/snice-treemap.js +1 -1
  161. package/dist/cdn/user-card/snice-user-card.js +1 -1
  162. package/dist/cdn/video-player/snice-video-player.js +1 -1
  163. package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +1 -1
  164. package/dist/cdn/waterfall/snice-waterfall.js +1 -1
  165. package/dist/cdn/weather/snice-weather.js +1 -1
  166. package/dist/cdn/work-order/snice-work-order.js +1 -1
  167. package/dist/index.cjs +25 -5
  168. package/dist/index.cjs.map +1 -1
  169. package/dist/index.esm.js +25 -5
  170. package/dist/index.esm.js.map +1 -1
  171. package/dist/index.iife.js +25 -5
  172. package/dist/index.iife.js.map +1 -1
  173. package/dist/react/SniceProvider.js +1 -1
  174. package/dist/react/SniceRouter.d.ts +2 -0
  175. package/dist/react/SniceRouter.js +3 -2
  176. package/dist/react/SniceRouter.js.map +1 -1
  177. package/dist/react/index.js +1 -1
  178. package/dist/react/matchRoute.d.ts +4 -1
  179. package/dist/react/matchRoute.js +13 -3
  180. package/dist/react/matchRoute.js.map +1 -1
  181. package/dist/react/useRequestHandler.js +1 -1
  182. package/dist/symbols.cjs +1 -1
  183. package/dist/symbols.esm.js +1 -1
  184. package/dist/transitions.cjs +1 -1
  185. package/dist/transitions.esm.js +1 -1
  186. package/dist/types/page-options.d.ts +15 -1
  187. package/dist/types/router-instance.d.ts +2 -1
  188. package/docs/ai/README.md +6 -4
  189. package/docs/ai/api.md +1 -1
  190. package/docs/ai/architecture.md +21 -8
  191. package/docs/ai/cli.md +4 -1
  192. package/docs/ai/controllers.md +11 -1
  193. package/docs/ai/decorators.md +1 -1
  194. package/docs/ai/react-integration.md +1 -0
  195. package/docs/ai/routing.md +25 -3
  196. package/docs/cli.md +4 -2
  197. package/docs/controllers.md +16 -4
  198. package/docs/react-integration.md +1 -0
  199. package/docs/routing.md +29 -2
  200. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.1.1
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.
@@ -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.1.1
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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"}
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.1.1
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.
@@ -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;
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.1.1
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 = [...routes].sort((a, b) => routeSpecificity(b.path) - routeSpecificity(a.path));
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 = [...routes].sort((a, b) => routeSpecificity(b.path) - routeSpecificity(a.path));\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;;ACnBA;;;;AAIG;AACG,SAAU,WAAW,CAAC,MAAqB,EAAE,QAAgB,EAAA;;AAEjE,IAAA,MAAM,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AAE9F,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;;;;"}
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;;;;"}
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.1.1
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.
package/dist/symbols.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.1.1
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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.1.1
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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.1.1
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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.1.1
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.
@@ -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 are purely visual** - no fetch(), no API calls, no backend logic
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
- - **Pages orchestrate** - handle routing, call APIs, coordinate elements
107
- - **Controllers add behavior** - attach to elements for reusable non-visual logic; bind by class in templates (`controller=${MyController}`, preferred) or by name in raw HTML (`controller="name"`)
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
- - Several server/storage/timer operations in one page, or substantial logic copied across pages, belongs in a controller; `snice check` suggests the extraction conservatively
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? }) // Define routable page
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
@@ -4,11 +4,17 @@
4
4
 
5
5
  **Separation of concerns:**
6
6
  - **Cross-cutting:** Router + global context
7
- - **Pages:** Orchestrate elements, handle URLs
8
- - **Elements:** Generic visual building blocksno fetch(), no API calls, no business logic
9
- - **Controllers:** Specific behavior (data, APIs, business rules) swappable per element
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; route orchestration and presentation
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 may coordinate a route and perform a small one-off load. Move behavior to
37
- a controller when a page owns several storage/server/timer operations or when
38
- substantial non-visual logic is copied across pages. `snice check` reports these
39
- as architecture suggestions, not errors.
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
- - several external-effect methods or substantial logic repeated across pages -> extract a controller
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
 
@@ -2,7 +2,15 @@
2
2
 
3
3
  Mirrors `docs/controllers.md`.
4
4
 
5
- Controllers handle data fetching, business logic, and server communication separately from visual components. Attach to any HTML element, including native elements.
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
@@ -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)
@@ -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[]; // route patterns
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: string, tag: string, transition?: Transition, guards?: Guard | Guard[], layout?: string | false, placard?: Placard | ((ctx: AppContext) => Placard)): void` — manually registers a route without the `@page` decorator. Note: the return-type shape above lists `register` with 4 params; this fuller 6-param signature (adding `layout`, `placard`) is documented separately in the source.
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`; extract clearly service-heavy or repeated
59
- page logic into a controller. Small route orchestration remains valid page code.
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
 
@@ -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 handle data fetching, business logic, and server communication separately from visual components. They can be attached to any HTML element, including native elements.
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
- Controllers own data fetching. Pass state through the element's public API and
359
- dispatch outcome events do not manipulate its rendered DOM. A production
360
- controller should also prevent an older response from overwriting a newer one:
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[]; // Route patterns
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.2.0",
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",