create-foldkit-app 0.27.3 → 0.28.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/dist/index.js +1 -5
- package/{templates → dist/templates}/base/AGENTS.md +17 -19
- package/dist/templates/examples/api-cache/package.json +29 -0
- package/dist/templates/examples/api-cache/src/data.test.ts +28 -0
- package/dist/templates/examples/api-cache/src/data.ts +126 -0
- package/dist/templates/examples/api-cache/src/entry.ts +17 -0
- package/dist/templates/examples/api-cache/src/main.fixtures.ts +70 -0
- package/dist/templates/examples/api-cache/src/main.ts +744 -0
- package/dist/templates/examples/api-cache/src/scene.test.ts +120 -0
- package/dist/templates/examples/api-cache/src/story.test.ts +288 -0
- package/dist/templates/examples/api-cache/src/styles.css +1 -0
- package/dist/templates/examples/api-cache/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/auth/package.json +30 -0
- package/dist/templates/examples/auth/src/command.ts +49 -0
- package/dist/templates/examples/auth/src/constant.ts +1 -0
- package/dist/templates/examples/auth/src/domain/session.ts +9 -0
- package/dist/templates/examples/auth/src/entry.ts +25 -0
- package/dist/templates/examples/auth/src/main.ts +75 -0
- package/dist/templates/examples/auth/src/message.ts +22 -0
- package/dist/templates/examples/auth/src/model.ts +9 -0
- package/dist/templates/examples/auth/src/notFoundView.ts +25 -0
- package/dist/templates/examples/auth/src/page/index.ts +2 -0
- package/dist/templates/examples/auth/src/page/loggedIn/index.ts +4 -0
- package/dist/templates/examples/auth/src/page/loggedIn/message.ts +17 -0
- package/dist/templates/examples/auth/src/page/loggedIn/model.ts +21 -0
- package/dist/templates/examples/auth/src/page/loggedIn/page/dashboard.ts +40 -0
- package/dist/templates/examples/auth/src/page/loggedIn/page/settings.ts +71 -0
- package/dist/templates/examples/auth/src/page/loggedIn/update.ts +16 -0
- package/dist/templates/examples/auth/src/page/loggedIn/view.ts +87 -0
- package/dist/templates/examples/auth/src/page/loggedOut/index.ts +4 -0
- package/dist/templates/examples/auth/src/page/loggedOut/message.ts +20 -0
- package/dist/templates/examples/auth/src/page/loggedOut/model.ts +21 -0
- package/dist/templates/examples/auth/src/page/loggedOut/page/home.ts +68 -0
- package/dist/templates/examples/auth/src/page/loggedOut/page/login.scene.test.ts +116 -0
- package/dist/templates/examples/auth/src/page/loggedOut/page/login.story.test.ts +112 -0
- package/dist/templates/examples/auth/src/page/loggedOut/page/login.ts +346 -0
- package/dist/templates/examples/auth/src/page/loggedOut/update.ts +32 -0
- package/dist/templates/examples/auth/src/page/loggedOut/view.ts +31 -0
- package/dist/templates/examples/auth/src/route.ts +56 -0
- package/dist/templates/examples/auth/src/scene.test.ts +61 -0
- package/dist/templates/examples/auth/src/styles.css +1 -0
- package/dist/templates/examples/auth/src/update.ts +179 -0
- package/dist/templates/examples/auth/src/view.ts +40 -0
- package/dist/templates/examples/auth/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/canvas-art/package.json +29 -0
- package/dist/templates/examples/canvas-art/src/entry.ts +17 -0
- package/dist/templates/examples/canvas-art/src/main.ts +274 -0
- package/dist/templates/examples/canvas-art/src/scene.test.ts +70 -0
- package/dist/templates/examples/canvas-art/src/story.test.ts +155 -0
- package/dist/templates/examples/canvas-art/src/styles.css +1 -0
- package/dist/templates/examples/canvas-art/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/charting/package.json +31 -0
- package/dist/templates/examples/charting/src/chartHost.ts +20 -0
- package/dist/templates/examples/charting/src/command.ts +62 -0
- package/dist/templates/examples/charting/src/domain.ts +175 -0
- package/dist/templates/examples/charting/src/echarts.ts +408 -0
- package/dist/templates/examples/charting/src/entry.ts +25 -0
- package/dist/templates/examples/charting/src/githubApi.ts +271 -0
- package/dist/templates/examples/charting/src/http.ts +50 -0
- package/dist/templates/examples/charting/src/init.test.ts +14 -0
- package/dist/templates/examples/charting/src/init.ts +29 -0
- package/dist/templates/examples/charting/src/main.fixtures.ts +162 -0
- package/dist/templates/examples/charting/src/message.ts +23 -0
- package/dist/templates/examples/charting/src/model.ts +22 -0
- package/dist/templates/examples/charting/src/npmApi.ts +75 -0
- package/dist/templates/examples/charting/src/radioGroups.ts +11 -0
- package/dist/templates/examples/charting/src/scene.test.ts +103 -0
- package/dist/templates/examples/charting/src/story.test.ts +145 -0
- package/dist/templates/examples/charting/src/styles.css +1 -0
- package/dist/templates/examples/charting/src/subscription.ts +70 -0
- package/dist/templates/examples/charting/src/telemetry.test.ts +64 -0
- package/dist/templates/examples/charting/src/telemetry.ts +432 -0
- package/dist/templates/examples/charting/src/update.ts +217 -0
- package/dist/templates/examples/charting/src/view/chart.ts +129 -0
- package/dist/templates/examples/charting/src/view/format.ts +23 -0
- package/dist/templates/examples/charting/src/view/index.ts +1 -0
- package/dist/templates/examples/charting/src/view/layout.ts +175 -0
- package/dist/templates/examples/charting/src/view/sidebar.ts +309 -0
- package/dist/templates/examples/charting/src/view/view.ts +23 -0
- package/dist/templates/examples/charting/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/charting/src/weeks.ts +105 -0
- package/dist/templates/examples/counter/package.json +29 -0
- package/dist/templates/examples/counter/src/entry.ts +16 -0
- package/dist/templates/examples/counter/src/main.ts +95 -0
- package/dist/templates/examples/counter/src/scene.test.ts +57 -0
- package/dist/templates/examples/counter/src/story.test.ts +71 -0
- package/dist/templates/examples/counter/src/styles.css +1 -0
- package/dist/templates/examples/counter/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/counters/package.json +29 -0
- package/dist/templates/examples/counters/src/counter.ts +70 -0
- package/dist/templates/examples/counters/src/entry.ts +16 -0
- package/dist/templates/examples/counters/src/main.ts +171 -0
- package/dist/templates/examples/counters/src/scene.test.ts +90 -0
- package/dist/templates/examples/counters/src/story.test.ts +127 -0
- package/dist/templates/examples/counters/src/styles.css +1 -0
- package/dist/templates/examples/counters/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/crash-view/package.json +29 -0
- package/dist/templates/examples/crash-view/src/entry.ts +28 -0
- package/dist/templates/examples/crash-view/src/main.ts +104 -0
- package/dist/templates/examples/crash-view/src/scene.test.ts +14 -0
- package/dist/templates/examples/crash-view/src/story.test.ts +11 -0
- package/dist/templates/examples/crash-view/src/styles.css +1 -0
- package/dist/templates/examples/crash-view/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/embedding/package.json +29 -0
- package/dist/templates/examples/embedding/src/entry.ts +3 -0
- package/dist/templates/examples/embedding/src/host.ts +108 -0
- package/dist/templates/examples/embedding/src/main.ts +144 -0
- package/dist/templates/examples/embedding/src/scene.test.ts +28 -0
- package/dist/templates/examples/embedding/src/story.test.ts +64 -0
- package/dist/templates/examples/embedding/src/styles.css +1 -0
- package/dist/templates/examples/embedding/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/form/package.json +30 -0
- package/dist/templates/examples/form/src/entry.ts +16 -0
- package/dist/templates/examples/form/src/main.ts +493 -0
- package/dist/templates/examples/form/src/scene.test.ts +165 -0
- package/dist/templates/examples/form/src/story.test.ts +190 -0
- package/dist/templates/examples/form/src/styles.css +1 -0
- package/dist/templates/examples/form/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/generative-art/package.json +29 -0
- package/dist/templates/examples/generative-art/src/command.ts +109 -0
- package/dist/templates/examples/generative-art/src/constant.ts +68 -0
- package/dist/templates/examples/generative-art/src/entry.ts +22 -0
- package/dist/templates/examples/generative-art/src/main.ts +59 -0
- package/dist/templates/examples/generative-art/src/message.ts +35 -0
- package/dist/templates/examples/generative-art/src/model.ts +33 -0
- package/dist/templates/examples/generative-art/src/noise.ts +93 -0
- package/dist/templates/examples/generative-art/src/scene.test.ts +103 -0
- package/dist/templates/examples/generative-art/src/story.test.ts +203 -0
- package/dist/templates/examples/generative-art/src/styles.css +1 -0
- package/dist/templates/examples/generative-art/src/subscription.ts +35 -0
- package/dist/templates/examples/generative-art/src/update.ts +319 -0
- package/dist/templates/examples/generative-art/src/view.ts +451 -0
- package/dist/templates/examples/generative-art/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/interrupting-commands/package.json +29 -0
- package/dist/templates/examples/interrupting-commands/src/entry.ts +16 -0
- package/dist/templates/examples/interrupting-commands/src/main.ts +354 -0
- package/dist/templates/examples/interrupting-commands/src/scene.test.ts +89 -0
- package/dist/templates/examples/interrupting-commands/src/story.test.ts +224 -0
- package/dist/templates/examples/interrupting-commands/src/styles.css +1 -0
- package/dist/templates/examples/interrupting-commands/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/job-application/package.json +30 -0
- package/dist/templates/examples/job-application/src/command.ts +23 -0
- package/dist/templates/examples/job-application/src/domain/index.ts +3 -0
- package/dist/templates/examples/job-application/src/domain/proficiencyLevel.ts +11 -0
- package/dist/templates/examples/job-application/src/domain/pronounOption.ts +13 -0
- package/dist/templates/examples/job-application/src/domain/step.ts +28 -0
- package/dist/templates/examples/job-application/src/entry.ts +17 -0
- package/dist/templates/examples/job-application/src/main.ts +69 -0
- package/dist/templates/examples/job-application/src/message.ts +44 -0
- package/dist/templates/examples/job-application/src/model.ts +47 -0
- package/dist/templates/examples/job-application/src/scene.test.ts +269 -0
- package/dist/templates/examples/job-application/src/step/attachments.ts +111 -0
- package/dist/templates/examples/job-application/src/step/coverLetter.ts +34 -0
- package/dist/templates/examples/job-application/src/step/education/education.ts +121 -0
- package/dist/templates/examples/job-application/src/step/education/entry.ts +168 -0
- package/dist/templates/examples/job-application/src/step/education/index.ts +2 -0
- package/dist/templates/examples/job-application/src/step/index.ts +6 -0
- package/dist/templates/examples/job-application/src/step/personalInfo.ts +305 -0
- package/dist/templates/examples/job-application/src/step/skills/entry.ts +126 -0
- package/dist/templates/examples/job-application/src/step/skills/index.ts +2 -0
- package/dist/templates/examples/job-application/src/step/skills/skills.ts +118 -0
- package/dist/templates/examples/job-application/src/step/validation.ts +6 -0
- package/dist/templates/examples/job-application/src/step/workHistory/entry.ts +206 -0
- package/dist/templates/examples/job-application/src/step/workHistory/index.ts +2 -0
- package/dist/templates/examples/job-application/src/step/workHistory/workHistory.ts +121 -0
- package/dist/templates/examples/job-application/src/story.test.ts +775 -0
- package/dist/templates/examples/job-application/src/styles.css +1 -0
- package/dist/templates/examples/job-application/src/update.ts +183 -0
- package/dist/templates/examples/job-application/src/view/attachments.ts +215 -0
- package/dist/templates/examples/job-application/src/view/coverLetter.ts +73 -0
- package/dist/templates/examples/job-application/src/view/datePicker.ts +207 -0
- package/dist/templates/examples/job-application/src/view/education.ts +63 -0
- package/dist/templates/examples/job-application/src/view/educationEntry.ts +165 -0
- package/dist/templates/examples/job-application/src/view/field.ts +183 -0
- package/dist/templates/examples/job-application/src/view/format.ts +50 -0
- package/dist/templates/examples/job-application/src/view/icon.ts +21 -0
- package/dist/templates/examples/job-application/src/view/index.ts +352 -0
- package/dist/templates/examples/job-application/src/view/personalInfo.ts +224 -0
- package/dist/templates/examples/job-application/src/view/preview.ts +265 -0
- package/dist/templates/examples/job-application/src/view/review.ts +388 -0
- package/dist/templates/examples/job-application/src/view/skillEntry.ts +102 -0
- package/dist/templates/examples/job-application/src/view/skills.ts +49 -0
- package/dist/templates/examples/job-application/src/view/stepNav.ts +181 -0
- package/dist/templates/examples/job-application/src/view/workEntry.ts +159 -0
- package/dist/templates/examples/job-application/src/view/workHistory.ts +55 -0
- package/dist/templates/examples/job-application/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/kanban/package.json +31 -0
- package/dist/templates/examples/kanban/src/command.ts +46 -0
- package/dist/templates/examples/kanban/src/constant.ts +149 -0
- package/dist/templates/examples/kanban/src/domain/card.ts +10 -0
- package/dist/templates/examples/kanban/src/domain/column.ts +115 -0
- package/dist/templates/examples/kanban/src/domain/index.ts +2 -0
- package/dist/templates/examples/kanban/src/entry.ts +27 -0
- package/dist/templates/examples/kanban/src/main.ts +56 -0
- package/dist/templates/examples/kanban/src/message.ts +21 -0
- package/dist/templates/examples/kanban/src/model.ts +21 -0
- package/dist/templates/examples/kanban/src/scene.test.ts +161 -0
- package/dist/templates/examples/kanban/src/story.test.ts +461 -0
- package/dist/templates/examples/kanban/src/styles.css +1 -0
- package/dist/templates/examples/kanban/src/subscription.ts +16 -0
- package/dist/templates/examples/kanban/src/update.ts +205 -0
- package/dist/templates/examples/kanban/src/view/card.ts +79 -0
- package/dist/templates/examples/kanban/src/view/column.ts +283 -0
- package/dist/templates/examples/kanban/src/view/index.ts +71 -0
- package/dist/templates/examples/kanban/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/managed-resource-layer/package.json +29 -0
- package/dist/templates/examples/managed-resource-layer/src/entry.ts +17 -0
- package/dist/templates/examples/managed-resource-layer/src/main.ts +309 -0
- package/dist/templates/examples/managed-resource-layer/src/scene.test.ts +117 -0
- package/dist/templates/examples/managed-resource-layer/src/story.test.ts +118 -0
- package/dist/templates/examples/managed-resource-layer/src/styles.css +1 -0
- package/dist/templates/examples/managed-resource-layer/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/map/package.json +31 -0
- package/dist/templates/examples/map/src/entry.ts +17 -0
- package/dist/templates/examples/map/src/locations.ts +69 -0
- package/dist/templates/examples/map/src/main.fixtures.ts +28 -0
- package/dist/templates/examples/map/src/main.ts +781 -0
- package/dist/templates/examples/map/src/mapHost.ts +23 -0
- package/dist/templates/examples/map/src/scene.test.ts +136 -0
- package/dist/templates/examples/map/src/story.test.ts +190 -0
- package/dist/templates/examples/map/src/styles.css +2 -0
- package/dist/templates/examples/map/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/pixel-art/package.json +31 -0
- package/dist/templates/examples/pixel-art/src/command.ts +87 -0
- package/dist/templates/examples/pixel-art/src/comparison.bench.test.ts +204 -0
- package/dist/templates/examples/pixel-art/src/constant.ts +21 -0
- package/dist/templates/examples/pixel-art/src/entry.ts +27 -0
- package/dist/templates/examples/pixel-art/src/grid.ts +134 -0
- package/dist/templates/examples/pixel-art/src/main.bench.ts +133 -0
- package/dist/templates/examples/pixel-art/src/main.ts +85 -0
- package/dist/templates/examples/pixel-art/src/message.ts +36 -0
- package/dist/templates/examples/pixel-art/src/model.ts +79 -0
- package/dist/templates/examples/pixel-art/src/palette.ts +134 -0
- package/dist/templates/examples/pixel-art/src/scene.test.ts +250 -0
- package/dist/templates/examples/pixel-art/src/story.test.ts +482 -0
- package/dist/templates/examples/pixel-art/src/styles.css +1 -0
- package/dist/templates/examples/pixel-art/src/subscription.ts +61 -0
- package/dist/templates/examples/pixel-art/src/update.ts +469 -0
- package/dist/templates/examples/pixel-art/src/view/canvas.ts +134 -0
- package/dist/templates/examples/pixel-art/src/view/dialog.ts +152 -0
- package/dist/templates/examples/pixel-art/src/view/history.ts +214 -0
- package/dist/templates/examples/pixel-art/src/view/index.ts +1 -0
- package/dist/templates/examples/pixel-art/src/view/toolbar.ts +452 -0
- package/dist/templates/examples/pixel-art/src/view/view.ts +183 -0
- package/dist/templates/examples/pixel-art/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/query-sync/package.json +30 -0
- package/dist/templates/examples/query-sync/src/data.ts +166 -0
- package/dist/templates/examples/query-sync/src/entry.ts +20 -0
- package/dist/templates/examples/query-sync/src/main.ts +898 -0
- package/dist/templates/examples/query-sync/src/scene.test.ts +89 -0
- package/dist/templates/examples/query-sync/src/story.test.ts +151 -0
- package/dist/templates/examples/query-sync/src/styles.css +1 -0
- package/dist/templates/examples/query-sync/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/route-transitions/package.json +28 -0
- package/dist/templates/examples/route-transitions/src/data.ts +61 -0
- package/dist/templates/examples/route-transitions/src/entry.ts +20 -0
- package/dist/templates/examples/route-transitions/src/main.ts +798 -0
- package/dist/templates/examples/route-transitions/src/route.ts +48 -0
- package/dist/templates/examples/route-transitions/src/story.test.ts +225 -0
- package/dist/templates/examples/route-transitions/src/styles.css +1 -0
- package/dist/templates/examples/route-transitions/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/routing/package.json +29 -0
- package/dist/templates/examples/routing/src/entry.ts +20 -0
- package/dist/templates/examples/routing/src/fileTree.ts +98 -0
- package/dist/templates/examples/routing/src/main.ts +573 -0
- package/dist/templates/examples/routing/src/page/index.ts +1 -0
- package/dist/templates/examples/routing/src/page/people.ts +349 -0
- package/dist/templates/examples/routing/src/route.ts +82 -0
- package/dist/templates/examples/routing/src/scene.test.ts +180 -0
- package/dist/templates/examples/routing/src/story.test.ts +242 -0
- package/dist/templates/examples/routing/src/styles.css +1 -0
- package/dist/templates/examples/routing/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/shopping-cart/package.json +29 -0
- package/dist/templates/examples/shopping-cart/src/data/products.ts +10 -0
- package/dist/templates/examples/shopping-cart/src/domain/cart.ts +77 -0
- package/dist/templates/examples/shopping-cart/src/domain/index.ts +2 -0
- package/dist/templates/examples/shopping-cart/src/domain/item.ts +16 -0
- package/dist/templates/examples/shopping-cart/src/entry.ts +20 -0
- package/dist/templates/examples/shopping-cart/src/main.ts +307 -0
- package/dist/templates/examples/shopping-cart/src/page/cart.ts +203 -0
- package/dist/templates/examples/shopping-cart/src/page/checkout.ts +205 -0
- package/dist/templates/examples/shopping-cart/src/page/index.ts +3 -0
- package/dist/templates/examples/shopping-cart/src/page/products.ts +267 -0
- package/dist/templates/examples/shopping-cart/src/route.ts +43 -0
- package/dist/templates/examples/shopping-cart/src/scene.test.ts +138 -0
- package/dist/templates/examples/shopping-cart/src/story.test.ts +233 -0
- package/dist/templates/examples/shopping-cart/src/styles.css +1 -0
- package/dist/templates/examples/shopping-cart/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/slow-warnings/package.json +28 -0
- package/dist/templates/examples/slow-warnings/src/entry.ts +29 -0
- package/dist/templates/examples/slow-warnings/src/handleSlow.test.ts +43 -0
- package/dist/templates/examples/slow-warnings/src/main.ts +574 -0
- package/dist/templates/examples/slow-warnings/src/scene.test.ts +57 -0
- package/dist/templates/examples/slow-warnings/src/story.test.ts +118 -0
- package/dist/templates/examples/slow-warnings/src/styles.css +1 -0
- package/dist/templates/examples/slow-warnings/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/snake/package.json +28 -0
- package/dist/templates/examples/snake/src/constants.ts +11 -0
- package/dist/templates/examples/snake/src/domain/apple.ts +22 -0
- package/dist/templates/examples/snake/src/domain/direction.ts +17 -0
- package/dist/templates/examples/snake/src/domain/index.ts +4 -0
- package/dist/templates/examples/snake/src/domain/position.ts +32 -0
- package/dist/templates/examples/snake/src/domain/snake.ts +43 -0
- package/dist/templates/examples/snake/src/entry.ts +17 -0
- package/dist/templates/examples/snake/src/main.ts +345 -0
- package/dist/templates/examples/snake/src/scene.test.ts +77 -0
- package/dist/templates/examples/snake/src/story.test.ts +165 -0
- package/dist/templates/examples/snake/src/styles.css +1 -0
- package/dist/templates/examples/snake/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/ssg/package.json +29 -0
- package/dist/templates/examples/ssg/src/entry.server.ts +14 -0
- package/{templates/rendering → dist/templates/examples}/ssg/src/entry.ts +3 -11
- package/dist/templates/examples/ssg/src/main.ts +163 -0
- package/dist/templates/examples/ssg/src/styles.css +1 -0
- package/dist/templates/examples/ssr/package.json +33 -0
- package/dist/templates/examples/ssr/src/cookie.ts +13 -0
- package/dist/templates/examples/ssr/src/entry.server.ts +51 -0
- package/dist/templates/examples/ssr/src/main.ts +170 -0
- package/{templates/rendering → dist/templates/examples}/ssr/src/scene.test.ts +5 -8
- package/dist/templates/examples/ssr/src/styles.css +1 -0
- package/dist/templates/examples/ssr/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/state-machine/package.json +30 -0
- package/dist/templates/examples/state-machine/src/entry.ts +17 -0
- package/dist/templates/examples/state-machine/src/icon.ts +51 -0
- package/dist/templates/examples/state-machine/src/main.ts +420 -0
- package/dist/templates/examples/state-machine/src/scene.test.ts +85 -0
- package/dist/templates/examples/state-machine/src/story.test.ts +199 -0
- package/dist/templates/examples/state-machine/src/styles.css +53 -0
- package/dist/templates/examples/state-machine/src/view.ts +1675 -0
- package/dist/templates/examples/state-machine/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/stopwatch/package.json +29 -0
- package/dist/templates/examples/stopwatch/src/entry.ts +17 -0
- package/dist/templates/examples/stopwatch/src/main.ts +237 -0
- package/dist/templates/examples/stopwatch/src/scene.test.ts +124 -0
- package/dist/templates/examples/stopwatch/src/story.test.ts +126 -0
- package/dist/templates/examples/stopwatch/src/styles.css +1 -0
- package/dist/templates/examples/stopwatch/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/todo/package.json +30 -0
- package/dist/templates/examples/todo/src/entry.ts +17 -0
- package/dist/templates/examples/todo/src/main.ts +682 -0
- package/dist/templates/examples/todo/src/scene.test.ts +177 -0
- package/dist/templates/examples/todo/src/story.test.ts +374 -0
- package/dist/templates/examples/todo/src/styles.css +1 -0
- package/dist/templates/examples/todo/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/ui-showcase/package.json +30 -0
- package/dist/templates/examples/ui-showcase/src/entry.ts +31 -0
- package/dist/templates/examples/ui-showcase/src/icon.ts +64 -0
- package/dist/templates/examples/ui-showcase/src/main.ts +673 -0
- package/dist/templates/examples/ui-showcase/src/scene.test.ts +102 -0
- package/dist/templates/examples/ui-showcase/src/story.test.ts +128 -0
- package/dist/templates/examples/ui-showcase/src/styles.css +21 -0
- package/dist/templates/examples/ui-showcase/src/ui/init.ts +185 -0
- package/dist/templates/examples/ui-showcase/src/ui/message.ts +164 -0
- package/dist/templates/examples/ui-showcase/src/ui/model.ts +138 -0
- package/dist/templates/examples/ui-showcase/src/ui/subscriptions.ts +55 -0
- package/dist/templates/examples/ui-showcase/src/ui/toast.ts +13 -0
- package/dist/templates/examples/ui-showcase/src/ui/update.ts +1234 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/animation.ts +58 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/button.ts +64 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/calendar.ts +202 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/checkbox.ts +185 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/combobox.ts +326 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/datePicker.ts +263 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/dialog.ts +427 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/disclosure.ts +142 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/dragAndDrop.ts +230 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/fieldset.ts +284 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/fileDrop.ts +119 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/index.ts +24 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/input.ts +90 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/listbox.ts +263 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/menu.ts +172 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/popover.ts +262 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/radioGroup.ts +192 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/select.ts +129 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/slider.ts +135 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/switch.ts +82 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/tabs.ts +187 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/textarea.ts +99 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/toast.ts +165 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/tooltip.ts +123 -0
- package/dist/templates/examples/ui-showcase/src/ui/view/virtualList.ts +361 -0
- package/dist/templates/examples/ui-showcase/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/view-transitions/package.json +28 -0
- package/dist/templates/examples/view-transitions/src/artwork.ts +80 -0
- package/dist/templates/examples/view-transitions/src/entry.ts +21 -0
- package/dist/templates/examples/view-transitions/src/main.ts +348 -0
- package/dist/templates/examples/view-transitions/src/route.ts +29 -0
- package/dist/templates/examples/view-transitions/src/scene.test.ts +130 -0
- package/dist/templates/examples/view-transitions/src/styles.css +66 -0
- package/dist/templates/examples/view-transitions/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/weather/package.json +29 -0
- package/dist/templates/examples/weather/src/entry.ts +16 -0
- package/dist/templates/examples/weather/src/main.fixtures.ts +59 -0
- package/dist/templates/examples/weather/src/main.ts +356 -0
- package/dist/templates/examples/weather/src/scene.test.ts +109 -0
- package/dist/templates/examples/weather/src/story.test.ts +125 -0
- package/dist/templates/examples/weather/src/styles.css +1 -0
- package/dist/templates/examples/weather/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/web-components/package.json +32 -0
- package/dist/templates/examples/web-components/src/entry.ts +16 -0
- package/dist/templates/examples/web-components/src/main.ts +327 -0
- package/dist/templates/examples/web-components/src/scene.test.ts +90 -0
- package/dist/templates/examples/web-components/src/story.test.ts +90 -0
- package/dist/templates/examples/web-components/src/styles.css +1 -0
- package/dist/templates/examples/web-components/src/vitest-setup.ts +3 -0
- package/dist/templates/examples/websocket-chat/package.json +29 -0
- package/dist/templates/examples/websocket-chat/src/entry.ts +26 -0
- package/dist/templates/examples/websocket-chat/src/main.ts +626 -0
- package/dist/templates/examples/websocket-chat/src/scene.test.ts +166 -0
- package/dist/templates/examples/websocket-chat/src/story.test.ts +183 -0
- package/dist/templates/examples/websocket-chat/src/styles.css +1 -0
- package/dist/templates/examples/websocket-chat/src/vitest-setup.ts +3 -0
- package/dist/templates/release.json +27 -0
- package/dist/templates/rendering/ssg/src/entry.ts +20 -0
- package/{templates → dist/templates}/rendering/ssg/src/main.ts +36 -43
- package/dist/templates/rendering/ssg/src/route.ts +20 -0
- package/dist/templates/rendering/ssg/src/vite-env.d.ts +19 -0
- package/dist/templates/rendering/ssr/src/entry.ts +17 -0
- package/{templates → dist/templates}/rendering/ssr/src/main.ts +17 -25
- package/dist/templates/rendering/ssr/src/scene.test.ts +40 -0
- package/dist/templates/rendering/ssr/src/vite-env.d.ts +19 -0
- package/dist/utils/files.js +23 -71
- package/dist/utils/packages.js +63 -44
- package/package.json +3 -4
- /package/{templates → dist/templates}/base/.mcp.json +0 -0
- /package/{templates → dist/templates}/base/.oxlintrc.json +0 -0
- /package/{templates → dist/templates}/base/.prettierignore +0 -0
- /package/{templates → dist/templates}/base/.prettierrc +0 -0
- /package/{templates → dist/templates}/base/README.md +0 -0
- /package/{templates → dist/templates}/base/gitignore +0 -0
- /package/{templates → dist/templates}/base/ignore +0 -0
- /package/{templates → dist/templates}/base/index.html +0 -0
- /package/{templates → dist/templates}/base/package.json +0 -0
- /package/{templates → dist/templates}/base/src/styles.css +0 -0
- /package/{templates → dist/templates}/base/src/vitest-setup.ts +0 -0
- /package/{templates → dist/templates}/base/tsconfig.json +0 -0
- /package/{templates → dist/templates}/base/vite.config.ts +0 -0
- /package/{templates → dist/templates}/base/vitest.config.ts +0 -0
- /package/{templates/rendering → dist/templates/examples}/ssg/src/route.ts +0 -0
- /package/{templates/rendering → dist/templates/examples}/ssg/src/vite-env.d.ts +0 -0
- /package/{templates/rendering → dist/templates/examples}/ssr/src/entry.ts +0 -0
- /package/{templates/rendering → dist/templates/examples}/ssr/src/vite-env.d.ts +0 -0
- /package/{templates → dist/templates}/package-managers/pnpm/pnpm-workspace.yaml +0 -0
- /package/{templates → dist/templates}/rendering/ssg/README.md +0 -0
- /package/{templates → dist/templates}/rendering/ssg/package.json +0 -0
- /package/{templates → dist/templates}/rendering/ssg/scripts/build.mjs +0 -0
- /package/{templates → dist/templates}/rendering/ssg/scripts/prerender.ts +0 -0
- /package/{templates → dist/templates}/rendering/ssg/src/entry.server.ts +0 -0
- /package/{templates → dist/templates}/rendering/ssg/src/scene.test.ts +0 -0
- /package/{templates → dist/templates}/rendering/ssg/tsconfig.json +0 -0
- /package/{templates → dist/templates}/rendering/ssg/vite.config.ts +0 -0
- /package/{templates → dist/templates}/rendering/ssr/README.md +0 -0
- /package/{templates → dist/templates}/rendering/ssr/package.json +0 -0
- /package/{templates → dist/templates}/rendering/ssr/scripts/build.mjs +0 -0
- /package/{templates → dist/templates}/rendering/ssr/server/main.ts +0 -0
- /package/{templates → dist/templates}/rendering/ssr/src/cookie.ts +0 -0
- /package/{templates → dist/templates}/rendering/ssr/src/entry.server.ts +0 -0
- /package/{templates → dist/templates}/rendering/ssr/tsconfig.json +0 -0
- /package/{templates → dist/templates}/rendering/ssr/vite.config.ts +0 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { Array, Effect, Match as M, Option, Schema as S, String } from 'effect'
|
|
2
|
+
import { Command, Runtime } from 'foldkit'
|
|
3
|
+
import { Document, Html, HtmlBuilder } from 'foldkit/html'
|
|
4
|
+
import { defineMessageUnion } from 'foldkit/message'
|
|
5
|
+
import { UrlRequest, load, pushUrl } from 'foldkit/navigation'
|
|
6
|
+
import { Transition } from 'foldkit/route'
|
|
7
|
+
import { evo } from 'foldkit/struct'
|
|
8
|
+
import { Url, toString as urlToString } from 'foldkit/url'
|
|
9
|
+
|
|
10
|
+
import { Artwork, artworks, findArtwork } from './artwork'
|
|
11
|
+
import { AppRoute, artworkRouter, galleryRouter, urlToAppRoute } from './route'
|
|
12
|
+
|
|
13
|
+
export { ArtworkRoute, GalleryRoute, NotFoundRoute } from './route'
|
|
14
|
+
|
|
15
|
+
// MODEL
|
|
16
|
+
|
|
17
|
+
export const Model = S.Struct({
|
|
18
|
+
route: AppRoute,
|
|
19
|
+
filterText: S.String,
|
|
20
|
+
})
|
|
21
|
+
export type Model = typeof Model.Type
|
|
22
|
+
|
|
23
|
+
// MESSAGE
|
|
24
|
+
|
|
25
|
+
export const Message = defineMessageUnion({
|
|
26
|
+
CompletedNavigateInternal: {},
|
|
27
|
+
CompletedLoadExternal: {},
|
|
28
|
+
ClickedLink: { request: UrlRequest },
|
|
29
|
+
ChangedUrl: { url: Url },
|
|
30
|
+
UpdatedFilterText: { filterText: S.String },
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export type Message = typeof Message.Type
|
|
34
|
+
|
|
35
|
+
// INIT
|
|
36
|
+
|
|
37
|
+
export const init: Runtime.RoutingApplicationInit<Model, Message> = (
|
|
38
|
+
url: Url,
|
|
39
|
+
) => [{ route: urlToAppRoute(url), filterText: '' }, []]
|
|
40
|
+
|
|
41
|
+
// COMMAND
|
|
42
|
+
|
|
43
|
+
const NavigateInternal = Command.define('NavigateInternal', {
|
|
44
|
+
args: { url: S.String },
|
|
45
|
+
messages: [Message.CompletedNavigateInternal],
|
|
46
|
+
execute: ({ url }) =>
|
|
47
|
+
pushUrl(url).pipe(Effect.as(Message.CompletedNavigateInternal())),
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const LoadExternal = Command.define('LoadExternal', {
|
|
51
|
+
args: { href: S.String },
|
|
52
|
+
messages: [Message.CompletedLoadExternal],
|
|
53
|
+
execute: ({ href }) =>
|
|
54
|
+
load(href).pipe(Effect.as(Message.CompletedLoadExternal())),
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// UPDATE
|
|
58
|
+
|
|
59
|
+
type UpdateReturn = readonly [Model, ReadonlyArray<Command.Command<Message>>]
|
|
60
|
+
const withUpdateReturn = M.withReturnType<UpdateReturn>()
|
|
61
|
+
|
|
62
|
+
export const update = (model: Model, message: Message) =>
|
|
63
|
+
Message.match<UpdateReturn>(message, {
|
|
64
|
+
CompletedNavigateInternal: () => [model, []],
|
|
65
|
+
CompletedLoadExternal: () => [model, []],
|
|
66
|
+
|
|
67
|
+
ClickedLink: ({ request }) =>
|
|
68
|
+
M.value(request).pipe(
|
|
69
|
+
withUpdateReturn,
|
|
70
|
+
M.tagsExhaustive({
|
|
71
|
+
Internal: ({ url }) => [
|
|
72
|
+
model,
|
|
73
|
+
[NavigateInternal({ url: urlToString(url) })],
|
|
74
|
+
],
|
|
75
|
+
External: ({ href }) => [model, [LoadExternal({ href })]],
|
|
76
|
+
}),
|
|
77
|
+
),
|
|
78
|
+
|
|
79
|
+
ChangedUrl: ({ url }) => [
|
|
80
|
+
evo(model, { route: () => urlToAppRoute(url) }),
|
|
81
|
+
[],
|
|
82
|
+
],
|
|
83
|
+
|
|
84
|
+
UpdatedFilterText: ({ filterText }) => [
|
|
85
|
+
evo(model, { filterText: () => filterText }),
|
|
86
|
+
[],
|
|
87
|
+
],
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// NOTE: every arm past the guard is a navigation, so none return `false`.
|
|
91
|
+
// `true` is "animate, with no direction to declare": the root fade in
|
|
92
|
+
// `styles.css` is not type-scoped, so an untyped transition still cross-fades,
|
|
93
|
+
// it just does not slide.
|
|
94
|
+
export const viewTransition: Runtime.ViewTransitionConfig<Model, Message> = ({
|
|
95
|
+
previousModel,
|
|
96
|
+
model,
|
|
97
|
+
message,
|
|
98
|
+
}) => {
|
|
99
|
+
if (message._tag !== 'ChangedUrl') {
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const transition = Transition.make(previousModel.route, model.route)
|
|
104
|
+
|
|
105
|
+
return Option.match(Transition.enteredAny(transition), {
|
|
106
|
+
onNone: () => true,
|
|
107
|
+
onSome: M.type<AppRoute>().pipe(
|
|
108
|
+
M.withReturnType<Runtime.ViewTransitionDecision>(),
|
|
109
|
+
M.tagsExhaustive({
|
|
110
|
+
Artwork: () => ({ types: ['to-artwork-detail'] }),
|
|
111
|
+
Gallery: () => ({ types: ['to-gallery'] }),
|
|
112
|
+
NotFound: () => true,
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// VIEW
|
|
119
|
+
|
|
120
|
+
const transitionNameFor = (artworkId: number): string => `artwork-${artworkId}`
|
|
121
|
+
|
|
122
|
+
// NOTE: names have to be unique per snapshot, so each artwork carries its own.
|
|
123
|
+
// The shared class is what lets `styles.css` reach all of them with one rule,
|
|
124
|
+
// since a per-artwork name cannot be written as a selector ahead of time.
|
|
125
|
+
const artworkTransitionStyle = (artworkId: number) => ({
|
|
126
|
+
viewTransitionName: transitionNameFor(artworkId),
|
|
127
|
+
viewTransitionClass: 'artwork',
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const filteredArtworks = (filterText: string): ReadonlyArray<Artwork> => {
|
|
131
|
+
const normalizedFilter = filterText.trim().toLowerCase()
|
|
132
|
+
|
|
133
|
+
if (String.isEmpty(normalizedFilter)) {
|
|
134
|
+
return artworks
|
|
135
|
+
} else {
|
|
136
|
+
return Array.filter(artworks, artwork =>
|
|
137
|
+
String.includes(normalizedFilter)(
|
|
138
|
+
`${artwork.title} ${artwork.medium}`.toLowerCase(),
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const artworkCardView = (artwork: Artwork, h: HtmlBuilder<Message>): Html =>
|
|
145
|
+
h.keyed('a')(
|
|
146
|
+
`artwork-${artwork.id}`,
|
|
147
|
+
[h.Href(artworkRouter({ artworkId: artwork.id })), h.Class('group block')],
|
|
148
|
+
[
|
|
149
|
+
h.div([
|
|
150
|
+
h.Class(
|
|
151
|
+
`aspect-square rounded-xl bg-gradient-to-br shadow-sm transition-shadow group-hover:shadow-lg ${artwork.gradientClassName}`,
|
|
152
|
+
),
|
|
153
|
+
h.Style(artworkTransitionStyle(artwork.id)),
|
|
154
|
+
]),
|
|
155
|
+
h.p([h.Class('mt-3 font-medium text-gray-900')], [artwork.title]),
|
|
156
|
+
h.p([h.Class('text-sm text-gray-500')], [artwork.medium]),
|
|
157
|
+
],
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const emptyGalleryView = (h: HtmlBuilder<Message>): Html =>
|
|
161
|
+
h.p([h.Class('text-gray-500')], ['No artworks match the filter.'])
|
|
162
|
+
|
|
163
|
+
const artworkGridView = (
|
|
164
|
+
matchedArtworks: ReadonlyArray<Artwork>,
|
|
165
|
+
h: HtmlBuilder<Message>,
|
|
166
|
+
): Html =>
|
|
167
|
+
h.div(
|
|
168
|
+
[h.Class('grid grid-cols-2 gap-6 sm:grid-cols-3 lg:grid-cols-4')],
|
|
169
|
+
Array.map(matchedArtworks, artwork => artworkCardView(artwork, h)),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
const galleryView = (filterText: string, h: HtmlBuilder<Message>): Html =>
|
|
173
|
+
h.div(
|
|
174
|
+
[h.Class('mx-auto max-w-5xl px-6')],
|
|
175
|
+
[
|
|
176
|
+
h.input([
|
|
177
|
+
h.Type('search'),
|
|
178
|
+
h.Value(filterText),
|
|
179
|
+
h.Placeholder('Filter by title or medium…'),
|
|
180
|
+
h.OnInput(value => Message.UpdatedFilterText({ filterText: value })),
|
|
181
|
+
h.Class(
|
|
182
|
+
'mb-8 w-full max-w-md rounded-lg border border-gray-300 bg-white px-4 py-2 focus:ring-2 focus:ring-gray-400 focus:outline-none',
|
|
183
|
+
),
|
|
184
|
+
]),
|
|
185
|
+
Array.match(filteredArtworks(filterText), {
|
|
186
|
+
onEmpty: () => emptyGalleryView(h),
|
|
187
|
+
onNonEmpty: matchedArtworks => artworkGridView(matchedArtworks, h),
|
|
188
|
+
}),
|
|
189
|
+
],
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
const missingArtworkView = (artworkId: number, h: HtmlBuilder<Message>): Html =>
|
|
193
|
+
h.div(
|
|
194
|
+
[],
|
|
195
|
+
[
|
|
196
|
+
h.h1(
|
|
197
|
+
[h.Class('text-3xl font-bold text-gray-900')],
|
|
198
|
+
['Artwork not found'],
|
|
199
|
+
),
|
|
200
|
+
h.p(
|
|
201
|
+
[h.Class('mt-2 text-gray-600')],
|
|
202
|
+
[`No artwork exists with ID ${artworkId}.`],
|
|
203
|
+
),
|
|
204
|
+
h.a(
|
|
205
|
+
[
|
|
206
|
+
h.Href(galleryRouter()),
|
|
207
|
+
h.Class('mt-4 inline-block text-gray-900 underline'),
|
|
208
|
+
],
|
|
209
|
+
['← Back to gallery'],
|
|
210
|
+
),
|
|
211
|
+
],
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
// NOTE: the hero keeps the card's square ratio on purpose. A View Transition
|
|
215
|
+
// interpolates the old and new snapshots while it interpolates the box, so a
|
|
216
|
+
// square growing into a wider box visibly stretches mid-flight. Matching the
|
|
217
|
+
// ratio makes the morph a pure scale and translate, which is the one case the
|
|
218
|
+
// browser renders cleanly.
|
|
219
|
+
const foundArtworkView = (artwork: Artwork, h: HtmlBuilder<Message>): Html =>
|
|
220
|
+
h.div(
|
|
221
|
+
[],
|
|
222
|
+
[
|
|
223
|
+
h.a(
|
|
224
|
+
[
|
|
225
|
+
h.Href(galleryRouter()),
|
|
226
|
+
h.Class('text-gray-600 underline hover:text-gray-900'),
|
|
227
|
+
],
|
|
228
|
+
['← Back to gallery'],
|
|
229
|
+
),
|
|
230
|
+
h.div(
|
|
231
|
+
[
|
|
232
|
+
h.Class(
|
|
233
|
+
'mt-6 grid gap-10 md:grid-cols-[minmax(0,24rem)_1fr] md:items-center',
|
|
234
|
+
),
|
|
235
|
+
],
|
|
236
|
+
[
|
|
237
|
+
h.div([
|
|
238
|
+
h.Class(
|
|
239
|
+
`aspect-square w-full rounded-2xl bg-gradient-to-br shadow-md ${artwork.gradientClassName}`,
|
|
240
|
+
),
|
|
241
|
+
h.Style(artworkTransitionStyle(artwork.id)),
|
|
242
|
+
]),
|
|
243
|
+
h.div(
|
|
244
|
+
[],
|
|
245
|
+
[
|
|
246
|
+
h.h1(
|
|
247
|
+
[h.Class('text-3xl font-bold text-gray-900')],
|
|
248
|
+
[artwork.title],
|
|
249
|
+
),
|
|
250
|
+
h.p([h.Class('mt-1 text-sm text-gray-500')], [artwork.medium]),
|
|
251
|
+
h.p(
|
|
252
|
+
[h.Class('mt-4 text-lg leading-relaxed text-gray-700')],
|
|
253
|
+
[artwork.description],
|
|
254
|
+
),
|
|
255
|
+
],
|
|
256
|
+
),
|
|
257
|
+
],
|
|
258
|
+
),
|
|
259
|
+
],
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
const artworkDetailView = (artworkId: number, h: HtmlBuilder<Message>): Html =>
|
|
263
|
+
h.div(
|
|
264
|
+
[h.Class('mx-auto max-w-5xl px-6')],
|
|
265
|
+
[
|
|
266
|
+
Option.match(findArtwork(artworkId), {
|
|
267
|
+
onNone: () => missingArtworkView(artworkId, h),
|
|
268
|
+
onSome: artwork => foundArtworkView(artwork, h),
|
|
269
|
+
}),
|
|
270
|
+
],
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
const notFoundView = (path: string, h: HtmlBuilder<Message>): Html =>
|
|
274
|
+
h.div(
|
|
275
|
+
[h.Class('mx-auto max-w-3xl px-6')],
|
|
276
|
+
[
|
|
277
|
+
h.h1([h.Class('text-3xl font-bold text-gray-900')], ['Page not found']),
|
|
278
|
+
h.p(
|
|
279
|
+
[h.Class('mt-2 text-gray-600')],
|
|
280
|
+
[`The path "${path}" does not exist.`],
|
|
281
|
+
),
|
|
282
|
+
h.a(
|
|
283
|
+
[
|
|
284
|
+
h.Href(galleryRouter()),
|
|
285
|
+
h.Class('mt-4 inline-block text-gray-900 underline'),
|
|
286
|
+
],
|
|
287
|
+
['← Back to gallery'],
|
|
288
|
+
),
|
|
289
|
+
],
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
const routeTitle = (route: AppRoute): string =>
|
|
293
|
+
M.value(route).pipe(
|
|
294
|
+
M.tagsExhaustive({
|
|
295
|
+
Gallery: () => 'View Transitions',
|
|
296
|
+
Artwork: ({ artworkId }) =>
|
|
297
|
+
Option.match(findArtwork(artworkId), {
|
|
298
|
+
onNone: () => 'Artwork Not Found | View Transitions',
|
|
299
|
+
onSome: artwork => `${artwork.title} | View Transitions`,
|
|
300
|
+
}),
|
|
301
|
+
NotFound: () => 'Page Not Found | View Transitions',
|
|
302
|
+
}),
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
export const view = (model: Model, h: HtmlBuilder<Message>): Document => {
|
|
306
|
+
const routeContent = M.value(model.route).pipe(
|
|
307
|
+
M.tagsExhaustive({
|
|
308
|
+
Gallery: () => galleryView(model.filterText, h),
|
|
309
|
+
Artwork: ({ artworkId }) => artworkDetailView(artworkId, h),
|
|
310
|
+
NotFound: ({ path }) => notFoundView(path, h),
|
|
311
|
+
}),
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
title: routeTitle(model.route),
|
|
316
|
+
body: h.div(
|
|
317
|
+
[h.Class('min-h-screen bg-gray-50 py-10')],
|
|
318
|
+
[
|
|
319
|
+
// NOTE: naming the header lifts it out of the root snapshot, so it
|
|
320
|
+
// stays a live element the transition never touches. Without a name it
|
|
321
|
+
// rides the page cross-fade and animates on every navigation despite
|
|
322
|
+
// being identical on both routes, which reads as jank.
|
|
323
|
+
h.header(
|
|
324
|
+
[
|
|
325
|
+
h.Class('mx-auto max-w-5xl px-6 pb-8'),
|
|
326
|
+
h.Style({ viewTransitionName: 'page-header' }),
|
|
327
|
+
],
|
|
328
|
+
[
|
|
329
|
+
h.a(
|
|
330
|
+
[
|
|
331
|
+
h.Href(galleryRouter()),
|
|
332
|
+
h.Class('text-2xl font-bold text-gray-900'),
|
|
333
|
+
],
|
|
334
|
+
['Gradient Gallery'],
|
|
335
|
+
),
|
|
336
|
+
h.p(
|
|
337
|
+
[h.Class('mt-1 text-gray-600')],
|
|
338
|
+
[
|
|
339
|
+
'Click an artwork and watch it grow into the detail page. Typing in the filter never animates.',
|
|
340
|
+
],
|
|
341
|
+
),
|
|
342
|
+
],
|
|
343
|
+
),
|
|
344
|
+
h.main([], [routeContent]),
|
|
345
|
+
],
|
|
346
|
+
),
|
|
347
|
+
}
|
|
348
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Schema as S, pipe } from 'effect'
|
|
2
|
+
import { Route } from 'foldkit'
|
|
3
|
+
import { int, literal, r, slash } from 'foldkit/route'
|
|
4
|
+
|
|
5
|
+
export const GalleryRoute = r('Gallery')
|
|
6
|
+
export const ArtworkRoute = r('Artwork', { artworkId: S.Number })
|
|
7
|
+
export const NotFoundRoute = r('NotFound', { path: S.String })
|
|
8
|
+
|
|
9
|
+
export const AppRoute = S.Union([GalleryRoute, ArtworkRoute, NotFoundRoute])
|
|
10
|
+
|
|
11
|
+
export type GalleryRoute = typeof GalleryRoute.Type
|
|
12
|
+
export type ArtworkRoute = typeof ArtworkRoute.Type
|
|
13
|
+
export type NotFoundRoute = typeof NotFoundRoute.Type
|
|
14
|
+
export type AppRoute = typeof AppRoute.Type
|
|
15
|
+
|
|
16
|
+
export const galleryRouter = pipe(Route.root, Route.mapTo(GalleryRoute))
|
|
17
|
+
|
|
18
|
+
export const artworkRouter = pipe(
|
|
19
|
+
literal('artwork'),
|
|
20
|
+
slash(int('artworkId')),
|
|
21
|
+
Route.mapTo(ArtworkRoute),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
const routeParser = Route.oneOf(artworkRouter, galleryRouter)
|
|
25
|
+
|
|
26
|
+
export const urlToAppRoute = Route.parseUrlWithFallback(
|
|
27
|
+
routeParser,
|
|
28
|
+
NotFoundRoute,
|
|
29
|
+
)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Option } from 'effect'
|
|
2
|
+
import { Scene } from 'foldkit'
|
|
3
|
+
import { fromString } from 'foldkit/url'
|
|
4
|
+
import { describe, expect, test } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ArtworkRoute,
|
|
8
|
+
GalleryRoute,
|
|
9
|
+
Message,
|
|
10
|
+
Model,
|
|
11
|
+
NotFoundRoute,
|
|
12
|
+
update,
|
|
13
|
+
view,
|
|
14
|
+
viewTransition,
|
|
15
|
+
} from './main'
|
|
16
|
+
|
|
17
|
+
const gallery = Model.make({ route: GalleryRoute(), filterText: '' })
|
|
18
|
+
const artworkDetail = (artworkId: number) =>
|
|
19
|
+
Model.make({ route: ArtworkRoute({ artworkId }), filterText: '' })
|
|
20
|
+
const notFound = Model.make({
|
|
21
|
+
route: NotFoundRoute({ path: '/nowhere' }),
|
|
22
|
+
filterText: '',
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const FILTER_PLACEHOLDER = 'Filter by title or medium…'
|
|
26
|
+
|
|
27
|
+
describe('gallery', () => {
|
|
28
|
+
test('renders a card for every artwork', () => {
|
|
29
|
+
Scene.scene(
|
|
30
|
+
{ update, view },
|
|
31
|
+
Scene.given(gallery),
|
|
32
|
+
Scene.expect(Scene.text('Dawn Chorus')).toExist(),
|
|
33
|
+
Scene.expect(Scene.text('Deep Water')).toExist(),
|
|
34
|
+
Scene.expect(Scene.text('Pollen')).toExist(),
|
|
35
|
+
)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('typing in the filter narrows the grid', () => {
|
|
39
|
+
Scene.scene(
|
|
40
|
+
{ update, view },
|
|
41
|
+
Scene.given(gallery),
|
|
42
|
+
Scene.type(Scene.placeholder(FILTER_PLACEHOLDER), 'graphite'),
|
|
43
|
+
Scene.expect(Scene.text('Graphite')).toExist(),
|
|
44
|
+
Scene.expect(Scene.text('Dawn Chorus')).toBeAbsent(),
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('a filter with no matches shows the empty state', () => {
|
|
49
|
+
Scene.scene(
|
|
50
|
+
{ update, view },
|
|
51
|
+
Scene.given(gallery),
|
|
52
|
+
Scene.type(Scene.placeholder(FILTER_PLACEHOLDER), 'watercolor'),
|
|
53
|
+
Scene.expect(Scene.text('No artworks match the filter.')).toExist(),
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('artwork detail', () => {
|
|
59
|
+
test('renders the artwork with its description and a back link', () => {
|
|
60
|
+
Scene.scene(
|
|
61
|
+
{ update, view },
|
|
62
|
+
Scene.given(artworkDetail(2)),
|
|
63
|
+
Scene.expect(Scene.text('Deep Water')).toExist(),
|
|
64
|
+
Scene.expect(Scene.role('link', { name: '← Back to gallery' })).toExist(),
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('renders a missing state for an unknown artwork ID', () => {
|
|
69
|
+
Scene.scene(
|
|
70
|
+
{ update, view },
|
|
71
|
+
Scene.given(artworkDetail(999)),
|
|
72
|
+
Scene.expect(Scene.text('Artwork not found')).toExist(),
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('not found', () => {
|
|
78
|
+
test('renders the missing path', () => {
|
|
79
|
+
Scene.scene(
|
|
80
|
+
{ update, view },
|
|
81
|
+
Scene.given(notFound),
|
|
82
|
+
Scene.expect(Scene.text('Page not found')).toExist(),
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('viewTransition', () => {
|
|
88
|
+
const urlFor = (path: string) =>
|
|
89
|
+
Option.getOrThrow(fromString(`http://localhost${path}`))
|
|
90
|
+
|
|
91
|
+
test('transitions to-artwork-detail when the gallery gives way to an artwork', () => {
|
|
92
|
+
const decision = viewTransition({
|
|
93
|
+
previousModel: gallery,
|
|
94
|
+
model: artworkDetail(1),
|
|
95
|
+
message: Message.ChangedUrl({ url: urlFor('/artwork/1') }),
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
expect(decision).toEqual({ types: ['to-artwork-detail'] })
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('transitions to-gallery when an artwork gives way to the gallery', () => {
|
|
102
|
+
const decision = viewTransition({
|
|
103
|
+
previousModel: artworkDetail(1),
|
|
104
|
+
model: gallery,
|
|
105
|
+
message: Message.ChangedUrl({ url: urlFor('/') }),
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
expect(decision).toEqual({ types: ['to-gallery'] })
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('transitions untyped between two artworks', () => {
|
|
112
|
+
const decision = viewTransition({
|
|
113
|
+
previousModel: artworkDetail(1),
|
|
114
|
+
model: artworkDetail(2),
|
|
115
|
+
message: Message.ChangedUrl({ url: urlFor('/artwork/2') }),
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
expect(decision).toBe(true)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('never transitions on filter keystrokes', () => {
|
|
122
|
+
const decision = viewTransition({
|
|
123
|
+
previousModel: gallery,
|
|
124
|
+
model: gallery,
|
|
125
|
+
message: Message.UpdatedFilterText({ filterText: 'g' }),
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
expect(decision).toBe(false)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
/* The morph is the whole show. The header is named so the browser lifts it out
|
|
4
|
+
of the root snapshot and holds it still, the artwork grows from grid card to
|
|
5
|
+
detail hero, and everything else cross-fades underneath it. Nothing slides,
|
|
6
|
+
so nothing competes with the one element the eye is already tracking.
|
|
7
|
+
Reduced-motion users never reach this file: the runtime skips
|
|
8
|
+
startViewTransition entirely. */
|
|
9
|
+
|
|
10
|
+
::view-transition-group(page-header) {
|
|
11
|
+
animation: none;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Staggered rather than cross-faded. Two layouts dissolving through each other
|
|
15
|
+
reads as a double exposure: grid cards sit on top of detail copy for the
|
|
16
|
+
whole overlap. The outgoing page leaves quickly, then the incoming one
|
|
17
|
+
arrives, so there is a beat where only the artwork is moving. */
|
|
18
|
+
::view-transition-old(root) {
|
|
19
|
+
animation-duration: 130ms;
|
|
20
|
+
animation-timing-function: ease-in;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
::view-transition-new(root) {
|
|
24
|
+
/* Overlaps the tail of the outgoing fade by a hair. A clean handoff at the
|
|
25
|
+
same instant leaves a stretch where neither page is drawn and the screen
|
|
26
|
+
washes out; starting slightly early covers it without bringing back the
|
|
27
|
+
double exposure. */
|
|
28
|
+
animation-delay: 100ms;
|
|
29
|
+
animation-duration: 240ms;
|
|
30
|
+
animation-timing-function: ease-out;
|
|
31
|
+
/* Holds the incoming page at its from-state through the delay, instead of
|
|
32
|
+
letting it sit at full opacity while the old page is still leaving. */
|
|
33
|
+
animation-fill-mode: backwards;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* Every card carries a name, so every card becomes its own group and none of
|
|
37
|
+
them are covered by the root fade above. The seven the user did not click
|
|
38
|
+
have no counterpart on the detail page, so they only fade, and without this
|
|
39
|
+
they would drift out on the hero's long timing instead of clearing with the
|
|
40
|
+
page they belong to. For the one card that does have a counterpart, the same
|
|
41
|
+
rule tightens the cross-fade between its two snapshots so the morph reads as
|
|
42
|
+
one solid object rather than two images dissolving through each other. */
|
|
43
|
+
::view-transition-old(.artwork),
|
|
44
|
+
::view-transition-new(.artwork) {
|
|
45
|
+
animation-duration: 160ms;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Both directions ease in and out rather than front-loading. The heavily
|
|
49
|
+
front-loaded curves (ease-out-quint and friends) put most of the travel in
|
|
50
|
+
the first fifth of the duration, so the artwork lunges and then crawls, which
|
|
51
|
+
is what made this read as janky.
|
|
52
|
+
|
|
53
|
+
The directions differ in pace, not in curve: arriving at a detail page is
|
|
54
|
+
unhurried, returning to the grid is brisk. Transition types are what let CSS
|
|
55
|
+
tell the two apart, since the DOM change is otherwise symmetric. */
|
|
56
|
+
:root:active-view-transition-type(to-artwork-detail)::view-transition-group(
|
|
57
|
+
.artwork
|
|
58
|
+
) {
|
|
59
|
+
animation-duration: 480ms;
|
|
60
|
+
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
:root:active-view-transition-type(to-gallery)::view-transition-group(.artwork) {
|
|
64
|
+
animation-duration: 320ms;
|
|
65
|
+
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
66
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "weather-example",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"typecheck": "tsc --noEmit",
|
|
9
|
+
"test": "vitest run"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@effect/platform-browser": "4.0.0-rc.109",
|
|
13
|
+
"@foldkit/ui": "workspace:*",
|
|
14
|
+
"@tailwindcss/vite": "^4.3.1",
|
|
15
|
+
"effect": "4.0.0-rc.109",
|
|
16
|
+
"foldkit": "workspace:*",
|
|
17
|
+
"tailwindcss": "^4.3.1"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@foldkit/devtools": "workspace:*",
|
|
21
|
+
"@foldkit/vite-plugin": "workspace:*",
|
|
22
|
+
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
|
23
|
+
"happy-dom": "^20.10.4",
|
|
24
|
+
"prettier": "^3.8.4",
|
|
25
|
+
"typescript": "^6.0.3",
|
|
26
|
+
"vite": "^8.0.16",
|
|
27
|
+
"vitest": "^4.1.9"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Runtime } from 'foldkit'
|
|
2
|
+
|
|
3
|
+
import { Message, Model, init, update, view } from './main'
|
|
4
|
+
|
|
5
|
+
const application = Runtime.makeApplication({
|
|
6
|
+
Model,
|
|
7
|
+
init,
|
|
8
|
+
update,
|
|
9
|
+
view,
|
|
10
|
+
container: document.getElementById('root'),
|
|
11
|
+
devTools: {
|
|
12
|
+
Message,
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
Runtime.run(application)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Model, WeatherData } from './main'
|
|
2
|
+
import { WeatherAsyncData } from './main'
|
|
3
|
+
|
|
4
|
+
export const weatherModel: Model = {
|
|
5
|
+
zipCodeInput: '90210',
|
|
6
|
+
weather: WeatherAsyncData.Idle(),
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const weatherData: WeatherData = {
|
|
10
|
+
zipCode: '90210',
|
|
11
|
+
temperature: 72,
|
|
12
|
+
description: 'Clear sky',
|
|
13
|
+
humidity: 45,
|
|
14
|
+
windSpeed: 10,
|
|
15
|
+
locationName: 'Beverly Hills',
|
|
16
|
+
region: 'California',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const mockGeocodingResponse: Readonly<{
|
|
20
|
+
results: ReadonlyArray<
|
|
21
|
+
Readonly<{
|
|
22
|
+
name: string
|
|
23
|
+
latitude: number
|
|
24
|
+
longitude: number
|
|
25
|
+
admin1: string
|
|
26
|
+
}>
|
|
27
|
+
>
|
|
28
|
+
generationtime_ms: number
|
|
29
|
+
}> = {
|
|
30
|
+
results: [
|
|
31
|
+
{
|
|
32
|
+
name: 'Beverly Hills',
|
|
33
|
+
latitude: 34.07362,
|
|
34
|
+
longitude: -118.40036,
|
|
35
|
+
admin1: 'California',
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
generationtime_ms: 0.5,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const mockWeatherResponse: Readonly<{
|
|
42
|
+
current: Readonly<{
|
|
43
|
+
time: string
|
|
44
|
+
interval: number
|
|
45
|
+
temperature_2m: number
|
|
46
|
+
relative_humidity_2m: number
|
|
47
|
+
wind_speed_10m: number
|
|
48
|
+
weather_code: number
|
|
49
|
+
}>
|
|
50
|
+
}> = {
|
|
51
|
+
current: {
|
|
52
|
+
time: '2026-03-10T01:30',
|
|
53
|
+
interval: 900,
|
|
54
|
+
temperature_2m: 72.4,
|
|
55
|
+
relative_humidity_2m: 45,
|
|
56
|
+
wind_speed_10m: 9.8,
|
|
57
|
+
weather_code: 0,
|
|
58
|
+
},
|
|
59
|
+
}
|