snice 4.28.0 → 4.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -10
- package/adapters/react/SniceProvider.d.ts +71 -0
- package/adapters/react/SniceProvider.js +49 -0
- package/adapters/react/SniceProvider.js.map +1 -0
- package/adapters/react/SniceRouter.d.ts +44 -0
- package/adapters/react/SniceRouter.js +190 -0
- package/adapters/react/SniceRouter.js.map +1 -0
- package/adapters/react/action-bar.d.ts +30 -0
- package/adapters/react/action-bar.d.ts.map +1 -0
- package/adapters/react/action-bar.js +24 -0
- package/adapters/react/action-bar.js.map +1 -0
- package/adapters/react/action-bar.tsx +38 -0
- package/adapters/react/binpack.d.ts +35 -0
- package/adapters/react/binpack.d.ts.map +1 -0
- package/adapters/react/binpack.js +24 -0
- package/adapters/react/binpack.js.map +1 -0
- package/adapters/react/binpack.tsx +43 -0
- package/adapters/react/components.d.ts +6 -0
- package/adapters/react/components.d.ts.map +1 -1
- package/adapters/react/components.js +3 -0
- package/adapters/react/components.js.map +1 -1
- package/adapters/react/components.ts +6 -0
- package/adapters/react/grid.d.ts +36 -0
- package/adapters/react/grid.d.ts.map +1 -0
- package/adapters/react/grid.js +24 -0
- package/adapters/react/grid.js.map +1 -0
- package/adapters/react/grid.tsx +44 -0
- package/adapters/react/index.d.ts +5 -0
- package/adapters/react/index.d.ts.map +1 -1
- package/adapters/react/index.js +3 -2
- package/adapters/react/index.js.map +1 -1
- package/adapters/react/index.ts +6 -3
- package/adapters/react/matchRoute.d.ts +16 -0
- package/adapters/react/matchRoute.js +32 -0
- package/adapters/react/matchRoute.js.map +1 -0
- package/adapters/react/types.d.ts +1 -15
- package/adapters/react/types.d.ts.map +1 -1
- package/adapters/react/types.ts +1 -15
- package/adapters/react/useRequestHandler.d.ts +56 -0
- package/adapters/react/useRequestHandler.js +103 -0
- package/adapters/react/useRequestHandler.js.map +1 -0
- package/bin/snice.js +8 -13
- package/bin/templates/{pwa → default}/index.html +1 -1
- package/bin/templates/{pwa → default}/src/components/app-header.ts +36 -18
- package/bin/templates/{pwa → default}/src/components/notification-badge.ts +2 -21
- package/bin/templates/{pwa → default}/src/components/search-bar.ts +12 -12
- package/bin/templates/default/src/context.ts +17 -0
- package/bin/templates/{pwa → default}/src/controllers/notification-controller.ts +10 -15
- package/bin/templates/{pwa → default}/src/daemons/notifications.ts +0 -12
- package/bin/templates/{pwa → default}/src/main.ts +1 -7
- package/bin/templates/{pwa → default}/src/middleware/error.ts +1 -8
- package/bin/templates/{pwa → default}/src/pages/dashboard.ts +17 -18
- package/bin/templates/{pwa → default}/src/pages/data.ts +24 -24
- package/bin/templates/{pwa → default}/src/pages/login.ts +3 -6
- package/bin/templates/{pwa → default}/src/pages/notifications.ts +21 -19
- package/bin/templates/{pwa → default}/src/pages/profile.ts +10 -12
- package/bin/templates/{pwa → default}/src/pages/settings.ts +22 -22
- package/bin/templates/default/src/router.ts +13 -0
- package/bin/templates/default/src/styles/global.css +16 -0
- package/bin/templates/{pwa → default}/tsconfig.json +2 -1
- package/bin/templates/react/README.md +124 -0
- package/bin/templates/react/global.d.ts +10 -0
- package/bin/templates/react/index.html +15 -0
- package/bin/templates/react/package.json +31 -0
- package/bin/templates/react/src/App.tsx +112 -0
- package/bin/templates/react/src/components/AppHeader.tsx +85 -0
- package/bin/templates/react/src/components/AppLayout.tsx +11 -0
- package/bin/templates/react/src/components/NotificationBadge.tsx +13 -0
- package/bin/templates/react/src/components/SearchBar.tsx +63 -0
- package/bin/templates/react/src/daemons/notifications.ts +136 -0
- package/bin/templates/react/src/fetcher.ts +15 -0
- package/bin/templates/react/src/guards/auth.ts +6 -0
- package/bin/templates/react/src/main.tsx +27 -0
- package/bin/templates/react/src/middleware/auth.ts +16 -0
- package/bin/templates/react/src/middleware/error.ts +29 -0
- package/bin/templates/react/src/middleware/retry.ts +31 -0
- package/bin/templates/react/src/pages/DashboardPage.tsx +111 -0
- package/bin/templates/react/src/pages/DataPage.tsx +119 -0
- package/bin/templates/react/src/pages/LoginPage.tsx +46 -0
- package/bin/templates/react/src/pages/NotificationsPage.tsx +119 -0
- package/bin/templates/react/src/pages/ProfilePage.tsx +92 -0
- package/bin/templates/react/src/pages/SettingsPage.tsx +165 -0
- package/bin/templates/react/src/services/auth.ts +48 -0
- package/bin/templates/react/src/services/jwt.ts +35 -0
- package/bin/templates/react/src/services/storage.ts +24 -0
- package/bin/templates/react/src/styles/global.css +382 -0
- package/bin/templates/react/src/types/auth.ts +21 -0
- package/bin/templates/react/src/types/notifications.ts +9 -0
- package/bin/templates/react/tests/helpers/test-utils.ts +79 -0
- package/bin/templates/react/tests/middleware/auth.test.ts +67 -0
- package/bin/templates/react/tests/middleware/error.test.ts +105 -0
- package/bin/templates/react/tests/middleware/retry.test.ts +103 -0
- package/bin/templates/react/tests/services/auth.test.ts +89 -0
- package/bin/templates/react/tests/services/jwt.test.ts +76 -0
- package/bin/templates/react/tests/services/storage.test.ts +69 -0
- package/bin/templates/{base → react}/tsconfig.json +4 -6
- package/bin/templates/react/vite.config.ts +18 -0
- package/bin/templates/react/vitest.config.ts +18 -0
- package/dist/cdn/accordion/snice-accordion.js +1 -1
- package/dist/cdn/accordion/snice-accordion.min.js +1 -1
- package/dist/cdn/action-bar/README.md +27 -0
- package/dist/cdn/action-bar/snice-action-bar.js +249 -0
- package/dist/cdn/action-bar/snice-action-bar.js.map +1 -0
- package/dist/cdn/action-bar/snice-action-bar.min.js +12 -0
- package/dist/cdn/action-bar/snice-action-bar.min.js.map +1 -0
- package/dist/cdn/activity-feed/snice-activity-feed.js +1 -1
- package/dist/cdn/activity-feed/snice-activity-feed.min.js +1 -1
- package/dist/cdn/alert/snice-alert.js +1 -1
- package/dist/cdn/alert/snice-alert.min.js +1 -1
- package/dist/cdn/app-tiles/snice-app-tiles.js +1 -1
- package/dist/cdn/app-tiles/snice-app-tiles.min.js +1 -1
- package/dist/cdn/approval-flow/snice-approval-flow.js +1 -1
- package/dist/cdn/approval-flow/snice-approval-flow.min.js +1 -1
- package/dist/cdn/audio-recorder/snice-audio-recorder.js +1 -1
- package/dist/cdn/audio-recorder/snice-audio-recorder.min.js +1 -1
- package/dist/cdn/availability/snice-availability.js +1 -1
- package/dist/cdn/availability/snice-availability.min.js +1 -1
- package/dist/cdn/avatar/snice-avatar.js +1 -1
- package/dist/cdn/avatar/snice-avatar.min.js +1 -1
- package/dist/cdn/avatar-group/snice-avatar-group.js +1 -1
- package/dist/cdn/avatar-group/snice-avatar-group.min.js +1 -1
- package/dist/cdn/badge/snice-badge.js +1 -1
- package/dist/cdn/badge/snice-badge.min.js +1 -1
- package/dist/cdn/banner/snice-banner.js +1 -1
- package/dist/cdn/banner/snice-banner.min.js +1 -1
- package/dist/cdn/binpack/README.md +27 -0
- package/dist/cdn/binpack/snice-binpack.js +1037 -0
- package/dist/cdn/binpack/snice-binpack.js.map +1 -0
- package/dist/cdn/binpack/snice-binpack.min.js +13 -0
- package/dist/cdn/binpack/snice-binpack.min.js.map +1 -0
- package/dist/cdn/book/snice-book.js +1 -1
- package/dist/cdn/book/snice-book.min.js +1 -1
- package/dist/cdn/booking/snice-booking.js +1 -1
- package/dist/cdn/booking/snice-booking.min.js +1 -1
- package/dist/cdn/breadcrumbs/snice-breadcrumbs.js +1 -1
- package/dist/cdn/breadcrumbs/snice-breadcrumbs.min.js +1 -1
- package/dist/cdn/button/README.md +1 -1
- package/dist/cdn/button/snice-button.js +2 -2
- package/dist/cdn/button/snice-button.js.map +1 -1
- package/dist/cdn/button/snice-button.min.js +2 -2
- package/dist/cdn/button/snice-button.min.js.map +1 -1
- package/dist/cdn/calendar/snice-calendar.js +1 -1
- package/dist/cdn/calendar/snice-calendar.min.js +1 -1
- package/dist/cdn/camera/snice-camera.js +1 -1
- package/dist/cdn/camera/snice-camera.min.js +1 -1
- package/dist/cdn/camera-annotate/snice-camera-annotate.js +1 -1
- package/dist/cdn/camera-annotate/snice-camera-annotate.min.js +1 -1
- package/dist/cdn/candlestick/snice-candlestick.js +1 -1
- package/dist/cdn/candlestick/snice-candlestick.min.js +1 -1
- package/dist/cdn/card/snice-card.js +1 -1
- package/dist/cdn/card/snice-card.min.js +1 -1
- package/dist/cdn/carousel/snice-carousel.js +1 -1
- package/dist/cdn/carousel/snice-carousel.min.js +1 -1
- package/dist/cdn/cart/snice-cart.js +1 -1
- package/dist/cdn/cart/snice-cart.min.js +1 -1
- package/dist/cdn/chart/snice-chart.js +1 -1
- package/dist/cdn/chart/snice-chart.min.js +1 -1
- package/dist/cdn/chat/snice-chat.js +1 -1
- package/dist/cdn/chat/snice-chat.min.js +1 -1
- package/dist/cdn/checkbox/snice-checkbox.js +1 -1
- package/dist/cdn/checkbox/snice-checkbox.min.js +1 -1
- package/dist/cdn/chip/README.md +2 -2
- package/dist/cdn/chip/snice-chip.js +2 -2
- package/dist/cdn/chip/snice-chip.js.map +1 -1
- package/dist/cdn/chip/snice-chip.min.js +3 -3
- package/dist/cdn/chip/snice-chip.min.js.map +1 -1
- package/dist/cdn/code-block/snice-code-block.js +1 -1
- package/dist/cdn/code-block/snice-code-block.min.js +1 -1
- package/dist/cdn/color-display/snice-color-display.js +1 -1
- package/dist/cdn/color-display/snice-color-display.min.js +1 -1
- package/dist/cdn/color-picker/snice-color-picker.js +1 -1
- package/dist/cdn/color-picker/snice-color-picker.min.js +1 -1
- package/dist/cdn/command-palette/snice-command-palette.js +1 -1
- package/dist/cdn/command-palette/snice-command-palette.min.js +1 -1
- package/dist/cdn/comments/snice-comments.js +1 -1
- package/dist/cdn/comments/snice-comments.min.js +1 -1
- package/dist/cdn/countdown/snice-countdown.js +1 -1
- package/dist/cdn/countdown/snice-countdown.min.js +1 -1
- package/dist/cdn/cropper/snice-cropper.js +1 -1
- package/dist/cdn/cropper/snice-cropper.min.js +1 -1
- package/dist/cdn/data-card/snice-data-card.js +1 -1
- package/dist/cdn/data-card/snice-data-card.min.js +1 -1
- package/dist/cdn/date-picker/README.md +1 -1
- package/dist/cdn/date-picker/snice-date-picker.js +2 -2
- package/dist/cdn/date-picker/snice-date-picker.js.map +1 -1
- package/dist/cdn/date-picker/snice-date-picker.min.js +2 -2
- package/dist/cdn/date-picker/snice-date-picker.min.js.map +1 -1
- package/dist/cdn/date-range-picker/README.md +1 -1
- package/dist/cdn/date-range-picker/snice-date-range-picker.js +2 -2
- package/dist/cdn/date-range-picker/snice-date-range-picker.js.map +1 -1
- package/dist/cdn/date-range-picker/snice-date-range-picker.min.js +11 -11
- package/dist/cdn/date-range-picker/snice-date-range-picker.min.js.map +1 -1
- package/dist/cdn/date-time-picker/README.md +1 -1
- package/dist/cdn/date-time-picker/snice-date-time-picker.js +2 -2
- package/dist/cdn/date-time-picker/snice-date-time-picker.js.map +1 -1
- package/dist/cdn/date-time-picker/snice-date-time-picker.min.js +2 -2
- package/dist/cdn/date-time-picker/snice-date-time-picker.min.js.map +1 -1
- package/dist/cdn/diff/snice-diff.js +1 -1
- package/dist/cdn/diff/snice-diff.min.js +1 -1
- package/dist/cdn/divider/snice-divider.js +1 -1
- package/dist/cdn/divider/snice-divider.min.js +1 -1
- package/dist/cdn/doc/snice-doc.js +1 -1
- package/dist/cdn/doc/snice-doc.min.js +1 -1
- package/dist/cdn/draw/README.md +2 -2
- package/dist/cdn/draw/snice-draw.js +26 -4
- package/dist/cdn/draw/snice-draw.js.map +1 -1
- package/dist/cdn/draw/snice-draw.min.js +3 -3
- package/dist/cdn/draw/snice-draw.min.js.map +1 -1
- package/dist/cdn/drawer/snice-drawer.js +1 -1
- package/dist/cdn/drawer/snice-drawer.min.js +1 -1
- package/dist/cdn/empty-state/snice-empty-state.js +1 -1
- package/dist/cdn/empty-state/snice-empty-state.min.js +1 -1
- package/dist/cdn/estimate/snice-estimate.js +1 -1
- package/dist/cdn/estimate/snice-estimate.min.js +1 -1
- package/dist/cdn/file-gallery/snice-file-gallery.js +1 -1
- package/dist/cdn/file-gallery/snice-file-gallery.min.js +1 -1
- package/dist/cdn/file-upload/snice-file-upload.js +1 -1
- package/dist/cdn/file-upload/snice-file-upload.min.js +1 -1
- package/dist/cdn/flip-card/snice-flip-card.js +1 -1
- package/dist/cdn/flip-card/snice-flip-card.min.js +1 -1
- package/dist/cdn/flow/snice-flow.js +1 -1
- package/dist/cdn/flow/snice-flow.min.js +1 -1
- package/dist/cdn/form-layout/snice-form-layout.js +1 -1
- package/dist/cdn/form-layout/snice-form-layout.min.js +1 -1
- package/dist/cdn/funnel/snice-funnel.js +1 -1
- package/dist/cdn/funnel/snice-funnel.min.js +1 -1
- package/dist/cdn/gantt/snice-gantt.js +1 -1
- package/dist/cdn/gantt/snice-gantt.min.js +1 -1
- package/dist/cdn/gauge/snice-gauge.js +1 -1
- package/dist/cdn/gauge/snice-gauge.min.js +1 -1
- package/dist/cdn/grid/README.md +27 -0
- package/dist/cdn/grid/snice-grid.js +862 -0
- package/dist/cdn/grid/snice-grid.js.map +1 -0
- package/dist/cdn/grid/snice-grid.min.js +13 -0
- package/dist/cdn/grid/snice-grid.min.js.map +1 -0
- package/dist/cdn/heatmap/snice-heatmap.js +1 -1
- package/dist/cdn/heatmap/snice-heatmap.min.js +1 -1
- package/dist/cdn/image/snice-image.js +1 -1
- package/dist/cdn/image/snice-image.min.js +1 -1
- package/dist/cdn/input/snice-input.js +1 -1
- package/dist/cdn/input/snice-input.min.js +1 -1
- package/dist/cdn/invoice/snice-invoice.js +1 -1
- package/dist/cdn/invoice/snice-invoice.min.js +1 -1
- package/dist/cdn/kanban/snice-kanban.js +1 -1
- package/dist/cdn/kanban/snice-kanban.min.js +1 -1
- package/dist/cdn/key-value/snice-key-value.js +1 -1
- package/dist/cdn/key-value/snice-key-value.min.js +1 -1
- package/dist/cdn/kpi/snice-kpi.js +1 -1
- package/dist/cdn/kpi/snice-kpi.min.js +1 -1
- package/dist/cdn/layout/snice-layout.js +1 -1
- package/dist/cdn/layout/snice-layout.min.js +1 -1
- package/dist/cdn/leaderboard/snice-leaderboard.js +1 -1
- package/dist/cdn/leaderboard/snice-leaderboard.min.js +1 -1
- package/dist/cdn/link/snice-link.js +1 -1
- package/dist/cdn/link/snice-link.min.js +1 -1
- package/dist/cdn/link-preview/snice-link-preview.js +1 -1
- package/dist/cdn/link-preview/snice-link-preview.min.js +1 -1
- package/dist/cdn/list/snice-list.js +1 -1
- package/dist/cdn/list/snice-list.min.js +1 -1
- package/dist/cdn/location/snice-location.js +1 -1
- package/dist/cdn/location/snice-location.min.js +1 -1
- package/dist/cdn/login/README.md +2 -2
- package/dist/cdn/login/snice-login.js +2 -2
- package/dist/cdn/login/snice-login.js.map +1 -1
- package/dist/cdn/login/snice-login.min.js +2 -2
- package/dist/cdn/login/snice-login.min.js.map +1 -1
- package/dist/cdn/map/snice-map.js +1 -1
- package/dist/cdn/map/snice-map.min.js +1 -1
- package/dist/cdn/markdown/snice-markdown.js +1 -1
- package/dist/cdn/markdown/snice-markdown.min.js +1 -1
- package/dist/cdn/masonry/snice-masonry.js +1 -1
- package/dist/cdn/masonry/snice-masonry.min.js +1 -1
- package/dist/cdn/menu/snice-menu.js +1 -1
- package/dist/cdn/menu/snice-menu.min.js +1 -1
- package/dist/cdn/message-strip/README.md +2 -2
- package/dist/cdn/message-strip/snice-message-strip.js +2 -2
- package/dist/cdn/message-strip/snice-message-strip.js.map +1 -1
- package/dist/cdn/message-strip/snice-message-strip.min.js +6 -6
- package/dist/cdn/message-strip/snice-message-strip.min.js.map +1 -1
- package/dist/cdn/metric-table/snice-metric-table.js +1 -1
- package/dist/cdn/metric-table/snice-metric-table.min.js +1 -1
- package/dist/cdn/modal/snice-modal.js +1 -1
- package/dist/cdn/modal/snice-modal.min.js +1 -1
- package/dist/cdn/music-player/snice-music-player.js +1 -1
- package/dist/cdn/music-player/snice-music-player.min.js +1 -1
- package/dist/cdn/nav/snice-nav.js +1 -1
- package/dist/cdn/nav/snice-nav.min.js +1 -1
- package/dist/cdn/network-graph/snice-network-graph.js +1 -1
- package/dist/cdn/network-graph/snice-network-graph.min.js +1 -1
- package/dist/cdn/notification-center/snice-notification-center.js +1 -1
- package/dist/cdn/notification-center/snice-notification-center.min.js +1 -1
- package/dist/cdn/order-tracker/snice-order-tracker.js +1 -1
- package/dist/cdn/order-tracker/snice-order-tracker.min.js +1 -1
- package/dist/cdn/org-chart/snice-org-chart.js +1 -1
- package/dist/cdn/org-chart/snice-org-chart.min.js +1 -1
- package/dist/cdn/pagination/snice-pagination.js +1 -1
- package/dist/cdn/pagination/snice-pagination.min.js +1 -1
- package/dist/cdn/paint/README.md +2 -2
- package/dist/cdn/paint/snice-paint.js +26 -3
- package/dist/cdn/paint/snice-paint.js.map +1 -1
- package/dist/cdn/paint/snice-paint.min.js +3 -3
- package/dist/cdn/paint/snice-paint.min.js.map +1 -1
- package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +1 -1
- package/dist/cdn/pdf-viewer/snice-pdf-viewer.min.js +1 -1
- package/dist/cdn/permission-matrix/snice-permission-matrix.js +1 -1
- package/dist/cdn/permission-matrix/snice-permission-matrix.min.js +1 -1
- package/dist/cdn/podcast-player/snice-podcast-player.js +1 -1
- package/dist/cdn/podcast-player/snice-podcast-player.min.js +1 -1
- package/dist/cdn/pricing-table/snice-pricing-table.js +1 -1
- package/dist/cdn/pricing-table/snice-pricing-table.min.js +1 -1
- package/dist/cdn/product-card/README.md +1 -1
- package/dist/cdn/product-card/snice-product-card.js +1 -1
- package/dist/cdn/product-card/snice-product-card.min.js +1 -1
- package/dist/cdn/progress/snice-progress.js +1 -1
- package/dist/cdn/progress/snice-progress.min.js +1 -1
- package/dist/cdn/progress-ring/snice-progress-ring.js +1 -1
- package/dist/cdn/progress-ring/snice-progress-ring.min.js +1 -1
- package/dist/cdn/qr-code/snice-qr-code.js +1 -1
- package/dist/cdn/qr-code/snice-qr-code.min.js +1 -1
- package/dist/cdn/qr-reader/snice-qr-reader.js +1 -1
- package/dist/cdn/qr-reader/snice-qr-reader.min.js +1 -1
- package/dist/cdn/radio/snice-radio.js +1 -1
- package/dist/cdn/radio/snice-radio.min.js +1 -1
- package/dist/cdn/range-slider/snice-range-slider.js +1 -1
- package/dist/cdn/range-slider/snice-range-slider.min.js +1 -1
- package/dist/cdn/rating/snice-rating.js +1 -1
- package/dist/cdn/rating/snice-rating.min.js +1 -1
- package/dist/cdn/receipt/snice-receipt.js +1 -1
- package/dist/cdn/receipt/snice-receipt.min.js +1 -1
- package/dist/cdn/recipe/snice-recipe.js +1 -1
- package/dist/cdn/recipe/snice-recipe.min.js +1 -1
- package/dist/cdn/runtime/README.md +2 -2
- package/dist/cdn/runtime/snice-runtime.esm.js +109 -16
- package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.esm.min.js +8 -8
- package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.js +109 -15
- package/dist/cdn/runtime/snice-runtime.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.min.js +7 -7
- package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
- package/dist/cdn/sankey/snice-sankey.js +1 -1
- package/dist/cdn/sankey/snice-sankey.min.js +1 -1
- package/dist/cdn/segmented-control/snice-segmented-control.js +1 -1
- package/dist/cdn/segmented-control/snice-segmented-control.min.js +1 -1
- package/dist/cdn/select/snice-select.js +1 -1
- package/dist/cdn/select/snice-select.min.js +1 -1
- package/dist/cdn/skeleton/snice-skeleton.js +1 -1
- package/dist/cdn/skeleton/snice-skeleton.min.js +1 -1
- package/dist/cdn/slider/snice-slider.js +1 -1
- package/dist/cdn/slider/snice-slider.min.js +1 -1
- package/dist/cdn/sortable/snice-sortable.js +1 -1
- package/dist/cdn/sortable/snice-sortable.min.js +1 -1
- package/dist/cdn/sparkline/snice-sparkline.js +1 -1
- package/dist/cdn/sparkline/snice-sparkline.min.js +1 -1
- package/dist/cdn/spinner/snice-spinner.js +1 -1
- package/dist/cdn/spinner/snice-spinner.min.js +1 -1
- package/dist/cdn/split-button/snice-split-button.js +1 -1
- package/dist/cdn/split-button/snice-split-button.min.js +1 -1
- package/dist/cdn/split-pane/snice-split-pane.js +1 -1
- package/dist/cdn/split-pane/snice-split-pane.min.js +1 -1
- package/dist/cdn/spotlight/snice-spotlight.js +1 -1
- package/dist/cdn/spotlight/snice-spotlight.min.js +1 -1
- package/dist/cdn/spreadsheet/snice-spreadsheet.js +1 -1
- package/dist/cdn/spreadsheet/snice-spreadsheet.min.js +1 -1
- package/dist/cdn/stat-group/snice-stat-group.js +1 -1
- package/dist/cdn/stat-group/snice-stat-group.min.js +1 -1
- package/dist/cdn/step-input/snice-step-input.js +1 -1
- package/dist/cdn/step-input/snice-step-input.min.js +1 -1
- package/dist/cdn/stepper/snice-stepper.js +1 -1
- package/dist/cdn/stepper/snice-stepper.min.js +1 -1
- package/dist/cdn/switch/snice-switch.js +1 -1
- package/dist/cdn/switch/snice-switch.min.js +1 -1
- package/dist/cdn/table/README.md +1 -1
- package/dist/cdn/table/snice-table.js +2 -2
- package/dist/cdn/table/snice-table.js.map +1 -1
- package/dist/cdn/table/snice-table.min.js +2 -2
- package/dist/cdn/table/snice-table.min.js.map +1 -1
- package/dist/cdn/tabs/snice-tabs.js +1 -1
- package/dist/cdn/tabs/snice-tabs.min.js +1 -1
- package/dist/cdn/tag/README.md +1 -1
- package/dist/cdn/tag/snice-tag.js +2 -2
- package/dist/cdn/tag/snice-tag.js.map +1 -1
- package/dist/cdn/tag/snice-tag.min.js +3 -3
- package/dist/cdn/tag/snice-tag.min.js.map +1 -1
- package/dist/cdn/tag-input/README.md +2 -2
- package/dist/cdn/tag-input/snice-tag-input.js +2 -2
- package/dist/cdn/tag-input/snice-tag-input.js.map +1 -1
- package/dist/cdn/tag-input/snice-tag-input.min.js +2 -2
- package/dist/cdn/tag-input/snice-tag-input.min.js.map +1 -1
- package/dist/cdn/terminal/snice-terminal.js +1 -1
- package/dist/cdn/terminal/snice-terminal.min.js +1 -1
- package/dist/cdn/testimonial/snice-testimonial.js +1 -1
- package/dist/cdn/testimonial/snice-testimonial.min.js +1 -1
- package/dist/cdn/textarea/snice-textarea.js +1 -1
- package/dist/cdn/textarea/snice-textarea.min.js +1 -1
- package/dist/cdn/time-picker/README.md +1 -1
- package/dist/cdn/time-picker/snice-time-picker.js +2 -2
- package/dist/cdn/time-picker/snice-time-picker.js.map +1 -1
- package/dist/cdn/time-picker/snice-time-picker.min.js +2 -2
- package/dist/cdn/time-picker/snice-time-picker.min.js.map +1 -1
- package/dist/cdn/time-range-picker/snice-time-range-picker.js +1 -1
- package/dist/cdn/time-range-picker/snice-time-range-picker.min.js +1 -1
- package/dist/cdn/timeline/snice-timeline.js +1 -1
- package/dist/cdn/timeline/snice-timeline.min.js +1 -1
- package/dist/cdn/timer/snice-timer.js +1 -1
- package/dist/cdn/timer/snice-timer.min.js +1 -1
- package/dist/cdn/toast/README.md +1 -1
- package/dist/cdn/toast/snice-toast.js +7 -3
- package/dist/cdn/toast/snice-toast.js.map +1 -1
- package/dist/cdn/toast/snice-toast.min.js +6 -6
- package/dist/cdn/toast/snice-toast.min.js.map +1 -1
- package/dist/cdn/tooltip/snice-tooltip.js +1 -1
- package/dist/cdn/tooltip/snice-tooltip.min.js +1 -1
- package/dist/cdn/tree/snice-tree.js +1 -1
- package/dist/cdn/tree/snice-tree.min.js +1 -1
- package/dist/cdn/treemap/snice-treemap.js +1 -1
- package/dist/cdn/treemap/snice-treemap.min.js +1 -1
- package/dist/cdn/user-card/snice-user-card.js +1 -1
- package/dist/cdn/user-card/snice-user-card.min.js +1 -1
- package/dist/cdn/video-player/snice-video-player.js +1 -1
- package/dist/cdn/video-player/snice-video-player.min.js +1 -1
- package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +1 -1
- package/dist/cdn/virtual-scroller/snice-virtual-scroller.min.js +1 -1
- package/dist/cdn/waterfall/README.md +1 -1
- package/dist/cdn/waterfall/snice-waterfall.js +1 -1
- package/dist/cdn/waterfall/snice-waterfall.min.js +1 -1
- package/dist/cdn/weather/snice-weather.js +1 -1
- package/dist/cdn/weather/snice-weather.min.js +1 -1
- package/dist/cdn/work-order/snice-work-order.js +1 -1
- package/dist/cdn/work-order/snice-work-order.min.js +1 -1
- package/dist/components/action-bar/snice-action-bar.d.ts +22 -0
- package/dist/components/action-bar/snice-action-bar.js +182 -0
- package/dist/components/action-bar/snice-action-bar.js.map +1 -0
- package/dist/components/action-bar/snice-action-bar.types.d.ts +17 -0
- package/dist/components/binpack/snice-binpack.d.ts +82 -0
- package/dist/components/binpack/snice-binpack.js +970 -0
- package/dist/components/binpack/snice-binpack.js.map +1 -0
- package/dist/components/binpack/snice-binpack.types.d.ts +52 -0
- package/dist/components/button/snice-button.js +1 -1
- package/dist/components/button/snice-button.js.map +1 -1
- package/dist/components/chip/snice-chip.js +1 -1
- package/dist/components/chip/snice-chip.js.map +1 -1
- package/dist/components/date-picker/snice-date-picker.js +1 -1
- package/dist/components/date-picker/snice-date-picker.js.map +1 -1
- package/dist/components/date-range-picker/snice-date-range-picker.js +1 -1
- package/dist/components/date-range-picker/snice-date-range-picker.js.map +1 -1
- package/dist/components/date-time-picker/snice-date-time-picker.js +1 -1
- package/dist/components/date-time-picker/snice-date-time-picker.js.map +1 -1
- package/dist/components/draw/snice-draw.d.ts +2 -0
- package/dist/components/draw/snice-draw.js +25 -3
- package/dist/components/draw/snice-draw.js.map +1 -1
- package/dist/components/grid/snice-grid.d.ts +73 -0
- package/dist/components/grid/snice-grid.js +795 -0
- package/dist/components/grid/snice-grid.js.map +1 -0
- package/dist/components/grid/snice-grid.types.d.ts +41 -0
- package/dist/components/message-strip/snice-message-strip.js +1 -1
- package/dist/components/message-strip/snice-message-strip.js.map +1 -1
- package/dist/components/paint/snice-paint.d.ts +2 -0
- package/dist/components/paint/snice-paint.js +25 -2
- package/dist/components/paint/snice-paint.js.map +1 -1
- package/dist/components/tag/snice-tag.js +1 -1
- package/dist/components/tag/snice-tag.js.map +1 -1
- package/dist/components/tag-input/snice-tag-input.js +1 -1
- package/dist/components/tag-input/snice-tag-input.js.map +1 -1
- package/dist/components/theme/theme.css +15 -0
- package/dist/components/time-picker/snice-time-picker.js +1 -1
- package/dist/components/time-picker/snice-time-picker.js.map +1 -1
- package/dist/components/toast/snice-toast-container.js +4 -0
- package/dist/components/toast/snice-toast-container.js.map +1 -1
- package/dist/components/toast/snice-toast.js +2 -2
- package/dist/create-request-handler.d.ts +42 -0
- package/dist/index.cjs +106 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.esm.js +106 -13
- package/dist/index.esm.js.map +1 -1
- package/dist/index.iife.js +106 -12
- package/dist/index.iife.js.map +1 -1
- package/dist/react/SniceProvider.d.ts +71 -0
- package/dist/react/SniceProvider.js +49 -0
- package/dist/react/SniceProvider.js.map +1 -0
- package/dist/react/SniceRouter.d.ts +44 -0
- package/dist/react/SniceRouter.js +190 -0
- package/dist/react/SniceRouter.js.map +1 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.js +14 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/matchRoute.d.ts +16 -0
- package/dist/react/matchRoute.js +32 -0
- package/dist/react/matchRoute.js.map +1 -0
- package/dist/react/useRequestHandler.d.ts +56 -0
- package/dist/react/useRequestHandler.js +103 -0
- package/dist/react/useRequestHandler.js.map +1 -0
- package/dist/symbols.cjs +1 -1
- package/dist/symbols.esm.js +1 -1
- package/dist/transitions.cjs +1 -1
- package/dist/transitions.esm.js +1 -1
- package/dist/types/guard.d.ts +4 -11
- package/docs/ai/DEVELOPMENT.md +1 -1
- package/docs/ai/README.md +7 -7
- package/docs/ai/components/accordion.md +46 -80
- package/docs/ai/components/action-bar.md +75 -0
- package/docs/ai/components/activity-feed.md +7 -7
- package/docs/ai/components/alert.md +26 -44
- package/docs/ai/components/app-tiles.md +34 -39
- package/docs/ai/components/approval-flow.md +1 -1
- package/docs/ai/components/audio-recorder.md +35 -67
- package/docs/ai/components/availability.md +13 -34
- package/docs/ai/components/avatar-group.md +26 -13
- package/docs/ai/components/avatar.md +52 -36
- package/docs/ai/components/badge.md +21 -32
- package/docs/ai/components/banner.md +21 -43
- package/docs/ai/components/binpack.md +89 -0
- package/docs/ai/components/book.md +25 -23
- package/docs/ai/components/booking.md +31 -36
- package/docs/ai/components/breadcrumbs.md +36 -11
- package/docs/ai/components/button.md +33 -44
- package/docs/ai/components/calendar.md +37 -70
- package/docs/ai/components/camera-annotate.md +31 -64
- package/docs/ai/components/camera.md +38 -120
- package/docs/ai/components/candlestick.md +32 -52
- package/docs/ai/components/card.md +20 -30
- package/docs/ai/components/carousel.md +32 -32
- package/docs/ai/components/cart.md +24 -29
- package/docs/ai/components/chart.md +29 -114
- package/docs/ai/components/chat.md +38 -75
- package/docs/ai/components/checkbox.md +22 -41
- package/docs/ai/components/chip.md +18 -54
- package/docs/ai/components/code-block.md +57 -178
- package/docs/ai/components/color-display.md +12 -32
- package/docs/ai/components/color-picker.md +17 -39
- package/docs/ai/components/command-palette.md +49 -71
- package/docs/ai/components/comments.md +55 -36
- package/docs/ai/components/countdown.md +28 -30
- package/docs/ai/components/cropper.md +33 -33
- package/docs/ai/components/data-card.md +20 -14
- package/docs/ai/components/date-picker.md +40 -47
- package/docs/ai/components/date-range-picker.md +31 -15
- package/docs/ai/components/date-time-picker.md +23 -46
- package/docs/ai/components/diff.md +30 -31
- package/docs/ai/components/divider.md +17 -47
- package/docs/ai/components/doc.md +43 -68
- package/docs/ai/components/draw.md +35 -87
- package/docs/ai/components/drawer.md +48 -77
- package/docs/ai/components/empty-state.md +10 -44
- package/docs/ai/components/estimate.md +33 -58
- package/docs/ai/components/file-gallery.md +48 -100
- package/docs/ai/components/file-upload.md +17 -53
- package/docs/ai/components/flip-card.md +31 -23
- package/docs/ai/components/flow.md +37 -65
- package/docs/ai/components/form-builder.md +35 -75
- package/docs/ai/components/form-layout.md +10 -20
- package/docs/ai/components/funnel.md +33 -48
- package/docs/ai/components/gantt.md +27 -23
- package/docs/ai/components/gauge.md +16 -17
- package/docs/ai/components/grid.md +116 -0
- package/docs/ai/components/heatmap.md +21 -21
- package/docs/ai/components/image.md +7 -13
- package/docs/ai/components/input.md +38 -70
- package/docs/ai/components/invoice.md +35 -36
- package/docs/ai/components/kanban.md +32 -61
- package/docs/ai/components/key-value.md +32 -16
- package/docs/ai/components/kpi.md +38 -73
- package/docs/ai/components/layout.md +29 -23
- package/docs/ai/components/leaderboard.md +28 -37
- package/docs/ai/components/link-preview.md +12 -18
- package/docs/ai/components/link.md +10 -7
- package/docs/ai/components/list.md +21 -5
- package/docs/ai/components/location.md +21 -25
- package/docs/ai/components/login.md +14 -9
- package/docs/ai/components/map.md +27 -33
- package/docs/ai/components/markdown.md +20 -24
- package/docs/ai/components/masonry.md +10 -14
- package/docs/ai/components/mentions.md +26 -12
- package/docs/ai/components/menu.md +54 -52
- package/docs/ai/components/message-strip.md +20 -20
- package/docs/ai/components/metric-table.md +14 -21
- package/docs/ai/components/modal.md +27 -18
- package/docs/ai/components/music-player.md +49 -41
- package/docs/ai/components/nav.md +34 -31
- package/docs/ai/components/network-graph.md +27 -42
- package/docs/ai/components/notification-center.md +31 -33
- package/docs/ai/components/order-tracker.md +36 -27
- package/docs/ai/components/org-chart.md +36 -31
- package/docs/ai/components/pagination.md +34 -26
- package/docs/ai/components/paint.md +53 -91
- package/docs/ai/components/pdf-viewer.md +35 -36
- package/docs/ai/components/permission-matrix.md +26 -31
- package/docs/ai/components/podcast-player.md +64 -70
- package/docs/ai/components/pricing-table.md +37 -48
- package/docs/ai/components/product-card.md +58 -41
- package/docs/ai/components/progress-ring.md +20 -24
- package/docs/ai/components/progress.md +28 -45
- package/docs/ai/components/qr-code.md +25 -27
- package/docs/ai/components/qr-reader.md +20 -23
- package/docs/ai/components/radio.md +17 -15
- package/docs/ai/components/range-slider.md +22 -11
- package/docs/ai/components/rating.md +29 -33
- package/docs/ai/components/receipt.md +50 -127
- package/docs/ai/components/recipe.md +44 -42
- package/docs/ai/components/sankey.md +21 -30
- package/docs/ai/components/scheduler.md +29 -41
- package/docs/ai/components/segmented-control.md +11 -15
- package/docs/ai/components/select.md +58 -102
- package/docs/ai/components/skeleton.md +16 -30
- package/docs/ai/components/slider.md +26 -20
- package/docs/ai/components/sortable.md +25 -27
- package/docs/ai/components/sparkline.md +21 -17
- package/docs/ai/components/spinner.md +9 -5
- package/docs/ai/components/split-button.md +10 -13
- package/docs/ai/components/split-pane.md +19 -14
- package/docs/ai/components/spotlight.md +31 -26
- package/docs/ai/components/spreadsheet.md +51 -97
- package/docs/ai/components/stat-group.md +9 -19
- package/docs/ai/components/step-input.md +17 -15
- package/docs/ai/components/stepper.md +14 -15
- package/docs/ai/components/switch.md +15 -9
- package/docs/ai/components/table.md +24 -84
- package/docs/ai/components/tabs.md +18 -10
- package/docs/ai/components/tag-input.md +18 -29
- package/docs/ai/components/tag.md +10 -22
- package/docs/ai/components/terminal.md +9 -9
- package/docs/ai/components/testimonial.md +9 -19
- package/docs/ai/components/textarea.md +15 -16
- package/docs/ai/components/theme.md +3 -3
- package/docs/ai/components/time-picker.md +30 -49
- package/docs/ai/components/time-range-picker.md +16 -15
- package/docs/ai/components/timeline.md +5 -4
- package/docs/ai/components/timer.md +8 -8
- package/docs/ai/components/toast.md +24 -18
- package/docs/ai/components/tooltip.md +11 -22
- package/docs/ai/components/tree.md +9 -9
- package/docs/ai/components/treemap.md +14 -13
- package/docs/ai/components/user-card.md +21 -27
- package/docs/ai/components/video-player.md +35 -52
- package/docs/ai/components/virtual-scroller.md +1 -1
- package/docs/ai/components/waterfall.md +17 -16
- package/docs/ai/components/weather.md +19 -34
- package/docs/ai/components/work-order.md +58 -134
- package/docs/ai/patterns.md +87 -0
- package/docs/ai/react-integration.md +97 -0
- package/docs/components/accordion.md +72 -151
- package/docs/components/action-bar.md +185 -0
- package/docs/components/activity-feed.md +9 -14
- package/docs/components/alert.md +17 -109
- package/docs/components/app-tiles.md +58 -43
- package/docs/components/approval-flow.md +8 -14
- package/docs/components/audio-recorder.md +45 -51
- package/docs/components/availability.md +30 -45
- package/docs/components/avatar-group.md +34 -14
- package/docs/components/avatar.md +20 -55
- package/docs/components/badge.md +53 -470
- package/docs/components/banner.md +44 -30
- package/docs/components/binpack.md +208 -0
- package/docs/components/book.md +78 -57
- package/docs/components/booking.md +35 -87
- package/docs/components/breadcrumbs.md +74 -448
- package/docs/components/button.md +72 -603
- package/docs/components/calendar.md +77 -261
- package/docs/components/camera-annotate.md +44 -96
- package/docs/components/camera.md +94 -333
- package/docs/components/candlestick.md +79 -116
- package/docs/components/card.md +51 -689
- package/docs/components/carousel.md +29 -76
- package/docs/components/cart.md +44 -136
- package/docs/components/chart.md +95 -438
- package/docs/components/chat.md +175 -439
- package/docs/components/checkbox.md +52 -609
- package/docs/components/chip.md +45 -574
- package/docs/components/code-block.md +157 -421
- package/docs/components/color-display.md +45 -54
- package/docs/components/color-picker.md +103 -36
- package/docs/components/command-palette.md +98 -92
- package/docs/components/comments.md +16 -10
- package/docs/components/countdown.md +15 -20
- package/docs/components/cropper.md +14 -16
- package/docs/components/data-card.md +16 -15
- package/docs/components/date-picker.md +45 -25
- package/docs/components/date-range-picker.md +140 -87
- package/docs/components/date-time-picker.md +25 -28
- package/docs/components/diff.md +22 -17
- package/docs/components/divider.md +18 -20
- package/docs/components/doc.md +105 -197
- package/docs/components/draw.md +117 -223
- package/docs/components/drawer.md +113 -478
- package/docs/components/empty-state.md +13 -29
- package/docs/components/estimate.md +58 -118
- package/docs/components/file-gallery.md +123 -495
- package/docs/components/file-upload.md +36 -123
- package/docs/components/flip-card.md +30 -34
- package/docs/components/flow.md +74 -89
- package/docs/components/form-builder.md +59 -86
- package/docs/components/form-layout.md +21 -31
- package/docs/components/funnel.md +21 -22
- package/docs/components/gantt.md +5 -5
- package/docs/components/gauge.md +5 -23
- package/docs/components/grid.md +249 -0
- package/docs/components/heatmap.md +13 -55
- package/docs/components/image.md +28 -32
- package/docs/components/input.md +25 -14
- package/docs/components/invoice.md +34 -33
- package/docs/components/kanban.md +99 -394
- package/docs/components/key-value.md +22 -12
- package/docs/components/kpi.md +41 -112
- package/docs/components/layout.md +7 -13
- package/docs/components/leaderboard.md +52 -76
- package/docs/components/link-preview.md +20 -16
- package/docs/components/link.md +22 -19
- package/docs/components/list.md +44 -26
- package/docs/components/location.md +9 -13
- package/docs/components/login.md +42 -36
- package/docs/components/map.md +24 -46
- package/docs/components/markdown.md +14 -25
- package/docs/components/masonry.md +15 -13
- package/docs/components/mentions.md +36 -25
- package/docs/components/menu.md +39 -46
- package/docs/components/message-strip.md +15 -51
- package/docs/components/metric-table.md +50 -72
- package/docs/components/modal.md +32 -43
- package/docs/components/music-player.md +41 -49
- package/docs/components/nav.md +23 -13
- package/docs/components/network-graph.md +14 -13
- package/docs/components/notification-center.md +68 -172
- package/docs/components/order-tracker.md +72 -117
- package/docs/components/org-chart.md +58 -207
- package/docs/components/pagination.md +67 -89
- package/docs/components/paint.md +86 -172
- package/docs/components/pdf-viewer.md +46 -151
- package/docs/components/permission-matrix.md +61 -112
- package/docs/components/podcast-player.md +41 -119
- package/docs/components/pricing-table.md +104 -147
- package/docs/components/product-card.md +94 -197
- package/docs/components/progress-ring.md +29 -32
- package/docs/components/progress.md +30 -61
- package/docs/components/qr-code.md +53 -61
- package/docs/components/qr-reader.md +53 -42
- package/docs/components/radio.md +42 -40
- package/docs/components/range-slider.md +41 -30
- package/docs/components/rating.md +50 -84
- package/docs/components/receipt.md +91 -129
- package/docs/components/recipe.md +107 -216
- package/docs/components/sankey.md +47 -83
- package/docs/components/scheduler.md +81 -184
- package/docs/components/segmented-control.md +48 -40
- package/docs/components/select.md +107 -129
- package/docs/components/skeleton.md +33 -47
- package/docs/components/slider.md +49 -53
- package/docs/components/sortable.md +43 -186
- package/docs/components/sparkline.md +26 -25
- package/docs/components/spinner.md +32 -37
- package/docs/components/split-button.md +43 -23
- package/docs/components/split-pane.md +41 -58
- package/docs/components/spotlight.md +53 -145
- package/docs/components/spreadsheet.md +84 -307
- package/docs/components/stat-group.md +27 -61
- package/docs/components/step-input.md +44 -41
- package/docs/components/stepper.md +55 -89
- package/docs/components/switch.md +39 -39
- package/docs/components/table.md +27 -32
- package/docs/components/tabs.md +36 -27
- package/docs/components/tag-input.md +50 -176
- package/docs/components/tag.md +12 -50
- package/docs/components/terminal.md +32 -37
- package/docs/components/testimonial.md +24 -81
- package/docs/components/textarea.md +9 -14
- package/docs/components/theme.md +10 -23
- package/docs/components/time-picker.md +48 -72
- package/docs/components/time-range-picker.md +22 -41
- package/docs/components/timeline.md +7 -14
- package/docs/components/timer.md +16 -32
- package/docs/components/toast.md +19 -45
- package/docs/components/tooltip.md +13 -115
- package/docs/components/tree.md +2 -19
- package/docs/components/treemap.md +19 -43
- package/docs/components/user-card.md +13 -22
- package/docs/components/video-player.md +53 -227
- package/docs/components/virtual-scroller.md +11 -44
- package/docs/components/waterfall.md +58 -137
- package/docs/components/weather.md +94 -153
- package/docs/components/work-order.md +56 -143
- package/docs/plans/2026-03-09-action-bar-design.md +104 -0
- package/docs/plans/2026-03-09-action-bar-plan.md +676 -0
- package/docs/plans/2026-03-10-grid-component-design.md +138 -0
- package/docs/plans/2026-03-10-grid-component-plan.md +716 -0
- package/docs/plans/2026-03-10-react-integration-design.md +166 -0
- package/docs/plans/2026-03-10-react-integration-plan.md +1178 -0
- package/docs/react-integration.md +508 -0
- package/docs/request-response.md +63 -0
- package/package.json +1 -1
- package/bin/templates/base/README.md +0 -33
- package/bin/templates/base/global.d.ts +0 -14
- package/bin/templates/base/index.html +0 -13
- package/bin/templates/base/package.json +0 -21
- package/bin/templates/base/src/components/counter-button.ts +0 -88
- package/bin/templates/base/src/components/counter-button.types.ts +0 -7
- package/bin/templates/base/src/components/feature-card.ts +0 -59
- package/bin/templates/base/src/components/feature-card.types.ts +0 -5
- package/bin/templates/base/src/controllers/counter-controller.ts +0 -24
- package/bin/templates/base/src/main.ts +0 -24
- package/bin/templates/base/src/pages/about-page.ts +0 -68
- package/bin/templates/base/src/pages/home-page.ts +0 -105
- package/bin/templates/base/src/pages/not-found-page.ts +0 -55
- package/bin/templates/base/src/router.ts +0 -9
- package/bin/templates/base/src/styles/global.css +0 -27
- package/bin/templates/base/src/types/api-response.ts +0 -5
- package/bin/templates/base/src/types/status.ts +0 -1
- package/bin/templates/base/src/types/theme.ts +0 -1
- package/bin/templates/base/src/types/user.ts +0 -5
- package/bin/templates/base/vite.config.ts +0 -38
- package/bin/templates/pwa/public/vite.svg +0 -1
- package/bin/templates/pwa/src/router.ts +0 -20
- package/bin/templates/pwa/src/styles/global.css +0 -64
- /package/bin/templates/{pwa → default}/README.md +0 -0
- /package/bin/templates/{pwa → default}/global.d.ts +0 -0
- /package/bin/templates/{pwa → default}/package.json +0 -0
- /package/bin/templates/{pwa → default}/public/icons/.gitkeep +0 -0
- /package/bin/templates/{base → default}/public/vite.svg +0 -0
- /package/bin/templates/{pwa → default}/src/fetcher.ts +0 -0
- /package/bin/templates/{pwa → default}/src/guards/auth.ts +0 -0
- /package/bin/templates/{pwa → default}/src/middleware/auth.ts +0 -0
- /package/bin/templates/{pwa → default}/src/middleware/retry.ts +0 -0
- /package/bin/templates/{pwa → default}/src/services/auth.ts +0 -0
- /package/bin/templates/{pwa → default}/src/services/jwt.ts +0 -0
- /package/bin/templates/{pwa → default}/src/services/storage.ts +0 -0
- /package/bin/templates/{pwa → default}/src/types/auth.ts +0 -0
- /package/bin/templates/{pwa → default}/src/types/notifications.ts +0 -0
- /package/bin/templates/{pwa → default}/tests/helpers/test-utils.ts +0 -0
- /package/bin/templates/{pwa → default}/tests/middleware/auth.test.ts +0 -0
- /package/bin/templates/{pwa → default}/tests/middleware/error.test.ts +0 -0
- /package/bin/templates/{pwa → default}/tests/middleware/retry.test.ts +0 -0
- /package/bin/templates/{pwa → default}/tests/services/auth.test.ts +0 -0
- /package/bin/templates/{pwa → default}/tests/services/jwt.test.ts +0 -0
- /package/bin/templates/{pwa → default}/tests/services/storage.test.ts +0 -0
- /package/bin/templates/{pwa → default}/vite.config.ts +0 -0
- /package/bin/templates/{pwa → default}/vitest.config.ts +0 -0
|
@@ -0,0 +1,1178 @@
|
|
|
1
|
+
# Snice React Integration (Full Parity) — Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Let React developers build full Snice-powered apps using only hooks and JSX — routing, guards, layouts, context — without decorators or custom element classes.
|
|
6
|
+
|
|
7
|
+
**Architecture:** React-native implementations of Snice's router, context, guards, and layouts in `src/react/`. Purpose-built for React's rendering model while mirroring Snice's API contracts. Source builds to `dist/react/`, copies to `adapters/react/`. Consumers import from `snice/react`.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** React 18+, TypeScript, Rollup, Vitest, pica-route (same route matching as vanilla Snice)
|
|
10
|
+
|
|
11
|
+
**Design doc:** `docs/plans/2026-03-10-react-integration-design.md`
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Prerequisite: Async Guard Support in Vanilla Snice
|
|
16
|
+
|
|
17
|
+
Before building the React router, the vanilla `Guard` type and `checkGuards` must support `Promise<boolean>`. This ensures shared guards work in both systems.
|
|
18
|
+
|
|
19
|
+
### Task 1: Update Guard Type to Support Async
|
|
20
|
+
|
|
21
|
+
**Files:**
|
|
22
|
+
- Modify: `src/types/guard.ts`
|
|
23
|
+
|
|
24
|
+
**Step 1: Update the Guard type**
|
|
25
|
+
|
|
26
|
+
Change `src/types/guard.ts` to:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { RouteParams } from './route-params';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Guard function type for route protection and visibility control.
|
|
33
|
+
* Guards determine if navigation to a route should proceed.
|
|
34
|
+
* Can return a boolean synchronously or a Promise<boolean> for async checks.
|
|
35
|
+
*
|
|
36
|
+
* @template T - The context type passed to the guard function
|
|
37
|
+
* @param context - The application context object
|
|
38
|
+
* @param params - Route parameters extracted from the URL
|
|
39
|
+
* @returns boolean or Promise<boolean> - true to allow, false to deny
|
|
40
|
+
*/
|
|
41
|
+
export type Guard<T = any> = (context: T, params: RouteParams) => boolean | Promise<boolean>;
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Step 2: Commit**
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
git add src/types/guard.ts
|
|
48
|
+
git commit -m "feat: support async guards in Guard type"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Task 2: Make Vanilla Router's checkGuards Async
|
|
52
|
+
|
|
53
|
+
**Files:**
|
|
54
|
+
- Modify: `src/router.ts`
|
|
55
|
+
|
|
56
|
+
**Step 1: Make checkGuards async**
|
|
57
|
+
|
|
58
|
+
In `src/router.ts`, change `checkGuards` (currently at ~line 242):
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
async function checkGuards(guards: Guard<any> | Guard<any>[] | undefined, params: RouteParams, target: Element): Promise<boolean> {
|
|
62
|
+
const hasGuards = !!guards;
|
|
63
|
+
if (!hasGuards) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const guardsArray = Array.isArray(guards) ? guards : [guards];
|
|
68
|
+
for (const guard of guardsArray) {
|
|
69
|
+
const allowed = await guard(context, params);
|
|
70
|
+
if (!allowed) {
|
|
71
|
+
renderForbiddenPage(target);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Step 2: Add `await` to all checkGuards call sites**
|
|
80
|
+
|
|
81
|
+
The function is already called in async contexts (`navigate` is async), but the calls need `await`:
|
|
82
|
+
|
|
83
|
+
- Line ~294 in `resolveRoute`: Change to make resolveRoute async. Currently `const guardsAllowed = checkGuards(...)` — needs `await`.
|
|
84
|
+
- Line ~435 in `navigate` (home path): `if (!checkGuards(...))` → `if (!(await checkGuards(...)))`
|
|
85
|
+
|
|
86
|
+
Change `resolveRoute` signature to async:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
async function resolveRoute(path: string, target: Element): Promise<{ result: RouteResult; element?: HTMLElement; transition?: Transition; layout?: string | false; routeParams?: RouteParams }> {
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
And in its body:
|
|
93
|
+
```typescript
|
|
94
|
+
const guardsAllowed = await checkGuards(route.guards, params as RouteParams, target);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
In `navigate`, the home path guard check:
|
|
98
|
+
```typescript
|
|
99
|
+
if (!(await checkGuards(homeRoute?.guards, {}, target))) return;
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
And the `resolveRoute` call already has `const routeResult = resolveRoute(...)` — just needs `await`:
|
|
103
|
+
```typescript
|
|
104
|
+
const routeResult = await resolveRoute(path, target);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Step 3: Run existing router tests**
|
|
108
|
+
|
|
109
|
+
Run: `npm run build:test && npx vitest run tests/router.test.ts`
|
|
110
|
+
Expected: All existing tests still pass (sync guards still work since `await true === true`)
|
|
111
|
+
|
|
112
|
+
**Step 4: Commit**
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
git add src/router.ts
|
|
116
|
+
git commit -m "feat: make router checkGuards async for async guard support"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Task 3: Add Async Guard Tests for Vanilla Router
|
|
120
|
+
|
|
121
|
+
**Files:**
|
|
122
|
+
- Modify: `tests/router.test.ts`
|
|
123
|
+
|
|
124
|
+
**Step 1: Add async guard tests**
|
|
125
|
+
|
|
126
|
+
Add these tests to the existing router test file, in the guards describe block:
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
it('should support async guards that resolve to true', async () => {
|
|
130
|
+
const elName = uniqueName('async-guard-pass');
|
|
131
|
+
const asyncGuard: Guard<any> = async (ctx, params) => {
|
|
132
|
+
await new Promise(r => setTimeout(r, 10));
|
|
133
|
+
return true;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
@page({ tag: elName, routes: ['/async-pass'], guards: [asyncGuard] })
|
|
137
|
+
class AsyncPassPage extends HTMLElement {
|
|
138
|
+
@render()
|
|
139
|
+
renderContent() {
|
|
140
|
+
return html`<div>async pass</div>`;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await router.navigate('/async-pass');
|
|
145
|
+
const el = target.querySelector(elName);
|
|
146
|
+
expect(el).toBeTruthy();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should support async guards that resolve to false', async () => {
|
|
150
|
+
const elName = uniqueName('async-guard-fail');
|
|
151
|
+
const asyncGuard: Guard<any> = async (ctx, params) => {
|
|
152
|
+
await new Promise(r => setTimeout(r, 10));
|
|
153
|
+
return false;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
@page({ tag: elName, routes: ['/async-fail'], guards: [asyncGuard] })
|
|
157
|
+
class AsyncFailPage extends HTMLElement {
|
|
158
|
+
@render()
|
|
159
|
+
renderContent() {
|
|
160
|
+
return html`<div>async fail</div>`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await router.navigate('/async-fail');
|
|
165
|
+
const el = target.querySelector(elName);
|
|
166
|
+
expect(el).toBeFalsy();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should support mixed sync and async guards', async () => {
|
|
170
|
+
const elName = uniqueName('mixed-guard');
|
|
171
|
+
const syncGuard: Guard<any> = (ctx, params) => true;
|
|
172
|
+
const asyncGuard: Guard<any> = async (ctx, params) => {
|
|
173
|
+
await new Promise(r => setTimeout(r, 10));
|
|
174
|
+
return true;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
@page({ tag: elName, routes: ['/mixed-guard'], guards: [syncGuard, asyncGuard] })
|
|
178
|
+
class MixedGuardPage extends HTMLElement {
|
|
179
|
+
@render()
|
|
180
|
+
renderContent() {
|
|
181
|
+
return html`<div>mixed guard</div>`;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await router.navigate('/mixed-guard');
|
|
186
|
+
const el = target.querySelector(elName);
|
|
187
|
+
expect(el).toBeTruthy();
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Step 2: Run tests**
|
|
192
|
+
|
|
193
|
+
Run: `npm run build:test && npx vitest run tests/router.test.ts`
|
|
194
|
+
Expected: All tests pass including new async guard tests
|
|
195
|
+
|
|
196
|
+
**Step 3: Commit**
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
git add tests/router.test.ts
|
|
200
|
+
git commit -m "test: add async guard tests for vanilla router"
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## React Integration
|
|
206
|
+
|
|
207
|
+
### Task 4: SniceProvider and useSniceContext
|
|
208
|
+
|
|
209
|
+
The context provider is the foundation — other components depend on it. It provides the merged context shape that mirrors Snice's `@context` decorator.
|
|
210
|
+
|
|
211
|
+
**Files:**
|
|
212
|
+
- Create: `src/react/SniceProvider.tsx`
|
|
213
|
+
- Create: `tests/react/snice-provider.test.ts`
|
|
214
|
+
|
|
215
|
+
**Step 1: Write the test file**
|
|
216
|
+
|
|
217
|
+
Create `tests/react/snice-provider.test.ts`:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { describe, it, expect } from 'vitest';
|
|
221
|
+
import { SniceProvider, useSniceContext, useNavigate, useParams, useRoute } from '../../src/react/SniceProvider';
|
|
222
|
+
|
|
223
|
+
describe('SniceProvider exports', () => {
|
|
224
|
+
it('should export SniceProvider component', () => {
|
|
225
|
+
expect(typeof SniceProvider).toBe('function');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should export useSniceContext hook', () => {
|
|
229
|
+
expect(typeof useSniceContext).toBe('function');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should export useNavigate hook', () => {
|
|
233
|
+
expect(typeof useNavigate).toBe('function');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should export useParams hook', () => {
|
|
237
|
+
expect(typeof useParams).toBe('function');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should export useRoute hook', () => {
|
|
241
|
+
expect(typeof useRoute).toBe('function');
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Step 2: Run test to verify it fails**
|
|
247
|
+
|
|
248
|
+
Run: `npx vitest run tests/react/snice-provider.test.ts`
|
|
249
|
+
Expected: FAIL — module not found
|
|
250
|
+
|
|
251
|
+
**Step 3: Write the SniceProvider**
|
|
252
|
+
|
|
253
|
+
Create `src/react/SniceProvider.tsx`:
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
import { createContext, useContext, useMemo, type ReactNode } from 'react';
|
|
257
|
+
import type { Placard } from '../types/placard';
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Mirrors Snice's @context shape exactly.
|
|
261
|
+
*/
|
|
262
|
+
export interface SniceReactContext {
|
|
263
|
+
/** User-defined app state */
|
|
264
|
+
application: Record<string, any>;
|
|
265
|
+
/** Navigation state */
|
|
266
|
+
navigation: {
|
|
267
|
+
route: string;
|
|
268
|
+
params: Record<string, string>;
|
|
269
|
+
placards: Placard[];
|
|
270
|
+
};
|
|
271
|
+
/** Programmatic navigation */
|
|
272
|
+
navigate: (path: string) => void;
|
|
273
|
+
/** Optional context-aware fetcher */
|
|
274
|
+
fetch?: typeof globalThis.fetch;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const SniceCtx = createContext<SniceReactContext | null>(null);
|
|
278
|
+
|
|
279
|
+
export interface SniceProviderProps {
|
|
280
|
+
/** Application context object (user state, theme, config, etc.) */
|
|
281
|
+
context?: Record<string, any>;
|
|
282
|
+
/** Navigation function — provided by SniceRouter, or your own */
|
|
283
|
+
navigate?: (path: string) => void;
|
|
284
|
+
/** Current route pattern */
|
|
285
|
+
route?: string;
|
|
286
|
+
/** Current route params */
|
|
287
|
+
params?: Record<string, string>;
|
|
288
|
+
/** Registered placards */
|
|
289
|
+
placards?: Placard[];
|
|
290
|
+
/** Optional fetch function */
|
|
291
|
+
fetch?: typeof globalThis.fetch;
|
|
292
|
+
children: ReactNode;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Context provider for Snice React integration.
|
|
297
|
+
* Usable standalone without SniceRouter for simpler apps.
|
|
298
|
+
*/
|
|
299
|
+
export function SniceProvider({
|
|
300
|
+
context = {},
|
|
301
|
+
navigate = () => {},
|
|
302
|
+
route = '',
|
|
303
|
+
params = {},
|
|
304
|
+
placards = [],
|
|
305
|
+
fetch: fetchFn,
|
|
306
|
+
children,
|
|
307
|
+
}: SniceProviderProps) {
|
|
308
|
+
const value = useMemo<SniceReactContext>(
|
|
309
|
+
() => ({
|
|
310
|
+
application: context,
|
|
311
|
+
navigation: { route, params, placards },
|
|
312
|
+
navigate,
|
|
313
|
+
...(fetchFn ? { fetch: fetchFn } : {}),
|
|
314
|
+
}),
|
|
315
|
+
[context, navigate, route, params, placards, fetchFn],
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
return <SniceCtx.Provider value={value}>{children}</SniceCtx.Provider>;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Returns the full merged Snice context.
|
|
323
|
+
* Mirrors the shape returned by Snice's @context decorator.
|
|
324
|
+
*/
|
|
325
|
+
export function useSniceContext(): SniceReactContext {
|
|
326
|
+
const ctx = useContext(SniceCtx);
|
|
327
|
+
if (!ctx) {
|
|
328
|
+
throw new Error('useSniceContext must be used within a <SniceProvider> or <SniceRouter>');
|
|
329
|
+
}
|
|
330
|
+
return ctx;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Convenience: returns just the navigate function */
|
|
334
|
+
export function useNavigate(): (path: string) => void {
|
|
335
|
+
return useSniceContext().navigate;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Convenience: returns current route params */
|
|
339
|
+
export function useParams(): Record<string, string> {
|
|
340
|
+
return useSniceContext().navigation.params;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Convenience: returns current matched route pattern */
|
|
344
|
+
export function useRoute(): string {
|
|
345
|
+
return useSniceContext().navigation.route;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Export the raw context for SniceRouter to use as its provider
|
|
349
|
+
export { SniceCtx as SniceContextInternal };
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**Step 4: Run test to verify it passes**
|
|
353
|
+
|
|
354
|
+
Run: `npx vitest run tests/react/snice-provider.test.ts`
|
|
355
|
+
Expected: PASS
|
|
356
|
+
|
|
357
|
+
**Step 5: Commit**
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
git add src/react/SniceProvider.tsx tests/react/snice-provider.test.ts
|
|
361
|
+
git commit -m "feat: add SniceProvider and context hooks"
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Task 5: Route Matching Utility
|
|
365
|
+
|
|
366
|
+
Extract route matching into a shared utility so the React router uses the exact same `pica-route` matching as vanilla Snice.
|
|
367
|
+
|
|
368
|
+
**Files:**
|
|
369
|
+
- Create: `src/react/matchRoute.ts`
|
|
370
|
+
- Create: `tests/react/match-route.test.ts`
|
|
371
|
+
|
|
372
|
+
**Step 1: Write the test**
|
|
373
|
+
|
|
374
|
+
Create `tests/react/match-route.test.ts`:
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
import { describe, it, expect } from 'vitest';
|
|
378
|
+
import { matchRoutes, type RouteConfig } from '../../src/react/matchRoute';
|
|
379
|
+
|
|
380
|
+
describe('matchRoutes', () => {
|
|
381
|
+
const routes: RouteConfig[] = [
|
|
382
|
+
{ path: '/users/:id', index: 0 },
|
|
383
|
+
{ path: '/users', index: 1 },
|
|
384
|
+
{ path: '/', index: 2 },
|
|
385
|
+
];
|
|
386
|
+
|
|
387
|
+
it('should match exact paths', () => {
|
|
388
|
+
const result = matchRoutes(routes, '/users');
|
|
389
|
+
expect(result).not.toBeNull();
|
|
390
|
+
expect(result!.index).toBe(1);
|
|
391
|
+
expect(result!.params).toEqual({});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should match parameterized paths', () => {
|
|
395
|
+
const result = matchRoutes(routes, '/users/42');
|
|
396
|
+
expect(result).not.toBeNull();
|
|
397
|
+
expect(result!.index).toBe(0);
|
|
398
|
+
expect(result!.params).toEqual({ id: '42' });
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should match root path', () => {
|
|
402
|
+
const result = matchRoutes(routes, '/');
|
|
403
|
+
expect(result).not.toBeNull();
|
|
404
|
+
expect(result!.index).toBe(2);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('should return null for unmatched paths', () => {
|
|
408
|
+
const result = matchRoutes(routes, '/nonexistent');
|
|
409
|
+
expect(result).toBeNull();
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('should match longest route first (most specific)', () => {
|
|
413
|
+
const result = matchRoutes(routes, '/users/42');
|
|
414
|
+
expect(result!.index).toBe(0); // /users/:id wins over /users
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**Step 2: Run test to verify it fails**
|
|
420
|
+
|
|
421
|
+
Run: `npx vitest run tests/react/match-route.test.ts`
|
|
422
|
+
Expected: FAIL — module not found
|
|
423
|
+
|
|
424
|
+
**Step 3: Write matchRoute.ts**
|
|
425
|
+
|
|
426
|
+
Create `src/react/matchRoute.ts`:
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
import { Route, type RouteParams } from 'pica-route';
|
|
430
|
+
|
|
431
|
+
export interface RouteConfig {
|
|
432
|
+
path: string;
|
|
433
|
+
index: number;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export interface MatchResult {
|
|
437
|
+
index: number;
|
|
438
|
+
params: RouteParams;
|
|
439
|
+
path: string;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Match a URL path against an array of route configs.
|
|
444
|
+
* Uses pica-route — same matching as vanilla Snice's Router.
|
|
445
|
+
* Routes are sorted by specificity (longest spec first).
|
|
446
|
+
*/
|
|
447
|
+
export function matchRoutes(routes: RouteConfig[], pathname: string): MatchResult | null {
|
|
448
|
+
// Sort by specificity (longest path first), same as vanilla Router
|
|
449
|
+
const sorted = [...routes].sort((a, b) => b.path.length - a.path.length);
|
|
450
|
+
|
|
451
|
+
for (const route of sorted) {
|
|
452
|
+
const matcher = new Route(route.path);
|
|
453
|
+
const params = matcher.match(pathname);
|
|
454
|
+
if (params !== false) {
|
|
455
|
+
return {
|
|
456
|
+
index: route.index,
|
|
457
|
+
params: params as RouteParams,
|
|
458
|
+
path: route.path,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
**Step 4: Run test to verify it passes**
|
|
468
|
+
|
|
469
|
+
Run: `npx vitest run tests/react/match-route.test.ts`
|
|
470
|
+
Expected: PASS
|
|
471
|
+
|
|
472
|
+
**Step 5: Commit**
|
|
473
|
+
|
|
474
|
+
```bash
|
|
475
|
+
git add src/react/matchRoute.ts tests/react/match-route.test.ts
|
|
476
|
+
git commit -m "feat: add route matching utility for React router"
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Task 6: SniceRouter and Route Components
|
|
480
|
+
|
|
481
|
+
The core router: URL state management, route matching, guard execution, layout selection, context propagation.
|
|
482
|
+
|
|
483
|
+
**Files:**
|
|
484
|
+
- Create: `src/react/SniceRouter.tsx`
|
|
485
|
+
- Create: `tests/react/snice-router.test.ts`
|
|
486
|
+
|
|
487
|
+
**Step 1: Write the test**
|
|
488
|
+
|
|
489
|
+
Create `tests/react/snice-router.test.ts`:
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
import { describe, it, expect } from 'vitest';
|
|
493
|
+
import { SniceRouter, Route } from '../../src/react/SniceRouter';
|
|
494
|
+
|
|
495
|
+
describe('SniceRouter exports', () => {
|
|
496
|
+
it('should export SniceRouter component', () => {
|
|
497
|
+
expect(typeof SniceRouter).toBe('function');
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('should export Route component', () => {
|
|
501
|
+
expect(typeof Route).toBe('function');
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
**Step 2: Run test to verify it fails**
|
|
507
|
+
|
|
508
|
+
Run: `npx vitest run tests/react/snice-router.test.ts`
|
|
509
|
+
Expected: FAIL — module not found
|
|
510
|
+
|
|
511
|
+
**Step 3: Write SniceRouter.tsx**
|
|
512
|
+
|
|
513
|
+
Create `src/react/SniceRouter.tsx`:
|
|
514
|
+
|
|
515
|
+
```tsx
|
|
516
|
+
import {
|
|
517
|
+
useState,
|
|
518
|
+
useEffect,
|
|
519
|
+
useCallback,
|
|
520
|
+
useMemo,
|
|
521
|
+
useRef,
|
|
522
|
+
type ReactNode,
|
|
523
|
+
type ReactElement,
|
|
524
|
+
type ComponentType,
|
|
525
|
+
Children,
|
|
526
|
+
isValidElement,
|
|
527
|
+
createElement,
|
|
528
|
+
} from 'react';
|
|
529
|
+
import { SniceProvider, type SniceReactContext } from './SniceProvider';
|
|
530
|
+
import { matchRoutes, type RouteConfig } from './matchRoute';
|
|
531
|
+
import type { Placard } from '../types/placard';
|
|
532
|
+
|
|
533
|
+
// ─── Route component (declarative config) ───
|
|
534
|
+
|
|
535
|
+
export interface RouteProps {
|
|
536
|
+
path: string;
|
|
537
|
+
/** React component OR Snice element tag name (string) */
|
|
538
|
+
page: ComponentType<any> | string;
|
|
539
|
+
/** Single guard function */
|
|
540
|
+
guard?: (context: Record<string, any>, params: Record<string, string>) => boolean | Promise<boolean>;
|
|
541
|
+
/** Multiple guards (AND logic) */
|
|
542
|
+
guards?: Array<(context: Record<string, any>, params: Record<string, string>) => boolean | Promise<boolean>>;
|
|
543
|
+
/** Redirect path if guard rejects */
|
|
544
|
+
guardRedirect?: string;
|
|
545
|
+
/** Layout override. Component, string (Snice tag), or false (no layout) */
|
|
546
|
+
layout?: ComponentType<{ children: ReactNode }> | string | false;
|
|
547
|
+
/** Page metadata for layouts */
|
|
548
|
+
placard?: Placard;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Route definition component. Child of <SniceRouter>.
|
|
553
|
+
* Does not render anything — SniceRouter reads its props.
|
|
554
|
+
*/
|
|
555
|
+
export function Route(_props: RouteProps): ReactElement | null {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ─── SniceRouter ───
|
|
560
|
+
|
|
561
|
+
export interface SniceRouterProps {
|
|
562
|
+
/** URL strategy */
|
|
563
|
+
mode: 'hash' | 'history';
|
|
564
|
+
/** Application context passed to guards, pages, layouts */
|
|
565
|
+
context?: Record<string, any>;
|
|
566
|
+
/** Default layout component or Snice tag name */
|
|
567
|
+
layout?: ComponentType<{ children: ReactNode }> | string;
|
|
568
|
+
/** Loading component shown during async guards. Component, string (Snice tag), or JSX. */
|
|
569
|
+
loading?: ComponentType | string | ReactNode;
|
|
570
|
+
/** Fallback when no route matches. Component, string (Snice tag), or JSX. */
|
|
571
|
+
fallback?: ComponentType | string | ReactNode;
|
|
572
|
+
children: ReactNode;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
interface ParsedRoute {
|
|
576
|
+
path: string;
|
|
577
|
+
page: ComponentType<any> | string;
|
|
578
|
+
guards: Array<(ctx: Record<string, any>, params: Record<string, string>) => boolean | Promise<boolean>>;
|
|
579
|
+
guardRedirect?: string;
|
|
580
|
+
layout?: ComponentType<{ children: ReactNode }> | string | false;
|
|
581
|
+
placard?: Placard;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function getPath(mode: 'hash' | 'history'): string {
|
|
585
|
+
if (mode === 'hash') {
|
|
586
|
+
return window.location.hash.slice(1) || '/';
|
|
587
|
+
}
|
|
588
|
+
return window.location.pathname;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function DefaultLoading() {
|
|
592
|
+
return createElement('div', {
|
|
593
|
+
style: {
|
|
594
|
+
display: 'flex',
|
|
595
|
+
alignItems: 'center',
|
|
596
|
+
justifyContent: 'center',
|
|
597
|
+
height: '100%',
|
|
598
|
+
minHeight: '200px',
|
|
599
|
+
},
|
|
600
|
+
}, createElement('div', {
|
|
601
|
+
style: {
|
|
602
|
+
width: '32px',
|
|
603
|
+
height: '32px',
|
|
604
|
+
border: '3px solid rgba(128,128,128,0.3)',
|
|
605
|
+
borderTopColor: 'rgba(128,128,128,0.8)',
|
|
606
|
+
borderRadius: '50%',
|
|
607
|
+
animation: 'snice-spin 0.6s linear infinite',
|
|
608
|
+
},
|
|
609
|
+
}));
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** Render a page/layout/loading/fallback prop that can be component, string tag, or JSX */
|
|
613
|
+
function renderFlexible(
|
|
614
|
+
value: ComponentType<any> | string | ReactNode | undefined,
|
|
615
|
+
props?: Record<string, any>,
|
|
616
|
+
): ReactNode {
|
|
617
|
+
if (!value) return null;
|
|
618
|
+
|
|
619
|
+
// String = Snice web component tag name
|
|
620
|
+
if (typeof value === 'string') {
|
|
621
|
+
return createElement(value, props);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Function = React component
|
|
625
|
+
if (typeof value === 'function') {
|
|
626
|
+
return createElement(value as ComponentType<any>, props);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Already JSX/ReactNode
|
|
630
|
+
return value;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Root provider component. Manages URL state, route matching,
|
|
635
|
+
* guard execution, layout selection, and context propagation.
|
|
636
|
+
*/
|
|
637
|
+
export function SniceRouter({
|
|
638
|
+
mode,
|
|
639
|
+
context = {},
|
|
640
|
+
layout: defaultLayout,
|
|
641
|
+
loading,
|
|
642
|
+
fallback,
|
|
643
|
+
children,
|
|
644
|
+
}: SniceRouterProps) {
|
|
645
|
+
const [currentPath, setCurrentPath] = useState(() => getPath(mode));
|
|
646
|
+
const [guardState, setGuardState] = useState<'idle' | 'checking' | 'passed' | 'failed'>('idle');
|
|
647
|
+
const contextRef = useRef(context);
|
|
648
|
+
contextRef.current = context;
|
|
649
|
+
|
|
650
|
+
// Parse Route children into config
|
|
651
|
+
const parsedRoutes = useMemo<ParsedRoute[]>(() => {
|
|
652
|
+
const result: ParsedRoute[] = [];
|
|
653
|
+
Children.forEach(children, (child) => {
|
|
654
|
+
if (!isValidElement(child) || child.type !== Route) return;
|
|
655
|
+
const props = child.props as RouteProps;
|
|
656
|
+
const guards: ParsedRoute['guards'] = [];
|
|
657
|
+
if (props.guard) guards.push(props.guard);
|
|
658
|
+
if (props.guards) guards.push(...props.guards);
|
|
659
|
+
result.push({
|
|
660
|
+
path: props.path,
|
|
661
|
+
page: props.page,
|
|
662
|
+
guards,
|
|
663
|
+
guardRedirect: props.guardRedirect,
|
|
664
|
+
layout: props.layout,
|
|
665
|
+
placard: props.placard,
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
return result;
|
|
669
|
+
}, [children]);
|
|
670
|
+
|
|
671
|
+
// Collect placards
|
|
672
|
+
const placards = useMemo<Placard[]>(() => {
|
|
673
|
+
return parsedRoutes
|
|
674
|
+
.filter((r) => r.placard)
|
|
675
|
+
.map((r) => r.placard!);
|
|
676
|
+
}, [parsedRoutes]);
|
|
677
|
+
|
|
678
|
+
// Build route configs for matching
|
|
679
|
+
const routeConfigs = useMemo<RouteConfig[]>(
|
|
680
|
+
() => parsedRoutes.map((r, i) => ({ path: r.path, index: i })),
|
|
681
|
+
[parsedRoutes],
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
// Navigate function
|
|
685
|
+
const navigate = useCallback(
|
|
686
|
+
(path: string) => {
|
|
687
|
+
if (mode === 'hash') {
|
|
688
|
+
window.location.hash = path;
|
|
689
|
+
} else {
|
|
690
|
+
window.history.pushState(null, '', path);
|
|
691
|
+
setCurrentPath(path);
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
[mode],
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
// Listen for URL changes
|
|
698
|
+
useEffect(() => {
|
|
699
|
+
const handler = () => setCurrentPath(getPath(mode));
|
|
700
|
+
const event = mode === 'hash' ? 'hashchange' : 'popstate';
|
|
701
|
+
window.addEventListener(event, handler);
|
|
702
|
+
return () => window.removeEventListener(event, handler);
|
|
703
|
+
}, [mode]);
|
|
704
|
+
|
|
705
|
+
// Match current path
|
|
706
|
+
const match = useMemo(
|
|
707
|
+
() => matchRoutes(routeConfigs, currentPath),
|
|
708
|
+
[routeConfigs, currentPath],
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
const matchedRoute = match ? parsedRoutes[match.index] : null;
|
|
712
|
+
const params = match?.params ?? {};
|
|
713
|
+
|
|
714
|
+
// Run guards
|
|
715
|
+
useEffect(() => {
|
|
716
|
+
if (!matchedRoute) {
|
|
717
|
+
setGuardState('idle');
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (matchedRoute.guards.length === 0) {
|
|
722
|
+
setGuardState('passed');
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
let cancelled = false;
|
|
727
|
+
setGuardState('checking');
|
|
728
|
+
|
|
729
|
+
(async () => {
|
|
730
|
+
try {
|
|
731
|
+
for (const guard of matchedRoute.guards) {
|
|
732
|
+
const result = await guard(contextRef.current, params);
|
|
733
|
+
if (cancelled) return;
|
|
734
|
+
if (!result) {
|
|
735
|
+
setGuardState('failed');
|
|
736
|
+
if (matchedRoute.guardRedirect) {
|
|
737
|
+
navigate(matchedRoute.guardRedirect);
|
|
738
|
+
}
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (!cancelled) setGuardState('passed');
|
|
743
|
+
} catch {
|
|
744
|
+
if (!cancelled) {
|
|
745
|
+
setGuardState('failed');
|
|
746
|
+
if (matchedRoute.guardRedirect) {
|
|
747
|
+
navigate(matchedRoute.guardRedirect);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
})();
|
|
752
|
+
|
|
753
|
+
return () => { cancelled = true; };
|
|
754
|
+
}, [matchedRoute, currentPath]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
755
|
+
|
|
756
|
+
// Determine what to render
|
|
757
|
+
let content: ReactNode;
|
|
758
|
+
|
|
759
|
+
if (!matchedRoute) {
|
|
760
|
+
// No route match → fallback
|
|
761
|
+
content = renderFlexible(fallback) ?? createElement('div', null, '404 — Page not found');
|
|
762
|
+
} else if (guardState === 'checking') {
|
|
763
|
+
// Async guards running → show loading
|
|
764
|
+
content = renderFlexible(loading) ?? createElement(DefaultLoading);
|
|
765
|
+
} else if (guardState === 'failed') {
|
|
766
|
+
// Guard rejected (redirect may have fired) — render nothing
|
|
767
|
+
content = null;
|
|
768
|
+
} else {
|
|
769
|
+
// Guard passed or no guards → render page
|
|
770
|
+
content = renderFlexible(matchedRoute.page, params);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Apply layout
|
|
774
|
+
const layoutToUse = matchedRoute?.layout !== undefined ? matchedRoute.layout : defaultLayout;
|
|
775
|
+
if (layoutToUse && layoutToUse !== false && content !== null) {
|
|
776
|
+
content = renderFlexible(
|
|
777
|
+
layoutToUse as ComponentType<{ children: ReactNode }> | string,
|
|
778
|
+
{ children: content },
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return (
|
|
783
|
+
<SniceProvider
|
|
784
|
+
context={context}
|
|
785
|
+
navigate={navigate}
|
|
786
|
+
route={match?.path ?? ''}
|
|
787
|
+
params={params}
|
|
788
|
+
placards={placards}
|
|
789
|
+
>
|
|
790
|
+
{content}
|
|
791
|
+
</SniceProvider>
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
**Step 4: Run test to verify it passes**
|
|
797
|
+
|
|
798
|
+
Run: `npx vitest run tests/react/snice-router.test.ts`
|
|
799
|
+
Expected: PASS
|
|
800
|
+
|
|
801
|
+
**Step 5: Commit**
|
|
802
|
+
|
|
803
|
+
```bash
|
|
804
|
+
git add src/react/SniceRouter.tsx tests/react/snice-router.test.ts
|
|
805
|
+
git commit -m "feat: add SniceRouter and Route components"
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
### Task 7: React Barrel Export
|
|
809
|
+
|
|
810
|
+
Create the barrel `src/react/index.ts` that re-exports everything.
|
|
811
|
+
|
|
812
|
+
**Files:**
|
|
813
|
+
- Create: `src/react/index.ts`
|
|
814
|
+
|
|
815
|
+
**Step 1: Create the barrel file**
|
|
816
|
+
|
|
817
|
+
Create `src/react/index.ts`:
|
|
818
|
+
|
|
819
|
+
```typescript
|
|
820
|
+
// Context
|
|
821
|
+
export {
|
|
822
|
+
SniceProvider,
|
|
823
|
+
useSniceContext,
|
|
824
|
+
useNavigate,
|
|
825
|
+
useParams,
|
|
826
|
+
useRoute,
|
|
827
|
+
type SniceReactContext,
|
|
828
|
+
type SniceProviderProps,
|
|
829
|
+
} from './SniceProvider';
|
|
830
|
+
|
|
831
|
+
// Router
|
|
832
|
+
export {
|
|
833
|
+
SniceRouter,
|
|
834
|
+
Route,
|
|
835
|
+
type SniceRouterProps,
|
|
836
|
+
type RouteProps,
|
|
837
|
+
} from './SniceRouter';
|
|
838
|
+
|
|
839
|
+
// Request handler
|
|
840
|
+
export {
|
|
841
|
+
useRequestHandler,
|
|
842
|
+
type UseRequestRoute,
|
|
843
|
+
type UseRequestRouteMap,
|
|
844
|
+
type UseRequestHandlerOptions,
|
|
845
|
+
} from './useRequestHandler';
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
**Step 2: Commit**
|
|
849
|
+
|
|
850
|
+
```bash
|
|
851
|
+
git add src/react/index.ts
|
|
852
|
+
git commit -m "feat: add React barrel export"
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
### Task 8: Rollup Build Configuration
|
|
856
|
+
|
|
857
|
+
Add rollup entries so all React modules build to `dist/react/` and copy to `adapters/react/`.
|
|
858
|
+
|
|
859
|
+
**Files:**
|
|
860
|
+
- Modify: `rollup.config.js`
|
|
861
|
+
|
|
862
|
+
**Step 1: Update rollup config**
|
|
863
|
+
|
|
864
|
+
Replace the existing single-file React hooks build entry with a multi-file build:
|
|
865
|
+
|
|
866
|
+
```javascript
|
|
867
|
+
// React integration (source in src/react/, built to dist/react/, copied to adapters/react/)
|
|
868
|
+
{
|
|
869
|
+
input: {
|
|
870
|
+
'index': 'src/react/index.ts',
|
|
871
|
+
'SniceProvider': 'src/react/SniceProvider.tsx',
|
|
872
|
+
'SniceRouter': 'src/react/SniceRouter.tsx',
|
|
873
|
+
'matchRoute': 'src/react/matchRoute.ts',
|
|
874
|
+
'useRequestHandler': 'src/react/useRequestHandler.ts',
|
|
875
|
+
},
|
|
876
|
+
external: ['react', 'pica-route'],
|
|
877
|
+
output: {
|
|
878
|
+
dir: 'dist/react',
|
|
879
|
+
format: 'es',
|
|
880
|
+
banner,
|
|
881
|
+
sourcemap: true,
|
|
882
|
+
entryFileNames: '[name].js',
|
|
883
|
+
},
|
|
884
|
+
plugins: [
|
|
885
|
+
resolve(),
|
|
886
|
+
typescript({
|
|
887
|
+
tsconfig: './tsconfig.src.json',
|
|
888
|
+
declaration: true,
|
|
889
|
+
declarationDir: './dist/react',
|
|
890
|
+
rootDir: './src/react',
|
|
891
|
+
}),
|
|
892
|
+
{
|
|
893
|
+
name: 'copy-react-hooks',
|
|
894
|
+
writeBundle() {
|
|
895
|
+
const src = 'dist/react';
|
|
896
|
+
const dest = 'adapters/react';
|
|
897
|
+
if (fs.existsSync(src)) {
|
|
898
|
+
// Only copy built JS, .d.ts, and map files — not .tsx source
|
|
899
|
+
for (const file of fs.readdirSync(src)) {
|
|
900
|
+
if (file.endsWith('.js') || file.endsWith('.d.ts') || file.endsWith('.js.map') || file.endsWith('.d.ts.map')) {
|
|
901
|
+
fs.copyFileSync(path.join(src, file), path.join(dest, file));
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
]
|
|
908
|
+
},
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
**Step 2: Build and verify**
|
|
912
|
+
|
|
913
|
+
Run: `npm run build:core`
|
|
914
|
+
Expected: Build succeeds, `dist/react/` contains index.js, SniceProvider.js, SniceRouter.js, matchRoute.js, useRequestHandler.js + .d.ts files. Same files copied to `adapters/react/`.
|
|
915
|
+
|
|
916
|
+
**Step 3: Commit**
|
|
917
|
+
|
|
918
|
+
```bash
|
|
919
|
+
git add rollup.config.js
|
|
920
|
+
git commit -m "feat: update rollup config for full React integration build"
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
### Task 9: Integration Test — Full Router Flow
|
|
924
|
+
|
|
925
|
+
Test the full router flow end-to-end without a browser (vitest + happy-dom).
|
|
926
|
+
|
|
927
|
+
**Files:**
|
|
928
|
+
- Create: `tests/react/snice-router-integration.test.ts`
|
|
929
|
+
|
|
930
|
+
**Step 1: Write integration tests**
|
|
931
|
+
|
|
932
|
+
Create `tests/react/snice-router-integration.test.ts`:
|
|
933
|
+
|
|
934
|
+
```typescript
|
|
935
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
936
|
+
import { SniceRouter, Route } from '../../src/react/SniceRouter';
|
|
937
|
+
import { SniceProvider, useSniceContext, useNavigate, useParams, useRoute } from '../../src/react/SniceProvider';
|
|
938
|
+
|
|
939
|
+
describe('SniceRouter integration', () => {
|
|
940
|
+
it('should export all public API', () => {
|
|
941
|
+
expect(typeof SniceRouter).toBe('function');
|
|
942
|
+
expect(typeof Route).toBe('function');
|
|
943
|
+
expect(typeof SniceProvider).toBe('function');
|
|
944
|
+
expect(typeof useSniceContext).toBe('function');
|
|
945
|
+
expect(typeof useNavigate).toBe('function');
|
|
946
|
+
expect(typeof useParams).toBe('function');
|
|
947
|
+
expect(typeof useRoute).toBe('function');
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
it('Route component should return null (config-only)', () => {
|
|
951
|
+
const result = Route({
|
|
952
|
+
path: '/test',
|
|
953
|
+
page: () => null,
|
|
954
|
+
});
|
|
955
|
+
expect(result).toBeNull();
|
|
956
|
+
});
|
|
957
|
+
});
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
**Step 2: Run test**
|
|
961
|
+
|
|
962
|
+
Run: `npx vitest run tests/react/snice-router-integration.test.ts`
|
|
963
|
+
Expected: PASS
|
|
964
|
+
|
|
965
|
+
**Step 3: Commit**
|
|
966
|
+
|
|
967
|
+
```bash
|
|
968
|
+
git add tests/react/snice-router-integration.test.ts
|
|
969
|
+
git commit -m "test: add React router integration tests"
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
### Task 10: Update Package.json Exports
|
|
973
|
+
|
|
974
|
+
Ensure `snice/react` resolves to the barrel and individual modules are importable.
|
|
975
|
+
|
|
976
|
+
**Files:**
|
|
977
|
+
- Modify: `package.json`
|
|
978
|
+
|
|
979
|
+
**Step 1: Verify existing exports**
|
|
980
|
+
|
|
981
|
+
Check the current `"./react"` and `"./react/*"` exports in package.json. They should already point to `adapters/react/`:
|
|
982
|
+
|
|
983
|
+
```json
|
|
984
|
+
"./react": {
|
|
985
|
+
"types": "./adapters/react/index.d.ts",
|
|
986
|
+
"import": "./adapters/react/index.js"
|
|
987
|
+
},
|
|
988
|
+
"./react/*": {
|
|
989
|
+
"types": "./adapters/react/*.d.ts",
|
|
990
|
+
"import": "./adapters/react/*.js"
|
|
991
|
+
}
|
|
992
|
+
```
|
|
993
|
+
|
|
994
|
+
These should already work with the new barrel export. Verify by checking that the built files exist after `npm run build:core`.
|
|
995
|
+
|
|
996
|
+
**Step 2: Commit (only if changes needed)**
|
|
997
|
+
|
|
998
|
+
```bash
|
|
999
|
+
git add package.json
|
|
1000
|
+
git commit -m "chore: update package.json exports for React integration"
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
### Task 11: Documentation
|
|
1004
|
+
|
|
1005
|
+
**Files:**
|
|
1006
|
+
- Create or modify: `docs/ai/react-integration.md`
|
|
1007
|
+
- Modify: `docs/ai/patterns.md` (add React router section)
|
|
1008
|
+
|
|
1009
|
+
**Step 1: Create `docs/ai/react-integration.md`**
|
|
1010
|
+
|
|
1011
|
+
```markdown
|
|
1012
|
+
# React Integration
|
|
1013
|
+
|
|
1014
|
+
Source: `src/react/` → Build: `dist/react/` → Published: `adapters/react/`
|
|
1015
|
+
Import: `import { SniceRouter, Route, useSniceContext } from 'snice/react'`
|
|
1016
|
+
|
|
1017
|
+
## Components
|
|
1018
|
+
|
|
1019
|
+
| Export | Type | Purpose |
|
|
1020
|
+
|---|---|---|
|
|
1021
|
+
| `<SniceRouter>` | Component | Root provider. URL state, route matching, guards, layouts, context. |
|
|
1022
|
+
| `<Route>` | Component | Route definition. Child of `<SniceRouter>`. |
|
|
1023
|
+
| `<SniceProvider>` | Component | Context provider (standalone, no router needed). |
|
|
1024
|
+
|
|
1025
|
+
## Hooks
|
|
1026
|
+
|
|
1027
|
+
| Export | Returns | Purpose |
|
|
1028
|
+
|---|---|---|
|
|
1029
|
+
| `useSniceContext()` | `SniceReactContext` | Full merged context (`application`, `navigation`, `navigate`, `params`, `route`) |
|
|
1030
|
+
| `useNavigate()` | `(path) => void` | Programmatic navigation |
|
|
1031
|
+
| `useParams()` | `Record<string, string>` | Current route params |
|
|
1032
|
+
| `useRoute()` | `string` | Current matched route pattern |
|
|
1033
|
+
| `useRequestHandler()` | `void` | Handle @request channels from Snice elements |
|
|
1034
|
+
|
|
1035
|
+
## SniceRouter Props
|
|
1036
|
+
|
|
1037
|
+
- `mode`: `"hash"` | `"history"` — URL strategy
|
|
1038
|
+
- `context`: `object` — App context passed to guards, pages, layouts
|
|
1039
|
+
- `layout`: `Component | string` — Default layout
|
|
1040
|
+
- `loading`: `Component | string | JSX` — Shown during async guards
|
|
1041
|
+
- `fallback`: `Component | string | JSX` — No route match
|
|
1042
|
+
|
|
1043
|
+
## Route Props
|
|
1044
|
+
|
|
1045
|
+
- `path`: Route pattern (e.g., `/users/:id`)
|
|
1046
|
+
- `page`: React component OR Snice tag name (string)
|
|
1047
|
+
- `guard`: `(ctx, params) => boolean | Promise<boolean>`
|
|
1048
|
+
- `guards`: Array of guard functions (AND logic)
|
|
1049
|
+
- `guardRedirect`: Redirect path on guard failure
|
|
1050
|
+
- `layout`: Override layout. `false` = no layout
|
|
1051
|
+
- `placard`: Page metadata for layouts
|
|
1052
|
+
|
|
1053
|
+
## Guards
|
|
1054
|
+
|
|
1055
|
+
Same signature for both Snice and React:
|
|
1056
|
+
`(context, params) => boolean | Promise<boolean>`
|
|
1057
|
+
|
|
1058
|
+
Async guards show the `loading` component until resolved.
|
|
1059
|
+
|
|
1060
|
+
## Context Shape
|
|
1061
|
+
|
|
1062
|
+
```typescript
|
|
1063
|
+
interface SniceReactContext {
|
|
1064
|
+
application: Record<string, any>; // User-defined app state
|
|
1065
|
+
navigation: { route: string; params: Record<string, string>; placards: Placard[] };
|
|
1066
|
+
navigate: (path: string) => void;
|
|
1067
|
+
fetch?: typeof globalThis.fetch;
|
|
1068
|
+
}
|
|
1069
|
+
```
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
**Step 2: Update `docs/ai/patterns.md`**
|
|
1073
|
+
|
|
1074
|
+
Add a section at the end for React Router usage:
|
|
1075
|
+
|
|
1076
|
+
```markdown
|
|
1077
|
+
## React Router
|
|
1078
|
+
|
|
1079
|
+
```tsx
|
|
1080
|
+
import { SniceRouter, Route, useSniceContext } from 'snice/react';
|
|
1081
|
+
|
|
1082
|
+
function App() {
|
|
1083
|
+
return (
|
|
1084
|
+
<SniceRouter mode="hash" context={{ user, theme: 'dark' }} layout={AppShell}>
|
|
1085
|
+
<Route path="/" page={HomePage} />
|
|
1086
|
+
<Route path="/settings" page={SettingsPage} guard={authGuard} guardRedirect="/login" />
|
|
1087
|
+
<Route path="/legacy" page="legacy-dashboard" />
|
|
1088
|
+
<Route path="/login" page={LoginPage} layout={false} />
|
|
1089
|
+
</SniceRouter>
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// Layout
|
|
1094
|
+
function AppShell({ children }) {
|
|
1095
|
+
const ctx = useSniceContext();
|
|
1096
|
+
return <div className="shell"><nav>...</nav><main>{children}</main></div>;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Guard (shared with vanilla Snice)
|
|
1100
|
+
const authGuard = (ctx, params) => !!ctx.principal;
|
|
1101
|
+
```
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
**Step 3: Commit**
|
|
1105
|
+
|
|
1106
|
+
```bash
|
|
1107
|
+
git add docs/ai/react-integration.md docs/ai/patterns.md
|
|
1108
|
+
git commit -m "docs: add React integration documentation"
|
|
1109
|
+
```
|
|
1110
|
+
|
|
1111
|
+
### Task 12: Human-Facing Documentation
|
|
1112
|
+
|
|
1113
|
+
**Files:**
|
|
1114
|
+
- Create: `docs/react-integration.md`
|
|
1115
|
+
|
|
1116
|
+
**Step 1: Write docs/react-integration.md**
|
|
1117
|
+
|
|
1118
|
+
Write the human-friendly version with full examples, matching the design doc's content. Use the design doc at `docs/plans/2026-03-10-react-integration-design.md` as the source. Include:
|
|
1119
|
+
- Installation/import instructions
|
|
1120
|
+
- SniceRouter + Route full API
|
|
1121
|
+
- Guards (sync + async) with examples
|
|
1122
|
+
- Layouts (React + Snice tag)
|
|
1123
|
+
- Mixed pages example
|
|
1124
|
+
- Context hooks
|
|
1125
|
+
- Comparison with vanilla Snice
|
|
1126
|
+
|
|
1127
|
+
**Step 2: Commit**
|
|
1128
|
+
|
|
1129
|
+
```bash
|
|
1130
|
+
git add docs/react-integration.md
|
|
1131
|
+
git commit -m "docs: add human-friendly React integration guide"
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
### Task 13: Final Build + Test Verification
|
|
1135
|
+
|
|
1136
|
+
**Step 1: Full build**
|
|
1137
|
+
|
|
1138
|
+
Run: `npm run build:core`
|
|
1139
|
+
Expected: All builds succeed including React integration
|
|
1140
|
+
|
|
1141
|
+
**Step 2: Run all tests**
|
|
1142
|
+
|
|
1143
|
+
Run: `npm run build:test && npx vitest run`
|
|
1144
|
+
Expected: All tests pass
|
|
1145
|
+
|
|
1146
|
+
**Step 3: Verify exports**
|
|
1147
|
+
|
|
1148
|
+
Run: `ls dist/react/ && ls adapters/react/index.js adapters/react/SniceRouter.js adapters/react/SniceProvider.js`
|
|
1149
|
+
Expected: All files present
|
|
1150
|
+
|
|
1151
|
+
**Step 4: Final commit if needed**
|
|
1152
|
+
|
|
1153
|
+
```bash
|
|
1154
|
+
git add -A
|
|
1155
|
+
git commit -m "chore: final build verification for React integration"
|
|
1156
|
+
```
|
|
1157
|
+
|
|
1158
|
+
---
|
|
1159
|
+
|
|
1160
|
+
## Task Summary
|
|
1161
|
+
|
|
1162
|
+
| Task | Description | Dependencies |
|
|
1163
|
+
|------|-------------|--------------|
|
|
1164
|
+
| 1 | Update Guard type for async | None |
|
|
1165
|
+
| 2 | Make vanilla checkGuards async | Task 1 |
|
|
1166
|
+
| 3 | Async guard tests for vanilla router | Task 2 |
|
|
1167
|
+
| 4 | SniceProvider + context hooks | None |
|
|
1168
|
+
| 5 | Route matching utility | None |
|
|
1169
|
+
| 6 | SniceRouter + Route components | Tasks 4, 5 |
|
|
1170
|
+
| 7 | React barrel export | Tasks 4, 5, 6 |
|
|
1171
|
+
| 8 | Rollup build configuration | Task 7 |
|
|
1172
|
+
| 9 | Integration tests | Tasks 6, 7 |
|
|
1173
|
+
| 10 | Package.json exports | Task 8 |
|
|
1174
|
+
| 11 | AI documentation | Task 6 |
|
|
1175
|
+
| 12 | Human documentation | Task 6 |
|
|
1176
|
+
| 13 | Final build + test verification | All |
|
|
1177
|
+
|
|
1178
|
+
**Parallelizable:** Tasks 1-3 (async guards) can be done independently from Tasks 4-5 (provider + matching). Tasks 11-12 (docs) can be done in parallel.
|