snice 7.2.0 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/.agents/skills/snice/SKILL.md +36 -13
  2. package/README.md +11 -6
  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 +4 -1
  12. package/bin/project-analyzer.js +521 -20
  13. package/bin/snice.js +77 -13
  14. package/bin/templates/AI_GUIDANCE.md +6 -1
  15. package/bin/templates/default/README.md +1 -1
  16. package/bin/templates/default/src/components/app-header.ts +11 -16
  17. package/bin/templates/default/src/components/notification-badge.ts +5 -14
  18. package/bin/templates/default/src/components/search-bar.ts +21 -65
  19. package/bin/templates/default/src/pages/dashboard.ts +15 -11
  20. package/bin/templates/default/src/pages/data.ts +10 -13
  21. package/bin/templates/default/src/pages/notifications.ts +4 -9
  22. package/bin/templates/default/src/pages/profile.ts +2 -8
  23. package/bin/templates/default/src/pages/settings.ts +16 -16
  24. package/dist/app-context.d.ts +10 -10
  25. package/dist/cdn/accordion/snice-accordion.js +1 -1
  26. package/dist/cdn/action-bar/snice-action-bar.js +1 -1
  27. package/dist/cdn/activity-feed/snice-activity-feed.js +1 -1
  28. package/dist/cdn/alert/snice-alert.js +1 -1
  29. package/dist/cdn/app-tiles/snice-app-tiles.js +1 -1
  30. package/dist/cdn/approval-flow/snice-approval-flow.js +1 -1
  31. package/dist/cdn/audio-recorder/snice-audio-recorder.js +1 -1
  32. package/dist/cdn/availability/snice-availability.js +1 -1
  33. package/dist/cdn/avatar/snice-avatar.js +1 -1
  34. package/dist/cdn/avatar-group/snice-avatar-group.js +1 -1
  35. package/dist/cdn/badge/snice-badge.js +1 -1
  36. package/dist/cdn/banner/snice-banner.js +1 -1
  37. package/dist/cdn/binpack/snice-binpack.js +1 -1
  38. package/dist/cdn/book/snice-book.js +1 -1
  39. package/dist/cdn/booking/snice-booking.js +1 -1
  40. package/dist/cdn/breadcrumbs/snice-breadcrumbs.js +1 -1
  41. package/dist/cdn/button/snice-button.js +1 -1
  42. package/dist/cdn/calendar/snice-calendar.js +1 -1
  43. package/dist/cdn/camera/snice-camera.js +1 -1
  44. package/dist/cdn/camera-annotate/snice-camera-annotate.js +1 -1
  45. package/dist/cdn/candlestick/snice-candlestick.js +1 -1
  46. package/dist/cdn/card/snice-card.js +1 -1
  47. package/dist/cdn/carousel/snice-carousel.js +1 -1
  48. package/dist/cdn/cart/snice-cart.js +1 -1
  49. package/dist/cdn/chart/snice-chart.js +1 -1
  50. package/dist/cdn/chat/snice-chat.js +1 -1
  51. package/dist/cdn/checkbox/snice-checkbox.js +1 -1
  52. package/dist/cdn/chip/snice-chip.js +1 -1
  53. package/dist/cdn/code-block/snice-code-block.js +1 -1
  54. package/dist/cdn/color-display/snice-color-display.js +1 -1
  55. package/dist/cdn/color-picker/snice-color-picker.js +1 -1
  56. package/dist/cdn/command-palette/snice-command-palette.js +1 -1
  57. package/dist/cdn/comments/snice-comments.js +1 -1
  58. package/dist/cdn/countdown/snice-countdown.js +1 -1
  59. package/dist/cdn/cropper/snice-cropper.js +1 -1
  60. package/dist/cdn/data-card/snice-data-card.js +1 -1
  61. package/dist/cdn/date-picker/snice-date-picker.js +1 -1
  62. package/dist/cdn/date-range-picker/snice-date-range-picker.js +1 -1
  63. package/dist/cdn/date-time-picker/snice-date-time-picker.js +1 -1
  64. package/dist/cdn/diff/snice-diff.js +1 -1
  65. package/dist/cdn/divider/snice-divider.js +1 -1
  66. package/dist/cdn/doc/snice-doc.js +1 -1
  67. package/dist/cdn/draw/snice-draw.js +1 -1
  68. package/dist/cdn/drawer/snice-drawer.js +1 -1
  69. package/dist/cdn/empty-state/snice-empty-state.js +1 -1
  70. package/dist/cdn/estimate/snice-estimate.js +1 -1
  71. package/dist/cdn/file-gallery/snice-file-gallery.js +1 -1
  72. package/dist/cdn/file-upload/snice-file-upload.js +1 -1
  73. package/dist/cdn/flip-card/snice-flip-card.js +1 -1
  74. package/dist/cdn/flow/snice-flow.js +1 -1
  75. package/dist/cdn/form-layout/snice-form-layout.js +1 -1
  76. package/dist/cdn/funnel/snice-funnel.js +1 -1
  77. package/dist/cdn/gantt/snice-gantt.js +1 -1
  78. package/dist/cdn/gauge/snice-gauge.js +1 -1
  79. package/dist/cdn/grid/snice-grid.js +1 -1
  80. package/dist/cdn/heatmap/snice-heatmap.js +1 -1
  81. package/dist/cdn/image/snice-image.js +1 -1
  82. package/dist/cdn/input/snice-input.js +1 -1
  83. package/dist/cdn/invoice/snice-invoice.js +1 -1
  84. package/dist/cdn/kanban/snice-kanban.js +1 -1
  85. package/dist/cdn/key-value/snice-key-value.js +1 -1
  86. package/dist/cdn/kpi/snice-kpi.js +1 -1
  87. package/dist/cdn/layout/snice-layout.js +1 -1
  88. package/dist/cdn/leaderboard/snice-leaderboard.js +1 -1
  89. package/dist/cdn/link/snice-link.js +1 -1
  90. package/dist/cdn/link-preview/snice-link-preview.js +1 -1
  91. package/dist/cdn/list/snice-list.js +1 -1
  92. package/dist/cdn/location/snice-location.js +1 -1
  93. package/dist/cdn/login/snice-login.js +1 -1
  94. package/dist/cdn/map/snice-map.js +1 -1
  95. package/dist/cdn/markdown/snice-markdown.js +1 -1
  96. package/dist/cdn/masonry/snice-masonry.js +1 -1
  97. package/dist/cdn/menu/snice-menu.js +1 -1
  98. package/dist/cdn/message-strip/snice-message-strip.js +1 -1
  99. package/dist/cdn/modal/snice-modal.js +1 -1
  100. package/dist/cdn/music-player/snice-music-player.js +1 -1
  101. package/dist/cdn/nav/snice-nav.js +1 -1
  102. package/dist/cdn/network-graph/snice-network-graph.js +1 -1
  103. package/dist/cdn/notification-center/snice-notification-center.js +1 -1
  104. package/dist/cdn/order-tracker/snice-order-tracker.js +1 -1
  105. package/dist/cdn/org-chart/snice-org-chart.js +1 -1
  106. package/dist/cdn/pagination/snice-pagination.js +1 -1
  107. package/dist/cdn/paint/snice-paint.js +1 -1
  108. package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +1 -1
  109. package/dist/cdn/permission-matrix/snice-permission-matrix.js +1 -1
  110. package/dist/cdn/podcast-player/snice-podcast-player.js +1 -1
  111. package/dist/cdn/popover/snice-popover.js +1 -1
  112. package/dist/cdn/pricing-table/snice-pricing-table.js +1 -1
  113. package/dist/cdn/product-card/snice-product-card.js +1 -1
  114. package/dist/cdn/progress/snice-progress.js +1 -1
  115. package/dist/cdn/progress-ring/snice-progress-ring.js +1 -1
  116. package/dist/cdn/qr-code/snice-qr-code.js +1 -1
  117. package/dist/cdn/qr-reader/snice-qr-reader.js +1 -1
  118. package/dist/cdn/radio/snice-radio.js +1 -1
  119. package/dist/cdn/range-slider/snice-range-slider.js +1 -1
  120. package/dist/cdn/rating/snice-rating.js +1 -1
  121. package/dist/cdn/receipt/snice-receipt.js +1 -1
  122. package/dist/cdn/recipe/snice-recipe.js +1 -1
  123. package/dist/cdn/runtime/README.md +2 -2
  124. package/dist/cdn/runtime/snice-runtime.esm.js +333 -192
  125. package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
  126. package/dist/cdn/runtime/snice-runtime.esm.min.js +1 -1
  127. package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
  128. package/dist/cdn/runtime/snice-runtime.js +333 -191
  129. package/dist/cdn/runtime/snice-runtime.js.map +1 -1
  130. package/dist/cdn/runtime/snice-runtime.min.js +1 -1
  131. package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
  132. package/dist/cdn/sankey/snice-sankey.js +1 -1
  133. package/dist/cdn/segmented-control/snice-segmented-control.js +1 -1
  134. package/dist/cdn/select/snice-select.js +1 -1
  135. package/dist/cdn/skeleton/snice-skeleton.js +1 -1
  136. package/dist/cdn/slider/snice-slider.js +1 -1
  137. package/dist/cdn/sortable/snice-sortable.js +1 -1
  138. package/dist/cdn/sparkline/snice-sparkline.js +1 -1
  139. package/dist/cdn/spinner/snice-spinner.js +1 -1
  140. package/dist/cdn/split-button/snice-split-button.js +1 -1
  141. package/dist/cdn/split-pane/snice-split-pane.js +1 -1
  142. package/dist/cdn/spotlight/snice-spotlight.js +1 -1
  143. package/dist/cdn/stat-group/snice-stat-group.js +1 -1
  144. package/dist/cdn/step-input/snice-step-input.js +1 -1
  145. package/dist/cdn/stepper/snice-stepper.js +1 -1
  146. package/dist/cdn/switch/snice-switch.js +1 -1
  147. package/dist/cdn/table/snice-table.esm.js +1 -1
  148. package/dist/cdn/table/snice-table.js +1 -1
  149. package/dist/cdn/tabs/snice-tabs.js +1 -1
  150. package/dist/cdn/tag/snice-tag.js +1 -1
  151. package/dist/cdn/tag-input/snice-tag-input.js +1 -1
  152. package/dist/cdn/terminal/snice-terminal.js +1 -1
  153. package/dist/cdn/testimonial/snice-testimonial.js +1 -1
  154. package/dist/cdn/textarea/snice-textarea.js +1 -1
  155. package/dist/cdn/time-picker/snice-time-picker.js +1 -1
  156. package/dist/cdn/time-range-picker/snice-time-range-picker.js +1 -1
  157. package/dist/cdn/timeline/snice-timeline.js +1 -1
  158. package/dist/cdn/timer/snice-timer.js +1 -1
  159. package/dist/cdn/toast/snice-toast.js +1 -1
  160. package/dist/cdn/tooltip/snice-tooltip.js +1 -1
  161. package/dist/cdn/tree/snice-tree.js +1 -1
  162. package/dist/cdn/treemap/snice-treemap.js +1 -1
  163. package/dist/cdn/user-card/snice-user-card.js +1 -1
  164. package/dist/cdn/video-player/snice-video-player.js +1 -1
  165. package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +1 -1
  166. package/dist/cdn/waterfall/snice-waterfall.js +1 -1
  167. package/dist/cdn/weather/snice-weather.js +1 -1
  168. package/dist/cdn/work-order/snice-work-order.js +1 -1
  169. package/dist/components/pagination/snice-pagination.types.d.ts +5 -0
  170. package/dist/context-provider.d.ts +11 -1
  171. package/dist/context.d.ts +5 -6
  172. package/dist/index.cjs +330 -188
  173. package/dist/index.cjs.map +1 -1
  174. package/dist/index.d.ts +2 -1
  175. package/dist/index.esm.js +330 -189
  176. package/dist/index.esm.js.map +1 -1
  177. package/dist/index.iife.js +330 -188
  178. package/dist/index.iife.js.map +1 -1
  179. package/dist/react/SniceProvider.js +1 -1
  180. package/dist/react/SniceRouter.d.ts +2 -0
  181. package/dist/react/SniceRouter.js +3 -2
  182. package/dist/react/SniceRouter.js.map +1 -1
  183. package/dist/react/index.js +1 -1
  184. package/dist/react/matchRoute.d.ts +4 -1
  185. package/dist/react/matchRoute.js +13 -3
  186. package/dist/react/matchRoute.js.map +1 -1
  187. package/dist/react/useRequestHandler.js +1 -1
  188. package/dist/symbols.cjs +1 -1
  189. package/dist/symbols.esm.js +1 -1
  190. package/dist/testing-dom.cjs +48 -0
  191. package/dist/testing-dom.cjs.map +1 -0
  192. package/dist/testing-dom.d.ts +23 -0
  193. package/dist/testing-dom.esm.js +45 -0
  194. package/dist/testing-dom.esm.js.map +1 -0
  195. package/dist/transitions.cjs +1 -1
  196. package/dist/transitions.esm.js +1 -1
  197. package/dist/types/context.d.ts +8 -8
  198. package/dist/types/page-options.d.ts +15 -1
  199. package/dist/types/router-instance.d.ts +2 -1
  200. package/dist/utils.d.ts +3 -1
  201. package/docs/ai/README.md +6 -4
  202. package/docs/ai/api.md +13 -2
  203. package/docs/ai/architecture.md +28 -11
  204. package/docs/ai/bindings.md +1 -1
  205. package/docs/ai/cli.md +26 -1
  206. package/docs/ai/components/badge.md +4 -0
  207. package/docs/ai/components/pagination.md +4 -2
  208. package/docs/ai/components/segmented-control.md +5 -1
  209. package/docs/ai/controllers.md +41 -12
  210. package/docs/ai/decorators.md +3 -3
  211. package/docs/ai/events.md +8 -2
  212. package/docs/ai/fetcher.md +32 -1
  213. package/docs/ai/lifecycle.md +4 -1
  214. package/docs/ai/patterns.md +9 -4
  215. package/docs/ai/react-integration.md +1 -0
  216. package/docs/ai/request-response.md +20 -6
  217. package/docs/ai/routing.md +33 -4
  218. package/docs/ai/testing.md +71 -0
  219. package/docs/bindings.md +1 -1
  220. package/docs/cli.md +27 -2
  221. package/docs/components/badge.md +4 -0
  222. package/docs/components/pagination.md +8 -5
  223. package/docs/components/segmented-control.md +5 -1
  224. package/docs/controllers.md +46 -15
  225. package/docs/events.md +11 -2
  226. package/docs/fetcher.md +32 -1
  227. package/docs/lifecycle.md +5 -1
  228. package/docs/react-integration.md +1 -0
  229. package/docs/request-response.md +25 -7
  230. package/docs/routing.md +39 -3
  231. package/docs/testing.md +71 -0
  232. package/package.json +6 -1
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.1.1
2
+ * snice v7.3.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -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.3.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -83,6 +83,7 @@ function SniceRouter({ mode, context = {}, layout: defaultLayout, loading, fallb
83
83
  guards.push(...props.guards);
84
84
  result.push({
85
85
  path: props.path,
86
+ order: props.order,
86
87
  page: props.page,
87
88
  guards,
88
89
  guardRedirect: props.guardRedirect,
@@ -99,7 +100,7 @@ function SniceRouter({ mode, context = {}, layout: defaultLayout, loading, fallb
99
100
  .map((r) => r.placard);
100
101
  }, [parsedRoutes]);
101
102
  // Build route configs for matching
102
- const routeConfigs = useMemo(() => parsedRoutes.map((r, i) => ({ path: r.path, index: i })), [parsedRoutes]);
103
+ const routeConfigs = useMemo(() => parsedRoutes.map((r, i) => ({ path: r.path, index: i, order: r.order })), [parsedRoutes]);
103
104
  // Navigate function
104
105
  const navigate = useCallback((path) => {
105
106
  if (mode === 'hash') {
@@ -1 +1 @@
1
- {"version":3,"file":"SniceRouter.js","sources":["../../src/react/SniceRouter.tsx"],"sourcesContent":["import {\n useState,\n useEffect,\n useCallback,\n useMemo,\n useRef,\n type ReactNode,\n type ReactElement,\n type ComponentType,\n Children,\n isValidElement,\n createElement,\n} from 'react';\nimport { SniceProvider } from './SniceProvider';\nimport { matchRoutes, type RouteConfig } from './matchRoute';\nimport type { Placard } from './SniceProvider';\n\n// ─── Route component (declarative config) ───\n\nexport interface RouteProps {\n path: string;\n /** React component OR Snice element tag name (string) */\n page: ComponentType<any> | string;\n /** Single guard function */\n guard?: (context: Record<string, any>, params: Record<string, string>) => boolean | Promise<boolean>;\n /** Multiple guards (AND logic) */\n guards?: Array<(context: Record<string, any>, params: Record<string, string>) => boolean | Promise<boolean>>;\n /** Redirect path if guard rejects */\n guardRedirect?: string;\n /** Layout override. Component, string (Snice tag), or false (no layout) */\n layout?: ComponentType<{ children: ReactNode }> | string | false;\n /** Page metadata for layouts */\n placard?: Placard;\n}\n\n/**\n * Route definition component. Child of <SniceRouter>.\n * Does not render anything — SniceRouter reads its props.\n */\nexport function Route(_props: RouteProps): ReactElement | null {\n return null;\n}\n\n// ─── SniceRouter ───\n\nexport interface SniceRouterProps {\n /** URL strategy */\n mode: 'hash' | 'history';\n /** Application context passed to guards, pages, layouts */\n context?: Record<string, any>;\n /** Default layout component or Snice tag name */\n layout?: ComponentType<{ children: ReactNode }> | string;\n /** Loading component shown during async guards. Component, string (Snice tag), or JSX. */\n loading?: ComponentType | string | ReactNode;\n /** Fallback when no route matches. Component, string (Snice tag), or JSX. */\n fallback?: ComponentType | string | ReactNode;\n children: ReactNode;\n}\n\ninterface ParsedRoute {\n path: string;\n page: ComponentType<any> | string;\n guards: Array<(ctx: Record<string, any>, params: Record<string, string>) => boolean | Promise<boolean>>;\n guardRedirect?: string;\n layout?: ComponentType<{ children: ReactNode }> | string | false;\n placard?: Placard;\n}\n\nfunction getPath(mode: 'hash' | 'history'): string {\n if (mode === 'hash') {\n return window.location.hash.slice(1) || '/';\n }\n return window.location.pathname;\n}\n\nfunction DefaultLoading() {\n return createElement('div', {\n style: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n height: '100%',\n minHeight: '200px',\n },\n },\n createElement('style', null, '@keyframes snice-spin{to{transform:rotate(360deg)}}'),\n createElement('div', {\n style: {\n width: '32px',\n height: '32px',\n border: '3px solid rgba(128,128,128,0.3)',\n borderTopColor: 'rgba(128,128,128,0.8)',\n borderRadius: '50%',\n animation: 'snice-spin 0.6s linear infinite',\n },\n }),\n );\n}\n\n/** Render a page/layout/loading/fallback prop that can be component, string tag, or JSX */\nfunction renderFlexible(\n value: ComponentType<any> | string | ReactNode | undefined,\n props?: Record<string, any>,\n): ReactNode {\n if (value === undefined || value === null) return null;\n\n // String = Snice web component tag name\n if (typeof value === 'string') {\n return createElement(value, props);\n }\n\n // Function = React component\n if (typeof value === 'function') {\n return createElement(value as ComponentType<any>, props);\n }\n\n // Already JSX/ReactNode\n return value;\n}\n\n/**\n * Root provider component. Manages URL state, route matching,\n * guard execution, layout selection, and context propagation.\n */\nexport function SniceRouter({\n mode,\n context = {},\n layout: defaultLayout,\n loading,\n fallback,\n children,\n}: SniceRouterProps) {\n const [currentPath, setCurrentPath] = useState(() => getPath(mode));\n const [guardState, setGuardState] = useState<'idle' | 'checking' | 'passed' | 'failed'>('idle');\n const contextRef = useRef(context);\n contextRef.current = context;\n\n // Parse Route children into config\n const parsedRoutes = useMemo<ParsedRoute[]>(() => {\n const result: ParsedRoute[] = [];\n Children.forEach(children, (child) => {\n if (!isValidElement(child) || child.type !== Route) return;\n const props = child.props as RouteProps;\n const guards: ParsedRoute['guards'] = [];\n if (props.guard) guards.push(props.guard);\n if (props.guards) guards.push(...props.guards);\n result.push({\n path: props.path,\n page: props.page,\n guards,\n guardRedirect: props.guardRedirect,\n layout: props.layout,\n placard: props.placard,\n });\n });\n return result;\n }, [children]);\n\n // Collect placards\n const placards = useMemo<Placard[]>(() => {\n return parsedRoutes\n .filter((r) => r.placard)\n .map((r) => r.placard!);\n }, [parsedRoutes]);\n\n // Build route configs for matching\n const routeConfigs = useMemo<RouteConfig[]>(\n () => parsedRoutes.map((r, i) => ({ path: r.path, index: i })),\n [parsedRoutes],\n );\n\n // Navigate function\n const navigate = useCallback(\n (path: string) => {\n if (mode === 'hash') {\n window.location.hash = path;\n } else {\n window.history.pushState(null, '', path);\n setCurrentPath(path);\n }\n },\n [mode],\n );\n\n // Listen for URL changes\n useEffect(() => {\n const handler = () => setCurrentPath(getPath(mode));\n const event = mode === 'hash' ? 'hashchange' : 'popstate';\n window.addEventListener(event, handler);\n return () => window.removeEventListener(event, handler);\n }, [mode]);\n\n // Match current path\n const match = useMemo(\n () => matchRoutes(routeConfigs, currentPath),\n [routeConfigs, currentPath],\n );\n\n const matchedRoute = match ? parsedRoutes[match.index] : null;\n // Stable params reference across renders so downstream `useMemo` deps\n // (SniceProvider value, guard effect) don't churn on every render when\n // nothing actually changed.\n const params = useMemo(\n () => match?.params ?? {},\n // Re-create only when the param content actually changes\n [JSON.stringify(match?.params ?? {})],\n );\n\n // Run guards\n useEffect(() => {\n if (!matchedRoute) {\n setGuardState('idle');\n return;\n }\n\n if (matchedRoute.guards.length === 0) {\n setGuardState('passed');\n return;\n }\n\n let cancelled = false;\n setGuardState('checking');\n\n (async () => {\n try {\n for (const guard of matchedRoute.guards) {\n const result = await guard(contextRef.current, params);\n if (cancelled) return;\n if (!result) {\n setGuardState('failed');\n if (matchedRoute.guardRedirect) {\n navigate(matchedRoute.guardRedirect);\n }\n return;\n }\n }\n if (!cancelled) setGuardState('passed');\n } catch {\n if (!cancelled) {\n setGuardState('failed');\n if (matchedRoute.guardRedirect) {\n navigate(matchedRoute.guardRedirect);\n }\n }\n }\n })();\n\n return () => { cancelled = true; };\n }, [matchedRoute, currentPath]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Determine what to render\n let content: ReactNode;\n\n if (!matchedRoute) {\n // No route match → fallback\n content = renderFlexible(fallback) ?? createElement('div', null, '404 — Page not found');\n } else if (guardState === 'checking') {\n // Async guards running → show loading\n content = renderFlexible(loading) ?? createElement(DefaultLoading);\n } else if (guardState === 'failed') {\n // Guard rejected (redirect may have fired) — render nothing\n content = null;\n } else {\n // Guard passed or no guards → render page\n content = renderFlexible(matchedRoute.page, params);\n }\n\n // Apply layout\n const layoutCandidate = matchedRoute?.layout !== undefined ? matchedRoute.layout : defaultLayout;\n const layoutToUse = layoutCandidate === false ? undefined : layoutCandidate;\n if (layoutToUse && content !== null) {\n content = renderFlexible(\n layoutToUse as ComponentType<{ children: ReactNode }> | string,\n { children: content },\n );\n }\n\n return (\n <SniceProvider\n context={context}\n navigate={navigate}\n route={match?.path ?? ''}\n params={params}\n placards={placards}\n >\n {content}\n </SniceProvider>\n );\n}\n"],"names":["_jsx"],"mappings":"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"}
1
+ {"version":3,"file":"SniceRouter.js","sources":["../../src/react/SniceRouter.tsx"],"sourcesContent":["import {\n useState,\n useEffect,\n useCallback,\n useMemo,\n useRef,\n type ReactNode,\n type ReactElement,\n type ComponentType,\n Children,\n isValidElement,\n createElement,\n} from 'react';\nimport { SniceProvider } from './SniceProvider';\nimport { matchRoutes, type RouteConfig } from './matchRoute';\nimport type { Placard } from './SniceProvider';\n\n// ─── Route component (declarative config) ───\n\nexport interface RouteProps {\n path: string;\n /** Lower values win when route specificity ties. Declaration order is the fallback. */\n order?: number;\n /** React component OR Snice element tag name (string) */\n page: ComponentType<any> | string;\n /** Single guard function */\n guard?: (context: Record<string, any>, params: Record<string, string>) => boolean | Promise<boolean>;\n /** Multiple guards (AND logic) */\n guards?: Array<(context: Record<string, any>, params: Record<string, string>) => boolean | Promise<boolean>>;\n /** Redirect path if guard rejects */\n guardRedirect?: string;\n /** Layout override. Component, string (Snice tag), or false (no layout) */\n layout?: ComponentType<{ children: ReactNode }> | string | false;\n /** Page metadata for layouts */\n placard?: Placard;\n}\n\n/**\n * Route definition component. Child of <SniceRouter>.\n * Does not render anything — SniceRouter reads its props.\n */\nexport function Route(_props: RouteProps): ReactElement | null {\n return null;\n}\n\n// ─── SniceRouter ───\n\nexport interface SniceRouterProps {\n /** URL strategy */\n mode: 'hash' | 'history';\n /** Application context passed to guards, pages, layouts */\n context?: Record<string, any>;\n /** Default layout component or Snice tag name */\n layout?: ComponentType<{ children: ReactNode }> | string;\n /** Loading component shown during async guards. Component, string (Snice tag), or JSX. */\n loading?: ComponentType | string | ReactNode;\n /** Fallback when no route matches. Component, string (Snice tag), or JSX. */\n fallback?: ComponentType | string | ReactNode;\n children: ReactNode;\n}\n\ninterface ParsedRoute {\n path: string;\n order?: number;\n page: ComponentType<any> | string;\n guards: Array<(ctx: Record<string, any>, params: Record<string, string>) => boolean | Promise<boolean>>;\n guardRedirect?: string;\n layout?: ComponentType<{ children: ReactNode }> | string | false;\n placard?: Placard;\n}\n\nfunction getPath(mode: 'hash' | 'history'): string {\n if (mode === 'hash') {\n return window.location.hash.slice(1) || '/';\n }\n return window.location.pathname;\n}\n\nfunction DefaultLoading() {\n return createElement('div', {\n style: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n height: '100%',\n minHeight: '200px',\n },\n },\n createElement('style', null, '@keyframes snice-spin{to{transform:rotate(360deg)}}'),\n createElement('div', {\n style: {\n width: '32px',\n height: '32px',\n border: '3px solid rgba(128,128,128,0.3)',\n borderTopColor: 'rgba(128,128,128,0.8)',\n borderRadius: '50%',\n animation: 'snice-spin 0.6s linear infinite',\n },\n }),\n );\n}\n\n/** Render a page/layout/loading/fallback prop that can be component, string tag, or JSX */\nfunction renderFlexible(\n value: ComponentType<any> | string | ReactNode | undefined,\n props?: Record<string, any>,\n): ReactNode {\n if (value === undefined || value === null) return null;\n\n // String = Snice web component tag name\n if (typeof value === 'string') {\n return createElement(value, props);\n }\n\n // Function = React component\n if (typeof value === 'function') {\n return createElement(value as ComponentType<any>, props);\n }\n\n // Already JSX/ReactNode\n return value;\n}\n\n/**\n * Root provider component. Manages URL state, route matching,\n * guard execution, layout selection, and context propagation.\n */\nexport function SniceRouter({\n mode,\n context = {},\n layout: defaultLayout,\n loading,\n fallback,\n children,\n}: SniceRouterProps) {\n const [currentPath, setCurrentPath] = useState(() => getPath(mode));\n const [guardState, setGuardState] = useState<'idle' | 'checking' | 'passed' | 'failed'>('idle');\n const contextRef = useRef(context);\n contextRef.current = context;\n\n // Parse Route children into config\n const parsedRoutes = useMemo<ParsedRoute[]>(() => {\n const result: ParsedRoute[] = [];\n Children.forEach(children, (child) => {\n if (!isValidElement(child) || child.type !== Route) return;\n const props = child.props as RouteProps;\n const guards: ParsedRoute['guards'] = [];\n if (props.guard) guards.push(props.guard);\n if (props.guards) guards.push(...props.guards);\n result.push({\n path: props.path,\n order: props.order,\n page: props.page,\n guards,\n guardRedirect: props.guardRedirect,\n layout: props.layout,\n placard: props.placard,\n });\n });\n return result;\n }, [children]);\n\n // Collect placards\n const placards = useMemo<Placard[]>(() => {\n return parsedRoutes\n .filter((r) => r.placard)\n .map((r) => r.placard!);\n }, [parsedRoutes]);\n\n // Build route configs for matching\n const routeConfigs = useMemo<RouteConfig[]>(\n () => parsedRoutes.map((r, i) => ({ path: r.path, index: i, order: r.order })),\n [parsedRoutes],\n );\n\n // Navigate function\n const navigate = useCallback(\n (path: string) => {\n if (mode === 'hash') {\n window.location.hash = path;\n } else {\n window.history.pushState(null, '', path);\n setCurrentPath(path);\n }\n },\n [mode],\n );\n\n // Listen for URL changes\n useEffect(() => {\n const handler = () => setCurrentPath(getPath(mode));\n const event = mode === 'hash' ? 'hashchange' : 'popstate';\n window.addEventListener(event, handler);\n return () => window.removeEventListener(event, handler);\n }, [mode]);\n\n // Match current path\n const match = useMemo(\n () => matchRoutes(routeConfigs, currentPath),\n [routeConfigs, currentPath],\n );\n\n const matchedRoute = match ? parsedRoutes[match.index] : null;\n // Stable params reference across renders so downstream `useMemo` deps\n // (SniceProvider value, guard effect) don't churn on every render when\n // nothing actually changed.\n const params = useMemo(\n () => match?.params ?? {},\n // Re-create only when the param content actually changes\n [JSON.stringify(match?.params ?? {})],\n );\n\n // Run guards\n useEffect(() => {\n if (!matchedRoute) {\n setGuardState('idle');\n return;\n }\n\n if (matchedRoute.guards.length === 0) {\n setGuardState('passed');\n return;\n }\n\n let cancelled = false;\n setGuardState('checking');\n\n (async () => {\n try {\n for (const guard of matchedRoute.guards) {\n const result = await guard(contextRef.current, params);\n if (cancelled) return;\n if (!result) {\n setGuardState('failed');\n if (matchedRoute.guardRedirect) {\n navigate(matchedRoute.guardRedirect);\n }\n return;\n }\n }\n if (!cancelled) setGuardState('passed');\n } catch {\n if (!cancelled) {\n setGuardState('failed');\n if (matchedRoute.guardRedirect) {\n navigate(matchedRoute.guardRedirect);\n }\n }\n }\n })();\n\n return () => { cancelled = true; };\n }, [matchedRoute, currentPath]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Determine what to render\n let content: ReactNode;\n\n if (!matchedRoute) {\n // No route match → fallback\n content = renderFlexible(fallback) ?? createElement('div', null, '404 — Page not found');\n } else if (guardState === 'checking') {\n // Async guards running → show loading\n content = renderFlexible(loading) ?? createElement(DefaultLoading);\n } else if (guardState === 'failed') {\n // Guard rejected (redirect may have fired) — render nothing\n content = null;\n } else {\n // Guard passed or no guards → render page\n content = renderFlexible(matchedRoute.page, params);\n }\n\n // Apply layout\n const layoutCandidate = matchedRoute?.layout !== undefined ? matchedRoute.layout : defaultLayout;\n const layoutToUse = layoutCandidate === false ? undefined : layoutCandidate;\n if (layoutToUse && content !== null) {\n content = renderFlexible(\n layoutToUse as ComponentType<{ children: ReactNode }> | string,\n { children: content },\n );\n }\n\n return (\n <SniceProvider\n context={context}\n navigate={navigate}\n route={match?.path ?? ''}\n params={params}\n placards={placards}\n >\n {content}\n </SniceProvider>\n );\n}\n"],"names":["_jsx"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.1.1
2
+ * snice v7.3.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -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.3.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -45,11 +45,21 @@ function routeSpecificity(spec) {
45
45
  /**
46
46
  * Match a URL path against an array of route configs.
47
47
  * Uses pica-route — same matching as vanilla Snice's Router.
48
- * Routes are sorted by per-segment specificity (static > dynamic > wildcard).
48
+ * Routes are sorted by per-segment specificity (static > dynamic > wildcard),
49
+ * then optional lower-first order, then declaration order.
49
50
  */
50
51
  function matchRoutes(routes, pathname) {
52
+ for (const route of routes) {
53
+ if (route.order !== undefined && !Number.isFinite(route.order)) {
54
+ throw new TypeError(`Route order for "${route.path}" must be a finite number.`);
55
+ }
56
+ }
51
57
  // Sort by specificity (most specific first), same model as the vanilla Router.
52
- const sorted = [...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.3.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
package/dist/symbols.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.1.1
2
+ * snice v7.3.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.1.1
2
+ * snice v7.3.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -0,0 +1,48 @@
1
+ /*!
2
+ * snice v7.3.0
3
+ * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
+ * (c) 2024
5
+ * Released under the MIT License.
6
+ *
7
+ * GENERATED FILE — DO NOT EDIT. Source: src/. Rebuild: npm run build:core
8
+ */
9
+ 'use strict';
10
+
11
+ /**
12
+ * Standards-shaped DOM compatibility for non-browser test runners.
13
+ *
14
+ * This module only fills missing IDL surface. It never replaces an existing
15
+ * implementation and does not attempt to simulate layout, paint, or browser
16
+ * focus navigation.
17
+ */
18
+ /**
19
+ * Fill proven DOM-runner IDL gaps without replacing native implementations.
20
+ * Safe to call more than once.
21
+ */
22
+ function installDOMTestingCompatibility(scope = globalThis) {
23
+ const installed = [];
24
+ const HTMLElementConstructor = scope.HTMLElement;
25
+ if (!HTMLElementConstructor)
26
+ return { installed };
27
+ const prototype = HTMLElementConstructor.prototype;
28
+ if (!('autofocus' in prototype)) {
29
+ Object.defineProperty(prototype, 'autofocus', {
30
+ configurable: true,
31
+ enumerable: true,
32
+ get() {
33
+ return this.hasAttribute('autofocus');
34
+ },
35
+ set(value) {
36
+ this.toggleAttribute('autofocus', Boolean(value));
37
+ },
38
+ });
39
+ installed.push('HTMLElement.autofocus');
40
+ }
41
+ return { installed };
42
+ }
43
+ /** Result of the side-effect installation performed by `snice/testing/dom`. */
44
+ const domTestingCompatibility = installDOMTestingCompatibility();
45
+
46
+ exports.domTestingCompatibility = domTestingCompatibility;
47
+ exports.installDOMTestingCompatibility = installDOMTestingCompatibility;
48
+ //# sourceMappingURL=testing-dom.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing-dom.cjs","sources":["../src/testing-dom.ts"],"sourcesContent":["/**\n * Standards-shaped DOM compatibility for non-browser test runners.\n *\n * This module only fills missing IDL surface. It never replaces an existing\n * implementation and does not attempt to simulate layout, paint, or browser\n * focus navigation.\n */\n\nexport type DOMTestingCompatibilityFeature = 'HTMLElement.autofocus';\n\nexport interface DOMTestingCompatibilityReport {\n /** Standards features installed because the current DOM omitted them. */\n installed: readonly DOMTestingCompatibilityFeature[];\n}\n\ninterface DOMTestingGlobal {\n HTMLElement?: typeof HTMLElement;\n}\n\n/**\n * Fill proven DOM-runner IDL gaps without replacing native implementations.\n * Safe to call more than once.\n */\nexport function installDOMTestingCompatibility(\n scope: DOMTestingGlobal = globalThis as DOMTestingGlobal\n): DOMTestingCompatibilityReport {\n const installed: DOMTestingCompatibilityFeature[] = [];\n const HTMLElementConstructor = scope.HTMLElement;\n if (!HTMLElementConstructor) return { installed };\n\n const prototype = HTMLElementConstructor.prototype;\n if (!('autofocus' in prototype)) {\n Object.defineProperty(prototype, 'autofocus', {\n configurable: true,\n enumerable: true,\n get(this: HTMLElement): boolean {\n return this.hasAttribute('autofocus');\n },\n set(this: HTMLElement, value: boolean) {\n this.toggleAttribute('autofocus', Boolean(value));\n },\n });\n installed.push('HTMLElement.autofocus');\n }\n\n return { installed };\n}\n\n/** Result of the side-effect installation performed by `snice/testing/dom`. */\nexport const domTestingCompatibility = installDOMTestingCompatibility();\n"],"names":[],"mappings":";;;;;;;;;;AAAA;;;;;;AAMG;AAaH;;;AAGG;AACG,SAAU,8BAA8B,CAC5C,KAAA,GAA0B,UAA8B,EAAA;IAExD,MAAM,SAAS,GAAqC,EAAE;AACtD,IAAA,MAAM,sBAAsB,GAAG,KAAK,CAAC,WAAW;AAChD,IAAA,IAAI,CAAC,sBAAsB;QAAE,OAAO,EAAE,SAAS,EAAE;AAEjD,IAAA,MAAM,SAAS,GAAG,sBAAsB,CAAC,SAAS;AAClD,IAAA,IAAI,EAAE,WAAW,IAAI,SAAS,CAAC,EAAE;AAC/B,QAAA,MAAM,CAAC,cAAc,CAAC,SAAS,EAAE,WAAW,EAAE;AAC5C,YAAA,YAAY,EAAE,IAAI;AAClB,YAAA,UAAU,EAAE,IAAI;YAChB,GAAG,GAAA;AACD,gBAAA,OAAO,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC;YACvC,CAAC;AACD,YAAA,GAAG,CAAoB,KAAc,EAAA;gBACnC,IAAI,CAAC,eAAe,CAAC,WAAW,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;YACnD,CAAC;AACF,SAAA,CAAC;AACF,QAAA,SAAS,CAAC,IAAI,CAAC,uBAAuB,CAAC;IACzC;IAEA,OAAO,EAAE,SAAS,EAAE;AACtB;AAEA;AACO,MAAM,uBAAuB,GAAG,8BAA8B;;;;;"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Standards-shaped DOM compatibility for non-browser test runners.
3
+ *
4
+ * This module only fills missing IDL surface. It never replaces an existing
5
+ * implementation and does not attempt to simulate layout, paint, or browser
6
+ * focus navigation.
7
+ */
8
+ export type DOMTestingCompatibilityFeature = 'HTMLElement.autofocus';
9
+ export interface DOMTestingCompatibilityReport {
10
+ /** Standards features installed because the current DOM omitted them. */
11
+ installed: readonly DOMTestingCompatibilityFeature[];
12
+ }
13
+ interface DOMTestingGlobal {
14
+ HTMLElement?: typeof HTMLElement;
15
+ }
16
+ /**
17
+ * Fill proven DOM-runner IDL gaps without replacing native implementations.
18
+ * Safe to call more than once.
19
+ */
20
+ export declare function installDOMTestingCompatibility(scope?: DOMTestingGlobal): DOMTestingCompatibilityReport;
21
+ /** Result of the side-effect installation performed by `snice/testing/dom`. */
22
+ export declare const domTestingCompatibility: DOMTestingCompatibilityReport;
23
+ export {};
@@ -0,0 +1,45 @@
1
+ /*!
2
+ * snice v7.3.0
3
+ * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
+ * (c) 2024
5
+ * Released under the MIT License.
6
+ *
7
+ * GENERATED FILE — DO NOT EDIT. Source: src/. Rebuild: npm run build:core
8
+ */
9
+ /**
10
+ * Standards-shaped DOM compatibility for non-browser test runners.
11
+ *
12
+ * This module only fills missing IDL surface. It never replaces an existing
13
+ * implementation and does not attempt to simulate layout, paint, or browser
14
+ * focus navigation.
15
+ */
16
+ /**
17
+ * Fill proven DOM-runner IDL gaps without replacing native implementations.
18
+ * Safe to call more than once.
19
+ */
20
+ function installDOMTestingCompatibility(scope = globalThis) {
21
+ const installed = [];
22
+ const HTMLElementConstructor = scope.HTMLElement;
23
+ if (!HTMLElementConstructor)
24
+ return { installed };
25
+ const prototype = HTMLElementConstructor.prototype;
26
+ if (!('autofocus' in prototype)) {
27
+ Object.defineProperty(prototype, 'autofocus', {
28
+ configurable: true,
29
+ enumerable: true,
30
+ get() {
31
+ return this.hasAttribute('autofocus');
32
+ },
33
+ set(value) {
34
+ this.toggleAttribute('autofocus', Boolean(value));
35
+ },
36
+ });
37
+ installed.push('HTMLElement.autofocus');
38
+ }
39
+ return { installed };
40
+ }
41
+ /** Result of the side-effect installation performed by `snice/testing/dom`. */
42
+ const domTestingCompatibility = installDOMTestingCompatibility();
43
+
44
+ export { domTestingCompatibility, installDOMTestingCompatibility };
45
+ //# sourceMappingURL=testing-dom.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing-dom.esm.js","sources":["../src/testing-dom.ts"],"sourcesContent":["/**\n * Standards-shaped DOM compatibility for non-browser test runners.\n *\n * This module only fills missing IDL surface. It never replaces an existing\n * implementation and does not attempt to simulate layout, paint, or browser\n * focus navigation.\n */\n\nexport type DOMTestingCompatibilityFeature = 'HTMLElement.autofocus';\n\nexport interface DOMTestingCompatibilityReport {\n /** Standards features installed because the current DOM omitted them. */\n installed: readonly DOMTestingCompatibilityFeature[];\n}\n\ninterface DOMTestingGlobal {\n HTMLElement?: typeof HTMLElement;\n}\n\n/**\n * Fill proven DOM-runner IDL gaps without replacing native implementations.\n * Safe to call more than once.\n */\nexport function installDOMTestingCompatibility(\n scope: DOMTestingGlobal = globalThis as DOMTestingGlobal\n): DOMTestingCompatibilityReport {\n const installed: DOMTestingCompatibilityFeature[] = [];\n const HTMLElementConstructor = scope.HTMLElement;\n if (!HTMLElementConstructor) return { installed };\n\n const prototype = HTMLElementConstructor.prototype;\n if (!('autofocus' in prototype)) {\n Object.defineProperty(prototype, 'autofocus', {\n configurable: true,\n enumerable: true,\n get(this: HTMLElement): boolean {\n return this.hasAttribute('autofocus');\n },\n set(this: HTMLElement, value: boolean) {\n this.toggleAttribute('autofocus', Boolean(value));\n },\n });\n installed.push('HTMLElement.autofocus');\n }\n\n return { installed };\n}\n\n/** Result of the side-effect installation performed by `snice/testing/dom`. */\nexport const domTestingCompatibility = installDOMTestingCompatibility();\n"],"names":[],"mappings":";;;;;;;;AAAA;;;;;;AAMG;AAaH;;;AAGG;AACG,SAAU,8BAA8B,CAC5C,KAAA,GAA0B,UAA8B,EAAA;IAExD,MAAM,SAAS,GAAqC,EAAE;AACtD,IAAA,MAAM,sBAAsB,GAAG,KAAK,CAAC,WAAW;AAChD,IAAA,IAAI,CAAC,sBAAsB;QAAE,OAAO,EAAE,SAAS,EAAE;AAEjD,IAAA,MAAM,SAAS,GAAG,sBAAsB,CAAC,SAAS;AAClD,IAAA,IAAI,EAAE,WAAW,IAAI,SAAS,CAAC,EAAE;AAC/B,QAAA,MAAM,CAAC,cAAc,CAAC,SAAS,EAAE,WAAW,EAAE;AAC5C,YAAA,YAAY,EAAE,IAAI;AAClB,YAAA,UAAU,EAAE,IAAI;YAChB,GAAG,GAAA;AACD,gBAAA,OAAO,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC;YACvC,CAAC;AACD,YAAA,GAAG,CAAoB,KAAc,EAAA;gBACnC,IAAI,CAAC,eAAe,CAAC,WAAW,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;YACnD,CAAC;AACF,SAAA,CAAC;AACF,QAAA,SAAS,CAAC,IAAI,CAAC,uBAAuB,CAAC;IACzC;IAEA,OAAO,EAAE,SAAS,EAAE;AACtB;AAEA;AACO,MAAM,uBAAuB,GAAG,8BAA8B;;;;"}
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.1.1
2
+ * snice v7.3.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.1.1
2
+ * snice v7.3.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -9,21 +9,21 @@ declare const REGISTERED_ELEMENTS_SET: unique symbol;
9
9
  * Represents the bundled router state that can notify registered elements of changes
10
10
  */
11
11
  export declare class Context {
12
- [REGISTERED_ELEMENTS]: WeakMap<HTMLElement, Set<string>>;
12
+ [REGISTERED_ELEMENTS]: WeakMap<object, Set<string>>;
13
13
  [IS_UPDATING]: boolean;
14
14
  /**
15
- * Register an element to receive context updates
15
+ * Register an element or controller to receive context updates
16
16
  * @internal Used by @context decorator
17
17
  */
18
- [CONTEXT_REGISTER]: (element: HTMLElement, methodName: string) => void;
18
+ [CONTEXT_REGISTER]: (element: object, methodName: string) => void;
19
19
  /**
20
- * Unregister an element from receiving context updates. With a methodName,
20
+ * Unregister a subscriber from context updates. With a methodName,
21
21
  * removes only that one handler (so a `once` handler tearing itself down
22
22
  * doesn't kill the element's other @context handlers); without one, removes
23
- * the element entirely (used on disconnect).
23
+ * the subscriber entirely (used on disconnect/detach).
24
24
  * @internal Used by @context decorator cleanup
25
25
  */
26
- [CONTEXT_UNREGISTER]: (element: HTMLElement, methodName?: string) => void;
26
+ [CONTEXT_UNREGISTER]: (element: object, methodName?: string) => void;
27
27
  /**
28
28
  * Update the context and notify all registered elements
29
29
  * Prevents infinite loops by tracking update state
@@ -31,10 +31,10 @@ export declare class Context {
31
31
  */
32
32
  [CONTEXT_UPDATE]: (context: AppContext, placards: Placard[], currentRoute: string, routeParams: RouteParams) => void;
33
33
  /**
34
- * Notify a specific element of the current context state
34
+ * Notify a specific subscriber of the current context state
35
35
  * @internal Used by @context decorator
36
36
  */
37
- [CONTEXT_NOTIFY_ELEMENT]: (element: HTMLElement) => void;
37
+ [CONTEXT_NOTIFY_ELEMENT]: (element: object) => void;
38
38
  private [REGISTERED_ELEMENTS_SET];
39
39
  /**
40
40
  * Unique immutable identifier for this context instance
@@ -2,6 +2,16 @@ import { Transition } from './transition';
2
2
  import { Placard } from './placard';
3
3
  import { Guard } from './guard';
4
4
  import { AppContext } from './app-context';
5
+ export interface PageRouteOptions {
6
+ /** Route pattern matched by the router. */
7
+ path: string;
8
+ /**
9
+ * Optional priority used only when route specificity ties.
10
+ * Lower numbers match first. Equal or omitted values preserve registration
11
+ * order, including the order of entries in PageOptions.routes.
12
+ */
13
+ order?: number;
14
+ }
5
15
  export interface PageOptions {
6
16
  /**
7
17
  * The tag name of the custom element.
@@ -11,9 +21,13 @@ export interface PageOptions {
11
21
  tag: string;
12
22
  /**
13
23
  * The routes that will trigger the page element.
24
+ * String entries are the normal form. Their array order breaks specificity
25
+ * ties. Use an object entry only when an explicit order is needed across
26
+ * registrations.
14
27
  * @example { routes: ['/login', '/login/:id'] }
28
+ * @example { routes: [{ path: '/login', order: 10 }] }
15
29
  */
16
- routes: string[];
30
+ routes: Array<string | PageRouteOptions>;
17
31
  /**
18
32
  * Optional per-page transition override
19
33
  */
@@ -2,11 +2,12 @@ import { PageOptions } from './page-options';
2
2
  import { Guard } from './guard';
3
3
  import { Transition } from './transition';
4
4
  import { AppContext } from './app-context';
5
+ import { Placard } from './placard';
5
6
  export interface RouterInstance {
6
7
  page: (pageOptions: PageOptions) => <C extends {
7
8
  new (...args: any[]): HTMLElement;
8
9
  }>(constructor: C, context: ClassDecoratorContext) => C;
9
10
  initialize: () => void;
10
11
  navigate: (path: string) => Promise<void>;
11
- register: (route: string, tag: string, transition?: Transition, guards?: Guard<AppContext> | Guard<AppContext>[]) => void;
12
+ register: (route: string, tag: string, transition?: Transition, guards?: Guard<AppContext> | Guard<AppContext>[], layout?: string | false, placard?: Placard | ((context: AppContext) => Placard), order?: number) => void;
12
13
  }
package/dist/utils.d.ts CHANGED
@@ -101,7 +101,9 @@ export declare function invokeImmediateWatchers(instance: any, constructor: any)
101
101
  /**
102
102
  * Create a debounced version of a function
103
103
  */
104
- export declare function createDebounced<T extends (...args: any[]) => any>(fn: T, delay: number): T;
104
+ export declare function createDebounced<T extends (...args: any[]) => any>(fn: T, delay: number): T & {
105
+ cancel(): void;
106
+ };
105
107
  /**
106
108
  * Create a throttled version of a function (leading + trailing edge)
107
109
  */
package/docs/ai/README.md CHANGED
@@ -101,13 +101,15 @@ Runtime first, then one bundle per component (any order after runtime):
101
101
  - Components re-render on any property change (decorated or not)
102
102
 
103
103
  **Architecture:**
104
- - **Elements 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
@@ -122,7 +122,7 @@ html`
122
122
  @dispatch(eventName: string, options?: { debounce?, throttle?, dispatchOnUndefined?, scope?, daemon?, ...EventInit })
123
123
  // Fires CustomEvent after method, detail = return value
124
124
  // Supports async methods (dispatches after promise resolves)
125
- // dispatchOnUndefined: false (default) — skips dispatch if method returns undefined
125
+ // dispatchOnUndefined: false — skips when return is undefined; omitted/true still dispatches
126
126
  // scope: 'global' | selector | EventTarget | () => EventTarget | null — redirects dispatch target
127
127
  ```
128
128
 
@@ -178,6 +178,8 @@ html`
178
178
  // Example:
179
179
  // @respond('fetch-user')
180
180
  // async handleFetchUser(payload: { id: string }) {
181
+ // const fetch = getContextFetch(this);
182
+ // if (!fetch) throw new Error('Controller requires context fetch');
181
183
  // const user = await fetch(`/api/users/${payload.id}`).then(r => r.json());
182
184
  // return user;
183
185
  // }
@@ -191,6 +193,14 @@ const context = getContext(elementOrController);
191
193
  // Provide before connect/attach. Call release() during app/test teardown.
192
194
  ```
193
195
 
196
+ Context transport:
197
+ ```typescript
198
+ const release = provideContext(appRoot, appContext, { fetch: appFetch });
199
+ const fetch = getContextFetch(elementOrController);
200
+ // Router supplies its ContextAwareFetcher-bound Context.fetch automatically.
201
+ // AppContext is not mutated with a reserved transport key.
202
+ ```
203
+
194
204
  ## Observers
195
205
 
196
206
  ```typescript
@@ -222,6 +232,7 @@ const context = getContext(elementOrController);
222
232
 
223
233
  @context(options?: { debounce?, throttle?, once? })
224
234
  // Method decorator: receives Context on navigation and ctx.update() calls
235
+ // Supported on pages, descendant elements, and attached controllers.
225
236
  // Called on: initial load, route change, ctx.update()
226
237
  // Example:
227
238
  // @context() handleContext(ctx: Context) {
@@ -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
 
@@ -90,8 +103,9 @@ as architecture suggestions, not errors.
90
103
  2. ControllerPart commit / attributeChangedCallback detects change
91
104
  3. Detach old controller (if any)
92
105
  4. Attach new controller (class refs skip the registry; reference-deduped)
93
- 5. setupEventHandlers for controller
94
- 6. On detach: cleanupEventHandlers
106
+ 5. Register `@context` and catch up with the current Router context
107
+ 6. Set up `@observe`, `@respond`, and `@on` handlers
108
+ 7. On detach: clean up every managed decorator
95
109
  - Class bindings own the element; their decorator name is reflected as a diagnostic-only `controller="name"` marker
96
110
  - Native elements: class bindings attach via ControllerPart; string attrs via MutationObserver
97
111
 
@@ -171,9 +185,12 @@ async *fetchData(): Response<Data> {
171
185
  }
172
186
 
173
187
  // Controller responds (receives payload, returns result directly)
188
+ private ctx!: Context;
189
+ @context() receiveContext(ctx: Context) { this.ctx = ctx; }
190
+
174
191
  @respond('fetch-data')
175
192
  async handleFetch(payload: { id: string }) {
176
- return await fetch(`/api/${payload.id}`).then(r => r.json());
193
+ return await this.ctx.fetch(`/api/${payload.id}`).then(r => r.json());
177
194
  }
178
195
  // Wiring: html`<my-element controller=${MyController}></my-element>` (or controller="my-controller" in raw HTML)
179
196
  ```