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.
Files changed (460) hide show
  1. package/dist/index.js +1 -5
  2. package/{templates → dist/templates}/base/AGENTS.md +17 -19
  3. package/dist/templates/examples/api-cache/package.json +29 -0
  4. package/dist/templates/examples/api-cache/src/data.test.ts +28 -0
  5. package/dist/templates/examples/api-cache/src/data.ts +126 -0
  6. package/dist/templates/examples/api-cache/src/entry.ts +17 -0
  7. package/dist/templates/examples/api-cache/src/main.fixtures.ts +70 -0
  8. package/dist/templates/examples/api-cache/src/main.ts +744 -0
  9. package/dist/templates/examples/api-cache/src/scene.test.ts +120 -0
  10. package/dist/templates/examples/api-cache/src/story.test.ts +288 -0
  11. package/dist/templates/examples/api-cache/src/styles.css +1 -0
  12. package/dist/templates/examples/api-cache/src/vitest-setup.ts +3 -0
  13. package/dist/templates/examples/auth/package.json +30 -0
  14. package/dist/templates/examples/auth/src/command.ts +49 -0
  15. package/dist/templates/examples/auth/src/constant.ts +1 -0
  16. package/dist/templates/examples/auth/src/domain/session.ts +9 -0
  17. package/dist/templates/examples/auth/src/entry.ts +25 -0
  18. package/dist/templates/examples/auth/src/main.ts +75 -0
  19. package/dist/templates/examples/auth/src/message.ts +22 -0
  20. package/dist/templates/examples/auth/src/model.ts +9 -0
  21. package/dist/templates/examples/auth/src/notFoundView.ts +25 -0
  22. package/dist/templates/examples/auth/src/page/index.ts +2 -0
  23. package/dist/templates/examples/auth/src/page/loggedIn/index.ts +4 -0
  24. package/dist/templates/examples/auth/src/page/loggedIn/message.ts +17 -0
  25. package/dist/templates/examples/auth/src/page/loggedIn/model.ts +21 -0
  26. package/dist/templates/examples/auth/src/page/loggedIn/page/dashboard.ts +40 -0
  27. package/dist/templates/examples/auth/src/page/loggedIn/page/settings.ts +71 -0
  28. package/dist/templates/examples/auth/src/page/loggedIn/update.ts +16 -0
  29. package/dist/templates/examples/auth/src/page/loggedIn/view.ts +87 -0
  30. package/dist/templates/examples/auth/src/page/loggedOut/index.ts +4 -0
  31. package/dist/templates/examples/auth/src/page/loggedOut/message.ts +20 -0
  32. package/dist/templates/examples/auth/src/page/loggedOut/model.ts +21 -0
  33. package/dist/templates/examples/auth/src/page/loggedOut/page/home.ts +68 -0
  34. package/dist/templates/examples/auth/src/page/loggedOut/page/login.scene.test.ts +116 -0
  35. package/dist/templates/examples/auth/src/page/loggedOut/page/login.story.test.ts +112 -0
  36. package/dist/templates/examples/auth/src/page/loggedOut/page/login.ts +346 -0
  37. package/dist/templates/examples/auth/src/page/loggedOut/update.ts +32 -0
  38. package/dist/templates/examples/auth/src/page/loggedOut/view.ts +31 -0
  39. package/dist/templates/examples/auth/src/route.ts +56 -0
  40. package/dist/templates/examples/auth/src/scene.test.ts +61 -0
  41. package/dist/templates/examples/auth/src/styles.css +1 -0
  42. package/dist/templates/examples/auth/src/update.ts +179 -0
  43. package/dist/templates/examples/auth/src/view.ts +40 -0
  44. package/dist/templates/examples/auth/src/vitest-setup.ts +3 -0
  45. package/dist/templates/examples/canvas-art/package.json +29 -0
  46. package/dist/templates/examples/canvas-art/src/entry.ts +17 -0
  47. package/dist/templates/examples/canvas-art/src/main.ts +274 -0
  48. package/dist/templates/examples/canvas-art/src/scene.test.ts +70 -0
  49. package/dist/templates/examples/canvas-art/src/story.test.ts +155 -0
  50. package/dist/templates/examples/canvas-art/src/styles.css +1 -0
  51. package/dist/templates/examples/canvas-art/src/vitest-setup.ts +3 -0
  52. package/dist/templates/examples/charting/package.json +31 -0
  53. package/dist/templates/examples/charting/src/chartHost.ts +20 -0
  54. package/dist/templates/examples/charting/src/command.ts +62 -0
  55. package/dist/templates/examples/charting/src/domain.ts +175 -0
  56. package/dist/templates/examples/charting/src/echarts.ts +408 -0
  57. package/dist/templates/examples/charting/src/entry.ts +25 -0
  58. package/dist/templates/examples/charting/src/githubApi.ts +271 -0
  59. package/dist/templates/examples/charting/src/http.ts +50 -0
  60. package/dist/templates/examples/charting/src/init.test.ts +14 -0
  61. package/dist/templates/examples/charting/src/init.ts +29 -0
  62. package/dist/templates/examples/charting/src/main.fixtures.ts +162 -0
  63. package/dist/templates/examples/charting/src/message.ts +23 -0
  64. package/dist/templates/examples/charting/src/model.ts +22 -0
  65. package/dist/templates/examples/charting/src/npmApi.ts +75 -0
  66. package/dist/templates/examples/charting/src/radioGroups.ts +11 -0
  67. package/dist/templates/examples/charting/src/scene.test.ts +103 -0
  68. package/dist/templates/examples/charting/src/story.test.ts +145 -0
  69. package/dist/templates/examples/charting/src/styles.css +1 -0
  70. package/dist/templates/examples/charting/src/subscription.ts +70 -0
  71. package/dist/templates/examples/charting/src/telemetry.test.ts +64 -0
  72. package/dist/templates/examples/charting/src/telemetry.ts +432 -0
  73. package/dist/templates/examples/charting/src/update.ts +217 -0
  74. package/dist/templates/examples/charting/src/view/chart.ts +129 -0
  75. package/dist/templates/examples/charting/src/view/format.ts +23 -0
  76. package/dist/templates/examples/charting/src/view/index.ts +1 -0
  77. package/dist/templates/examples/charting/src/view/layout.ts +175 -0
  78. package/dist/templates/examples/charting/src/view/sidebar.ts +309 -0
  79. package/dist/templates/examples/charting/src/view/view.ts +23 -0
  80. package/dist/templates/examples/charting/src/vitest-setup.ts +3 -0
  81. package/dist/templates/examples/charting/src/weeks.ts +105 -0
  82. package/dist/templates/examples/counter/package.json +29 -0
  83. package/dist/templates/examples/counter/src/entry.ts +16 -0
  84. package/dist/templates/examples/counter/src/main.ts +95 -0
  85. package/dist/templates/examples/counter/src/scene.test.ts +57 -0
  86. package/dist/templates/examples/counter/src/story.test.ts +71 -0
  87. package/dist/templates/examples/counter/src/styles.css +1 -0
  88. package/dist/templates/examples/counter/src/vitest-setup.ts +3 -0
  89. package/dist/templates/examples/counters/package.json +29 -0
  90. package/dist/templates/examples/counters/src/counter.ts +70 -0
  91. package/dist/templates/examples/counters/src/entry.ts +16 -0
  92. package/dist/templates/examples/counters/src/main.ts +171 -0
  93. package/dist/templates/examples/counters/src/scene.test.ts +90 -0
  94. package/dist/templates/examples/counters/src/story.test.ts +127 -0
  95. package/dist/templates/examples/counters/src/styles.css +1 -0
  96. package/dist/templates/examples/counters/src/vitest-setup.ts +3 -0
  97. package/dist/templates/examples/crash-view/package.json +29 -0
  98. package/dist/templates/examples/crash-view/src/entry.ts +28 -0
  99. package/dist/templates/examples/crash-view/src/main.ts +104 -0
  100. package/dist/templates/examples/crash-view/src/scene.test.ts +14 -0
  101. package/dist/templates/examples/crash-view/src/story.test.ts +11 -0
  102. package/dist/templates/examples/crash-view/src/styles.css +1 -0
  103. package/dist/templates/examples/crash-view/src/vitest-setup.ts +3 -0
  104. package/dist/templates/examples/embedding/package.json +29 -0
  105. package/dist/templates/examples/embedding/src/entry.ts +3 -0
  106. package/dist/templates/examples/embedding/src/host.ts +108 -0
  107. package/dist/templates/examples/embedding/src/main.ts +144 -0
  108. package/dist/templates/examples/embedding/src/scene.test.ts +28 -0
  109. package/dist/templates/examples/embedding/src/story.test.ts +64 -0
  110. package/dist/templates/examples/embedding/src/styles.css +1 -0
  111. package/dist/templates/examples/embedding/src/vitest-setup.ts +3 -0
  112. package/dist/templates/examples/form/package.json +30 -0
  113. package/dist/templates/examples/form/src/entry.ts +16 -0
  114. package/dist/templates/examples/form/src/main.ts +493 -0
  115. package/dist/templates/examples/form/src/scene.test.ts +165 -0
  116. package/dist/templates/examples/form/src/story.test.ts +190 -0
  117. package/dist/templates/examples/form/src/styles.css +1 -0
  118. package/dist/templates/examples/form/src/vitest-setup.ts +3 -0
  119. package/dist/templates/examples/generative-art/package.json +29 -0
  120. package/dist/templates/examples/generative-art/src/command.ts +109 -0
  121. package/dist/templates/examples/generative-art/src/constant.ts +68 -0
  122. package/dist/templates/examples/generative-art/src/entry.ts +22 -0
  123. package/dist/templates/examples/generative-art/src/main.ts +59 -0
  124. package/dist/templates/examples/generative-art/src/message.ts +35 -0
  125. package/dist/templates/examples/generative-art/src/model.ts +33 -0
  126. package/dist/templates/examples/generative-art/src/noise.ts +93 -0
  127. package/dist/templates/examples/generative-art/src/scene.test.ts +103 -0
  128. package/dist/templates/examples/generative-art/src/story.test.ts +203 -0
  129. package/dist/templates/examples/generative-art/src/styles.css +1 -0
  130. package/dist/templates/examples/generative-art/src/subscription.ts +35 -0
  131. package/dist/templates/examples/generative-art/src/update.ts +319 -0
  132. package/dist/templates/examples/generative-art/src/view.ts +451 -0
  133. package/dist/templates/examples/generative-art/src/vitest-setup.ts +3 -0
  134. package/dist/templates/examples/interrupting-commands/package.json +29 -0
  135. package/dist/templates/examples/interrupting-commands/src/entry.ts +16 -0
  136. package/dist/templates/examples/interrupting-commands/src/main.ts +354 -0
  137. package/dist/templates/examples/interrupting-commands/src/scene.test.ts +89 -0
  138. package/dist/templates/examples/interrupting-commands/src/story.test.ts +224 -0
  139. package/dist/templates/examples/interrupting-commands/src/styles.css +1 -0
  140. package/dist/templates/examples/interrupting-commands/src/vitest-setup.ts +3 -0
  141. package/dist/templates/examples/job-application/package.json +30 -0
  142. package/dist/templates/examples/job-application/src/command.ts +23 -0
  143. package/dist/templates/examples/job-application/src/domain/index.ts +3 -0
  144. package/dist/templates/examples/job-application/src/domain/proficiencyLevel.ts +11 -0
  145. package/dist/templates/examples/job-application/src/domain/pronounOption.ts +13 -0
  146. package/dist/templates/examples/job-application/src/domain/step.ts +28 -0
  147. package/dist/templates/examples/job-application/src/entry.ts +17 -0
  148. package/dist/templates/examples/job-application/src/main.ts +69 -0
  149. package/dist/templates/examples/job-application/src/message.ts +44 -0
  150. package/dist/templates/examples/job-application/src/model.ts +47 -0
  151. package/dist/templates/examples/job-application/src/scene.test.ts +269 -0
  152. package/dist/templates/examples/job-application/src/step/attachments.ts +111 -0
  153. package/dist/templates/examples/job-application/src/step/coverLetter.ts +34 -0
  154. package/dist/templates/examples/job-application/src/step/education/education.ts +121 -0
  155. package/dist/templates/examples/job-application/src/step/education/entry.ts +168 -0
  156. package/dist/templates/examples/job-application/src/step/education/index.ts +2 -0
  157. package/dist/templates/examples/job-application/src/step/index.ts +6 -0
  158. package/dist/templates/examples/job-application/src/step/personalInfo.ts +305 -0
  159. package/dist/templates/examples/job-application/src/step/skills/entry.ts +126 -0
  160. package/dist/templates/examples/job-application/src/step/skills/index.ts +2 -0
  161. package/dist/templates/examples/job-application/src/step/skills/skills.ts +118 -0
  162. package/dist/templates/examples/job-application/src/step/validation.ts +6 -0
  163. package/dist/templates/examples/job-application/src/step/workHistory/entry.ts +206 -0
  164. package/dist/templates/examples/job-application/src/step/workHistory/index.ts +2 -0
  165. package/dist/templates/examples/job-application/src/step/workHistory/workHistory.ts +121 -0
  166. package/dist/templates/examples/job-application/src/story.test.ts +775 -0
  167. package/dist/templates/examples/job-application/src/styles.css +1 -0
  168. package/dist/templates/examples/job-application/src/update.ts +183 -0
  169. package/dist/templates/examples/job-application/src/view/attachments.ts +215 -0
  170. package/dist/templates/examples/job-application/src/view/coverLetter.ts +73 -0
  171. package/dist/templates/examples/job-application/src/view/datePicker.ts +207 -0
  172. package/dist/templates/examples/job-application/src/view/education.ts +63 -0
  173. package/dist/templates/examples/job-application/src/view/educationEntry.ts +165 -0
  174. package/dist/templates/examples/job-application/src/view/field.ts +183 -0
  175. package/dist/templates/examples/job-application/src/view/format.ts +50 -0
  176. package/dist/templates/examples/job-application/src/view/icon.ts +21 -0
  177. package/dist/templates/examples/job-application/src/view/index.ts +352 -0
  178. package/dist/templates/examples/job-application/src/view/personalInfo.ts +224 -0
  179. package/dist/templates/examples/job-application/src/view/preview.ts +265 -0
  180. package/dist/templates/examples/job-application/src/view/review.ts +388 -0
  181. package/dist/templates/examples/job-application/src/view/skillEntry.ts +102 -0
  182. package/dist/templates/examples/job-application/src/view/skills.ts +49 -0
  183. package/dist/templates/examples/job-application/src/view/stepNav.ts +181 -0
  184. package/dist/templates/examples/job-application/src/view/workEntry.ts +159 -0
  185. package/dist/templates/examples/job-application/src/view/workHistory.ts +55 -0
  186. package/dist/templates/examples/job-application/src/vitest-setup.ts +3 -0
  187. package/dist/templates/examples/kanban/package.json +31 -0
  188. package/dist/templates/examples/kanban/src/command.ts +46 -0
  189. package/dist/templates/examples/kanban/src/constant.ts +149 -0
  190. package/dist/templates/examples/kanban/src/domain/card.ts +10 -0
  191. package/dist/templates/examples/kanban/src/domain/column.ts +115 -0
  192. package/dist/templates/examples/kanban/src/domain/index.ts +2 -0
  193. package/dist/templates/examples/kanban/src/entry.ts +27 -0
  194. package/dist/templates/examples/kanban/src/main.ts +56 -0
  195. package/dist/templates/examples/kanban/src/message.ts +21 -0
  196. package/dist/templates/examples/kanban/src/model.ts +21 -0
  197. package/dist/templates/examples/kanban/src/scene.test.ts +161 -0
  198. package/dist/templates/examples/kanban/src/story.test.ts +461 -0
  199. package/dist/templates/examples/kanban/src/styles.css +1 -0
  200. package/dist/templates/examples/kanban/src/subscription.ts +16 -0
  201. package/dist/templates/examples/kanban/src/update.ts +205 -0
  202. package/dist/templates/examples/kanban/src/view/card.ts +79 -0
  203. package/dist/templates/examples/kanban/src/view/column.ts +283 -0
  204. package/dist/templates/examples/kanban/src/view/index.ts +71 -0
  205. package/dist/templates/examples/kanban/src/vitest-setup.ts +3 -0
  206. package/dist/templates/examples/managed-resource-layer/package.json +29 -0
  207. package/dist/templates/examples/managed-resource-layer/src/entry.ts +17 -0
  208. package/dist/templates/examples/managed-resource-layer/src/main.ts +309 -0
  209. package/dist/templates/examples/managed-resource-layer/src/scene.test.ts +117 -0
  210. package/dist/templates/examples/managed-resource-layer/src/story.test.ts +118 -0
  211. package/dist/templates/examples/managed-resource-layer/src/styles.css +1 -0
  212. package/dist/templates/examples/managed-resource-layer/src/vitest-setup.ts +3 -0
  213. package/dist/templates/examples/map/package.json +31 -0
  214. package/dist/templates/examples/map/src/entry.ts +17 -0
  215. package/dist/templates/examples/map/src/locations.ts +69 -0
  216. package/dist/templates/examples/map/src/main.fixtures.ts +28 -0
  217. package/dist/templates/examples/map/src/main.ts +781 -0
  218. package/dist/templates/examples/map/src/mapHost.ts +23 -0
  219. package/dist/templates/examples/map/src/scene.test.ts +136 -0
  220. package/dist/templates/examples/map/src/story.test.ts +190 -0
  221. package/dist/templates/examples/map/src/styles.css +2 -0
  222. package/dist/templates/examples/map/src/vitest-setup.ts +3 -0
  223. package/dist/templates/examples/pixel-art/package.json +31 -0
  224. package/dist/templates/examples/pixel-art/src/command.ts +87 -0
  225. package/dist/templates/examples/pixel-art/src/comparison.bench.test.ts +204 -0
  226. package/dist/templates/examples/pixel-art/src/constant.ts +21 -0
  227. package/dist/templates/examples/pixel-art/src/entry.ts +27 -0
  228. package/dist/templates/examples/pixel-art/src/grid.ts +134 -0
  229. package/dist/templates/examples/pixel-art/src/main.bench.ts +133 -0
  230. package/dist/templates/examples/pixel-art/src/main.ts +85 -0
  231. package/dist/templates/examples/pixel-art/src/message.ts +36 -0
  232. package/dist/templates/examples/pixel-art/src/model.ts +79 -0
  233. package/dist/templates/examples/pixel-art/src/palette.ts +134 -0
  234. package/dist/templates/examples/pixel-art/src/scene.test.ts +250 -0
  235. package/dist/templates/examples/pixel-art/src/story.test.ts +482 -0
  236. package/dist/templates/examples/pixel-art/src/styles.css +1 -0
  237. package/dist/templates/examples/pixel-art/src/subscription.ts +61 -0
  238. package/dist/templates/examples/pixel-art/src/update.ts +469 -0
  239. package/dist/templates/examples/pixel-art/src/view/canvas.ts +134 -0
  240. package/dist/templates/examples/pixel-art/src/view/dialog.ts +152 -0
  241. package/dist/templates/examples/pixel-art/src/view/history.ts +214 -0
  242. package/dist/templates/examples/pixel-art/src/view/index.ts +1 -0
  243. package/dist/templates/examples/pixel-art/src/view/toolbar.ts +452 -0
  244. package/dist/templates/examples/pixel-art/src/view/view.ts +183 -0
  245. package/dist/templates/examples/pixel-art/src/vitest-setup.ts +3 -0
  246. package/dist/templates/examples/query-sync/package.json +30 -0
  247. package/dist/templates/examples/query-sync/src/data.ts +166 -0
  248. package/dist/templates/examples/query-sync/src/entry.ts +20 -0
  249. package/dist/templates/examples/query-sync/src/main.ts +898 -0
  250. package/dist/templates/examples/query-sync/src/scene.test.ts +89 -0
  251. package/dist/templates/examples/query-sync/src/story.test.ts +151 -0
  252. package/dist/templates/examples/query-sync/src/styles.css +1 -0
  253. package/dist/templates/examples/query-sync/src/vitest-setup.ts +3 -0
  254. package/dist/templates/examples/route-transitions/package.json +28 -0
  255. package/dist/templates/examples/route-transitions/src/data.ts +61 -0
  256. package/dist/templates/examples/route-transitions/src/entry.ts +20 -0
  257. package/dist/templates/examples/route-transitions/src/main.ts +798 -0
  258. package/dist/templates/examples/route-transitions/src/route.ts +48 -0
  259. package/dist/templates/examples/route-transitions/src/story.test.ts +225 -0
  260. package/dist/templates/examples/route-transitions/src/styles.css +1 -0
  261. package/dist/templates/examples/route-transitions/src/vitest-setup.ts +3 -0
  262. package/dist/templates/examples/routing/package.json +29 -0
  263. package/dist/templates/examples/routing/src/entry.ts +20 -0
  264. package/dist/templates/examples/routing/src/fileTree.ts +98 -0
  265. package/dist/templates/examples/routing/src/main.ts +573 -0
  266. package/dist/templates/examples/routing/src/page/index.ts +1 -0
  267. package/dist/templates/examples/routing/src/page/people.ts +349 -0
  268. package/dist/templates/examples/routing/src/route.ts +82 -0
  269. package/dist/templates/examples/routing/src/scene.test.ts +180 -0
  270. package/dist/templates/examples/routing/src/story.test.ts +242 -0
  271. package/dist/templates/examples/routing/src/styles.css +1 -0
  272. package/dist/templates/examples/routing/src/vitest-setup.ts +3 -0
  273. package/dist/templates/examples/shopping-cart/package.json +29 -0
  274. package/dist/templates/examples/shopping-cart/src/data/products.ts +10 -0
  275. package/dist/templates/examples/shopping-cart/src/domain/cart.ts +77 -0
  276. package/dist/templates/examples/shopping-cart/src/domain/index.ts +2 -0
  277. package/dist/templates/examples/shopping-cart/src/domain/item.ts +16 -0
  278. package/dist/templates/examples/shopping-cart/src/entry.ts +20 -0
  279. package/dist/templates/examples/shopping-cart/src/main.ts +307 -0
  280. package/dist/templates/examples/shopping-cart/src/page/cart.ts +203 -0
  281. package/dist/templates/examples/shopping-cart/src/page/checkout.ts +205 -0
  282. package/dist/templates/examples/shopping-cart/src/page/index.ts +3 -0
  283. package/dist/templates/examples/shopping-cart/src/page/products.ts +267 -0
  284. package/dist/templates/examples/shopping-cart/src/route.ts +43 -0
  285. package/dist/templates/examples/shopping-cart/src/scene.test.ts +138 -0
  286. package/dist/templates/examples/shopping-cart/src/story.test.ts +233 -0
  287. package/dist/templates/examples/shopping-cart/src/styles.css +1 -0
  288. package/dist/templates/examples/shopping-cart/src/vitest-setup.ts +3 -0
  289. package/dist/templates/examples/slow-warnings/package.json +28 -0
  290. package/dist/templates/examples/slow-warnings/src/entry.ts +29 -0
  291. package/dist/templates/examples/slow-warnings/src/handleSlow.test.ts +43 -0
  292. package/dist/templates/examples/slow-warnings/src/main.ts +574 -0
  293. package/dist/templates/examples/slow-warnings/src/scene.test.ts +57 -0
  294. package/dist/templates/examples/slow-warnings/src/story.test.ts +118 -0
  295. package/dist/templates/examples/slow-warnings/src/styles.css +1 -0
  296. package/dist/templates/examples/slow-warnings/src/vitest-setup.ts +3 -0
  297. package/dist/templates/examples/snake/package.json +28 -0
  298. package/dist/templates/examples/snake/src/constants.ts +11 -0
  299. package/dist/templates/examples/snake/src/domain/apple.ts +22 -0
  300. package/dist/templates/examples/snake/src/domain/direction.ts +17 -0
  301. package/dist/templates/examples/snake/src/domain/index.ts +4 -0
  302. package/dist/templates/examples/snake/src/domain/position.ts +32 -0
  303. package/dist/templates/examples/snake/src/domain/snake.ts +43 -0
  304. package/dist/templates/examples/snake/src/entry.ts +17 -0
  305. package/dist/templates/examples/snake/src/main.ts +345 -0
  306. package/dist/templates/examples/snake/src/scene.test.ts +77 -0
  307. package/dist/templates/examples/snake/src/story.test.ts +165 -0
  308. package/dist/templates/examples/snake/src/styles.css +1 -0
  309. package/dist/templates/examples/snake/src/vitest-setup.ts +3 -0
  310. package/dist/templates/examples/ssg/package.json +29 -0
  311. package/dist/templates/examples/ssg/src/entry.server.ts +14 -0
  312. package/{templates/rendering → dist/templates/examples}/ssg/src/entry.ts +3 -11
  313. package/dist/templates/examples/ssg/src/main.ts +163 -0
  314. package/dist/templates/examples/ssg/src/styles.css +1 -0
  315. package/dist/templates/examples/ssr/package.json +33 -0
  316. package/dist/templates/examples/ssr/src/cookie.ts +13 -0
  317. package/dist/templates/examples/ssr/src/entry.server.ts +51 -0
  318. package/dist/templates/examples/ssr/src/main.ts +170 -0
  319. package/{templates/rendering → dist/templates/examples}/ssr/src/scene.test.ts +5 -8
  320. package/dist/templates/examples/ssr/src/styles.css +1 -0
  321. package/dist/templates/examples/ssr/src/vitest-setup.ts +3 -0
  322. package/dist/templates/examples/state-machine/package.json +30 -0
  323. package/dist/templates/examples/state-machine/src/entry.ts +17 -0
  324. package/dist/templates/examples/state-machine/src/icon.ts +51 -0
  325. package/dist/templates/examples/state-machine/src/main.ts +420 -0
  326. package/dist/templates/examples/state-machine/src/scene.test.ts +85 -0
  327. package/dist/templates/examples/state-machine/src/story.test.ts +199 -0
  328. package/dist/templates/examples/state-machine/src/styles.css +53 -0
  329. package/dist/templates/examples/state-machine/src/view.ts +1675 -0
  330. package/dist/templates/examples/state-machine/src/vitest-setup.ts +3 -0
  331. package/dist/templates/examples/stopwatch/package.json +29 -0
  332. package/dist/templates/examples/stopwatch/src/entry.ts +17 -0
  333. package/dist/templates/examples/stopwatch/src/main.ts +237 -0
  334. package/dist/templates/examples/stopwatch/src/scene.test.ts +124 -0
  335. package/dist/templates/examples/stopwatch/src/story.test.ts +126 -0
  336. package/dist/templates/examples/stopwatch/src/styles.css +1 -0
  337. package/dist/templates/examples/stopwatch/src/vitest-setup.ts +3 -0
  338. package/dist/templates/examples/todo/package.json +30 -0
  339. package/dist/templates/examples/todo/src/entry.ts +17 -0
  340. package/dist/templates/examples/todo/src/main.ts +682 -0
  341. package/dist/templates/examples/todo/src/scene.test.ts +177 -0
  342. package/dist/templates/examples/todo/src/story.test.ts +374 -0
  343. package/dist/templates/examples/todo/src/styles.css +1 -0
  344. package/dist/templates/examples/todo/src/vitest-setup.ts +3 -0
  345. package/dist/templates/examples/ui-showcase/package.json +30 -0
  346. package/dist/templates/examples/ui-showcase/src/entry.ts +31 -0
  347. package/dist/templates/examples/ui-showcase/src/icon.ts +64 -0
  348. package/dist/templates/examples/ui-showcase/src/main.ts +673 -0
  349. package/dist/templates/examples/ui-showcase/src/scene.test.ts +102 -0
  350. package/dist/templates/examples/ui-showcase/src/story.test.ts +128 -0
  351. package/dist/templates/examples/ui-showcase/src/styles.css +21 -0
  352. package/dist/templates/examples/ui-showcase/src/ui/init.ts +185 -0
  353. package/dist/templates/examples/ui-showcase/src/ui/message.ts +164 -0
  354. package/dist/templates/examples/ui-showcase/src/ui/model.ts +138 -0
  355. package/dist/templates/examples/ui-showcase/src/ui/subscriptions.ts +55 -0
  356. package/dist/templates/examples/ui-showcase/src/ui/toast.ts +13 -0
  357. package/dist/templates/examples/ui-showcase/src/ui/update.ts +1234 -0
  358. package/dist/templates/examples/ui-showcase/src/ui/view/animation.ts +58 -0
  359. package/dist/templates/examples/ui-showcase/src/ui/view/button.ts +64 -0
  360. package/dist/templates/examples/ui-showcase/src/ui/view/calendar.ts +202 -0
  361. package/dist/templates/examples/ui-showcase/src/ui/view/checkbox.ts +185 -0
  362. package/dist/templates/examples/ui-showcase/src/ui/view/combobox.ts +326 -0
  363. package/dist/templates/examples/ui-showcase/src/ui/view/datePicker.ts +263 -0
  364. package/dist/templates/examples/ui-showcase/src/ui/view/dialog.ts +427 -0
  365. package/dist/templates/examples/ui-showcase/src/ui/view/disclosure.ts +142 -0
  366. package/dist/templates/examples/ui-showcase/src/ui/view/dragAndDrop.ts +230 -0
  367. package/dist/templates/examples/ui-showcase/src/ui/view/fieldset.ts +284 -0
  368. package/dist/templates/examples/ui-showcase/src/ui/view/fileDrop.ts +119 -0
  369. package/dist/templates/examples/ui-showcase/src/ui/view/index.ts +24 -0
  370. package/dist/templates/examples/ui-showcase/src/ui/view/input.ts +90 -0
  371. package/dist/templates/examples/ui-showcase/src/ui/view/listbox.ts +263 -0
  372. package/dist/templates/examples/ui-showcase/src/ui/view/menu.ts +172 -0
  373. package/dist/templates/examples/ui-showcase/src/ui/view/popover.ts +262 -0
  374. package/dist/templates/examples/ui-showcase/src/ui/view/radioGroup.ts +192 -0
  375. package/dist/templates/examples/ui-showcase/src/ui/view/select.ts +129 -0
  376. package/dist/templates/examples/ui-showcase/src/ui/view/slider.ts +135 -0
  377. package/dist/templates/examples/ui-showcase/src/ui/view/switch.ts +82 -0
  378. package/dist/templates/examples/ui-showcase/src/ui/view/tabs.ts +187 -0
  379. package/dist/templates/examples/ui-showcase/src/ui/view/textarea.ts +99 -0
  380. package/dist/templates/examples/ui-showcase/src/ui/view/toast.ts +165 -0
  381. package/dist/templates/examples/ui-showcase/src/ui/view/tooltip.ts +123 -0
  382. package/dist/templates/examples/ui-showcase/src/ui/view/virtualList.ts +361 -0
  383. package/dist/templates/examples/ui-showcase/src/vitest-setup.ts +3 -0
  384. package/dist/templates/examples/view-transitions/package.json +28 -0
  385. package/dist/templates/examples/view-transitions/src/artwork.ts +80 -0
  386. package/dist/templates/examples/view-transitions/src/entry.ts +21 -0
  387. package/dist/templates/examples/view-transitions/src/main.ts +348 -0
  388. package/dist/templates/examples/view-transitions/src/route.ts +29 -0
  389. package/dist/templates/examples/view-transitions/src/scene.test.ts +130 -0
  390. package/dist/templates/examples/view-transitions/src/styles.css +66 -0
  391. package/dist/templates/examples/view-transitions/src/vitest-setup.ts +3 -0
  392. package/dist/templates/examples/weather/package.json +29 -0
  393. package/dist/templates/examples/weather/src/entry.ts +16 -0
  394. package/dist/templates/examples/weather/src/main.fixtures.ts +59 -0
  395. package/dist/templates/examples/weather/src/main.ts +356 -0
  396. package/dist/templates/examples/weather/src/scene.test.ts +109 -0
  397. package/dist/templates/examples/weather/src/story.test.ts +125 -0
  398. package/dist/templates/examples/weather/src/styles.css +1 -0
  399. package/dist/templates/examples/weather/src/vitest-setup.ts +3 -0
  400. package/dist/templates/examples/web-components/package.json +32 -0
  401. package/dist/templates/examples/web-components/src/entry.ts +16 -0
  402. package/dist/templates/examples/web-components/src/main.ts +327 -0
  403. package/dist/templates/examples/web-components/src/scene.test.ts +90 -0
  404. package/dist/templates/examples/web-components/src/story.test.ts +90 -0
  405. package/dist/templates/examples/web-components/src/styles.css +1 -0
  406. package/dist/templates/examples/web-components/src/vitest-setup.ts +3 -0
  407. package/dist/templates/examples/websocket-chat/package.json +29 -0
  408. package/dist/templates/examples/websocket-chat/src/entry.ts +26 -0
  409. package/dist/templates/examples/websocket-chat/src/main.ts +626 -0
  410. package/dist/templates/examples/websocket-chat/src/scene.test.ts +166 -0
  411. package/dist/templates/examples/websocket-chat/src/story.test.ts +183 -0
  412. package/dist/templates/examples/websocket-chat/src/styles.css +1 -0
  413. package/dist/templates/examples/websocket-chat/src/vitest-setup.ts +3 -0
  414. package/dist/templates/release.json +27 -0
  415. package/dist/templates/rendering/ssg/src/entry.ts +20 -0
  416. package/{templates → dist/templates}/rendering/ssg/src/main.ts +36 -43
  417. package/dist/templates/rendering/ssg/src/route.ts +20 -0
  418. package/dist/templates/rendering/ssg/src/vite-env.d.ts +19 -0
  419. package/dist/templates/rendering/ssr/src/entry.ts +17 -0
  420. package/{templates → dist/templates}/rendering/ssr/src/main.ts +17 -25
  421. package/dist/templates/rendering/ssr/src/scene.test.ts +40 -0
  422. package/dist/templates/rendering/ssr/src/vite-env.d.ts +19 -0
  423. package/dist/utils/files.js +23 -71
  424. package/dist/utils/packages.js +63 -44
  425. package/package.json +3 -4
  426. /package/{templates → dist/templates}/base/.mcp.json +0 -0
  427. /package/{templates → dist/templates}/base/.oxlintrc.json +0 -0
  428. /package/{templates → dist/templates}/base/.prettierignore +0 -0
  429. /package/{templates → dist/templates}/base/.prettierrc +0 -0
  430. /package/{templates → dist/templates}/base/README.md +0 -0
  431. /package/{templates → dist/templates}/base/gitignore +0 -0
  432. /package/{templates → dist/templates}/base/ignore +0 -0
  433. /package/{templates → dist/templates}/base/index.html +0 -0
  434. /package/{templates → dist/templates}/base/package.json +0 -0
  435. /package/{templates → dist/templates}/base/src/styles.css +0 -0
  436. /package/{templates → dist/templates}/base/src/vitest-setup.ts +0 -0
  437. /package/{templates → dist/templates}/base/tsconfig.json +0 -0
  438. /package/{templates → dist/templates}/base/vite.config.ts +0 -0
  439. /package/{templates → dist/templates}/base/vitest.config.ts +0 -0
  440. /package/{templates/rendering → dist/templates/examples}/ssg/src/route.ts +0 -0
  441. /package/{templates/rendering → dist/templates/examples}/ssg/src/vite-env.d.ts +0 -0
  442. /package/{templates/rendering → dist/templates/examples}/ssr/src/entry.ts +0 -0
  443. /package/{templates/rendering → dist/templates/examples}/ssr/src/vite-env.d.ts +0 -0
  444. /package/{templates → dist/templates}/package-managers/pnpm/pnpm-workspace.yaml +0 -0
  445. /package/{templates → dist/templates}/rendering/ssg/README.md +0 -0
  446. /package/{templates → dist/templates}/rendering/ssg/package.json +0 -0
  447. /package/{templates → dist/templates}/rendering/ssg/scripts/build.mjs +0 -0
  448. /package/{templates → dist/templates}/rendering/ssg/scripts/prerender.ts +0 -0
  449. /package/{templates → dist/templates}/rendering/ssg/src/entry.server.ts +0 -0
  450. /package/{templates → dist/templates}/rendering/ssg/src/scene.test.ts +0 -0
  451. /package/{templates → dist/templates}/rendering/ssg/tsconfig.json +0 -0
  452. /package/{templates → dist/templates}/rendering/ssg/vite.config.ts +0 -0
  453. /package/{templates → dist/templates}/rendering/ssr/README.md +0 -0
  454. /package/{templates → dist/templates}/rendering/ssr/package.json +0 -0
  455. /package/{templates → dist/templates}/rendering/ssr/scripts/build.mjs +0 -0
  456. /package/{templates → dist/templates}/rendering/ssr/server/main.ts +0 -0
  457. /package/{templates → dist/templates}/rendering/ssr/src/cookie.ts +0 -0
  458. /package/{templates → dist/templates}/rendering/ssr/src/entry.server.ts +0 -0
  459. /package/{templates → dist/templates}/rendering/ssr/tsconfig.json +0 -0
  460. /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,3 @@
1
+ import { setup } from 'foldkit/test/vitest'
2
+
3
+ setup()
@@ -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
+ }