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,1675 @@
1
+ import { clsx } from 'clsx'
2
+ import { Array, Match as M, Option, pipe } from 'effect'
3
+ import { Document, Html, HtmlBuilder } from 'foldkit/html'
4
+
5
+ import { Button, Checkbox, Input, RadioGroup } from '@foldkit/ui'
6
+
7
+ import * as Icon from './icon'
8
+ import {
9
+ EDITIONS,
10
+ EditionRadioGroup,
11
+ Message,
12
+ checkoutMachine,
13
+ editionName,
14
+ isReviewReady,
15
+ promoToMaybeDiscount,
16
+ } from './main'
17
+ import type {
18
+ Cart,
19
+ CheckoutState,
20
+ Confirmed,
21
+ Discount,
22
+ Model,
23
+ Payment,
24
+ Review,
25
+ } from './main'
26
+
27
+ // VIEW
28
+
29
+ const PHYSICAL_PRICE = 52
30
+ const DIGITAL_PRICE = 28
31
+ const SALES_TAX_RATE = 0.0825
32
+
33
+ const primaryButtonClassName =
34
+ 'inline-flex min-h-12 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-sm bg-stone-900 px-5 py-3 text-sm font-semibold text-white transition-colors hover:bg-orange-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-stone-900 disabled:cursor-not-allowed disabled:bg-stone-300 disabled:text-stone-500'
35
+
36
+ const secondaryButtonClassName =
37
+ 'inline-flex min-h-12 cursor-pointer items-center justify-center whitespace-nowrap rounded-sm border border-stone-400 bg-transparent px-5 py-3 text-sm font-semibold text-stone-900 transition-colors hover:border-stone-900 hover:bg-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-stone-900'
38
+
39
+ const promoFeedbackClassName =
40
+ 'min-h-4 text-xs font-bold uppercase tracking-wider'
41
+
42
+ const cancelButtonClassName =
43
+ 'cursor-pointer text-sm font-medium text-stone-600 underline decoration-stone-400 underline-offset-4 transition-colors hover:text-orange-800 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-stone-900'
44
+
45
+ const stateEyebrow = (state: typeof CheckoutState.Type): string =>
46
+ M.value(state).pipe(
47
+ M.tagsExhaustive({
48
+ Cart: () => 'Bag',
49
+ Shipping: () => 'Delivery',
50
+ Payment: () => 'Payment',
51
+ Review: () => 'Review',
52
+ Placing: () => 'Order',
53
+ Confirmed: () => 'Order',
54
+ Cancelled: () => 'Checkout',
55
+ }),
56
+ )
57
+
58
+ const stateTitle = (state: typeof CheckoutState.Type): string =>
59
+ M.value(state).pipe(
60
+ M.tagsExhaustive({
61
+ Cart: () => 'Your order',
62
+ Shipping: () => 'Delivery details',
63
+ Payment: () => 'Payment',
64
+ Review: () => 'Review and place order',
65
+ Placing: () => 'Processing your order',
66
+ Confirmed: () => 'Thank you',
67
+ Cancelled: () => 'Checkout cancelled',
68
+ }),
69
+ )
70
+
71
+ const stateDescription = (state: typeof CheckoutState.Type): string =>
72
+ M.value(state).pipe(
73
+ M.tagsExhaustive({
74
+ Cart: () => 'Choose your edition before continuing to checkout.',
75
+ Shipping: () => 'Confirm where we should send your hardcover.',
76
+ Payment: () => 'Select a saved payment method for this order.',
77
+ Review: () => 'Check the details below before placing your order.',
78
+ Placing: () => 'Please keep this page open for a moment.',
79
+ Confirmed: () => 'Your order is confirmed and a receipt is on its way.',
80
+ Cancelled: () => 'Nothing was charged. Your selection has been saved.',
81
+ }),
82
+ )
83
+
84
+ const progressSteps: ReadonlyArray<string> = [
85
+ 'Bag',
86
+ 'Delivery',
87
+ 'Payment',
88
+ 'Review',
89
+ ]
90
+
91
+ const stateToMaybeProgressStep = (
92
+ state: typeof CheckoutState.Type,
93
+ ): Option.Option<string> =>
94
+ M.value(state).pipe(
95
+ M.tags({
96
+ Cart: () => 'Bag',
97
+ Shipping: () => 'Delivery',
98
+ Payment: () => 'Payment',
99
+ Review: () => 'Review',
100
+ Cancelled: () => 'Bag',
101
+ }),
102
+ M.option,
103
+ )
104
+
105
+ const currentProgressIndex = (state: typeof CheckoutState.Type): number =>
106
+ pipe(
107
+ stateToMaybeProgressStep(state),
108
+ Option.flatMap(progressStep =>
109
+ Array.findFirstIndex(progressSteps, step => step === progressStep),
110
+ ),
111
+ Option.getOrElse(() => progressSteps.length),
112
+ )
113
+
114
+ const editionPrice = (edition: string): number =>
115
+ edition === 'Hardcover' ? PHYSICAL_PRICE : DIGITAL_PRICE
116
+
117
+ const editionDescription = (edition: string): string =>
118
+ edition === 'Hardcover'
119
+ ? 'Clothbound, 248 pages. Ships in 1–2 business days.'
120
+ : 'PDF and EPUB files delivered by email after purchase.'
121
+
122
+ const formatMoney = (amount: number): string => `$${amount.toFixed(2)}`
123
+
124
+ const roundToCents = (amount: number): number => Math.round(amount * 100) / 100
125
+
126
+ const orderPricing = (
127
+ isShippingRequired: boolean,
128
+ maybeDiscount: Option.Option<typeof Discount.Type>,
129
+ ) => {
130
+ const subtotal = isShippingRequired ? PHYSICAL_PRICE : DIGITAL_PRICE
131
+ const maybeAppliedDiscount = Option.map(maybeDiscount, discount => ({
132
+ code: discount.code,
133
+ amount: roundToCents((subtotal * discount.percentOff) / 100),
134
+ }))
135
+ const discountedSubtotal =
136
+ subtotal -
137
+ Option.match(maybeAppliedDiscount, {
138
+ onNone: () => 0,
139
+ onSome: appliedDiscount => appliedDiscount.amount,
140
+ })
141
+ const tax = roundToCents(discountedSubtotal * SALES_TAX_RATE)
142
+
143
+ return {
144
+ subtotal,
145
+ maybeAppliedDiscount,
146
+ tax,
147
+ total: discountedSubtotal + tax,
148
+ }
149
+ }
150
+
151
+ const stateToMaybeDiscount = (
152
+ state: typeof CheckoutState.Type,
153
+ ): Option.Option<typeof Discount.Type> =>
154
+ M.value(state).pipe(
155
+ M.tags({
156
+ Review: reviewState => promoToMaybeDiscount(reviewState.promo),
157
+ Placing: placingState => placingState.maybeDiscount,
158
+ Confirmed: confirmedState => confirmedState.maybeDiscount,
159
+ }),
160
+ M.option,
161
+ Option.flatten,
162
+ )
163
+
164
+ const bookCoverView = (
165
+ className: string,
166
+ isCompact: boolean,
167
+ h: HtmlBuilder<Message>,
168
+ ): Html => {
169
+ const coverClassName = clsx(
170
+ 'relative grid content-start overflow-hidden bg-orange-800 text-orange-50',
171
+ className,
172
+ {
173
+ 'h-24 w-16 pt-2.5 pr-2 pb-2 pl-3 shadow-lg': isCompact,
174
+ 'h-40 w-30 p-4 pl-6 shadow-xl': !isCompact,
175
+ },
176
+ )
177
+ const ruleClassName = clsx('w-1/4 bg-current', {
178
+ 'mb-1.5 h-px': isCompact,
179
+ 'mb-3 h-0.5': !isCompact,
180
+ })
181
+ const eyebrowClassName = clsx('font-extrabold uppercase tracking-widest', {
182
+ hidden: isCompact,
183
+ 'origin-left scale-75 text-xs': !isCompact,
184
+ })
185
+ const titleClassName = clsx('font-serif leading-none tracking-tighter', {
186
+ 'mt-1 text-xs': isCompact,
187
+ 'mt-2 text-base': !isCompact,
188
+ })
189
+ const authorClassName = clsx('mt-auto font-bold uppercase tracking-wider', {
190
+ hidden: isCompact,
191
+ 'origin-left scale-75 text-xs': !isCompact,
192
+ })
193
+
194
+ return h.div(
195
+ [h.AriaHidden(true), h.Class(coverClassName)],
196
+ [
197
+ h.div([h.Class('absolute inset-y-0 left-3 w-px bg-orange-100/30')]),
198
+ h.div([
199
+ h.Class(
200
+ 'absolute inset-x-0 bottom-0 h-1 border-t border-stone-900/25 bg-orange-50',
201
+ ),
202
+ ]),
203
+ h.div([h.Class(ruleClassName)]),
204
+ h.div([h.Class(eyebrowClassName)], ['Signal Press']),
205
+ h.div(
206
+ [h.Class(titleClassName)],
207
+ ['Designing', h.br([]), 'Durable', h.br([]), 'Interfaces'],
208
+ ),
209
+ h.div([h.Class(authorClassName)], ['Mira Chen']),
210
+ ],
211
+ )
212
+ }
213
+
214
+ const pageHeaderView = (h: HtmlBuilder<Message>): Html =>
215
+ h.header(
216
+ [h.Class('border-b border-stone-300 bg-stone-50')],
217
+ [
218
+ h.div(
219
+ [
220
+ h.Class(
221
+ 'mx-auto flex max-w-6xl items-center justify-between px-5 py-4 sm:px-8',
222
+ ),
223
+ ],
224
+ [
225
+ h.div(
226
+ [h.Class('flex items-center gap-3')],
227
+ [
228
+ h.div(
229
+ [
230
+ h.AriaHidden(true),
231
+ h.Class(
232
+ 'grid h-9 w-9 place-items-center bg-stone-900 text-xs font-bold tracking-widest text-white',
233
+ ),
234
+ ],
235
+ ['SP'],
236
+ ),
237
+ h.div(
238
+ [],
239
+ [
240
+ h.div(
241
+ [
242
+ h.Class(
243
+ 'text-sm font-bold uppercase tracking-widest text-stone-900',
244
+ ),
245
+ ],
246
+ ['Signal Press'],
247
+ ),
248
+ h.div(
249
+ [h.Class('hidden text-xs text-stone-600 sm:block')],
250
+ ['Independent books on design and technology'],
251
+ ),
252
+ ],
253
+ ),
254
+ ],
255
+ ),
256
+ h.div(
257
+ [
258
+ h.Class(
259
+ 'flex items-center gap-2 text-xs font-medium text-stone-600',
260
+ ),
261
+ ],
262
+ [Icon.lockClosed(), 'Secure checkout'],
263
+ ),
264
+ ],
265
+ ),
266
+ ],
267
+ )
268
+
269
+ const progressIndicatorClassName = (
270
+ isActive: boolean,
271
+ isComplete: boolean,
272
+ isSkipped: boolean,
273
+ ): string => {
274
+ const isCompleteOrSkipped = isComplete || isSkipped
275
+
276
+ return clsx(
277
+ 'grid h-6 w-6 place-items-center rounded-full border-2 sm:h-7 sm:w-7',
278
+ {
279
+ 'border-stone-900 bg-stone-900 text-white': isCompleteOrSkipped,
280
+ 'border-stone-900 bg-stone-50 text-stone-900':
281
+ !isCompleteOrSkipped && isActive,
282
+ 'border-stone-300 bg-stone-50 text-stone-600':
283
+ !isCompleteOrSkipped && !isActive,
284
+ },
285
+ )
286
+ }
287
+
288
+ const progressView = (
289
+ state: typeof CheckoutState.Type,
290
+ h: HtmlBuilder<Message>,
291
+ ): Html => {
292
+ const progressIndex = currentProgressIndex(state)
293
+
294
+ return h.nav(
295
+ [h.AriaLabel('Checkout progress'), h.Class('w-full pt-7 sm:pt-9')],
296
+ [
297
+ h.ol(
298
+ [h.Class('grid grid-cols-4 gap-2 sm:gap-5')],
299
+ Array.map(progressSteps, (stepLabel, index) => {
300
+ const isActive = progressIndex === index
301
+ const isComplete = progressIndex > index
302
+ const isSkipped =
303
+ stepLabel === 'Delivery' &&
304
+ !state.isShippingRequired &&
305
+ progressIndex > index
306
+ const indicatorClassName = progressIndicatorClassName(
307
+ isActive,
308
+ isComplete,
309
+ isSkipped,
310
+ )
311
+
312
+ return h.keyed('li')(
313
+ stepLabel,
314
+ [h.Class('min-w-0 border-t border-stone-300 pt-3')],
315
+ [
316
+ h.div(
317
+ [h.Class('flex items-center gap-1 sm:gap-2')],
318
+ [
319
+ h.span(
320
+ [h.Class(indicatorClassName)],
321
+ [
322
+ isComplete || isSkipped
323
+ ? Icon.check('h-3.5 w-3.5')
324
+ : String(index + 1),
325
+ ],
326
+ ),
327
+ h.span(
328
+ [
329
+ h.Class(
330
+ clsx('truncate text-xs sm:text-sm', {
331
+ 'font-bold uppercase tracking-wider text-stone-900':
332
+ isActive,
333
+ 'font-medium text-stone-600': !isActive,
334
+ }),
335
+ ),
336
+ ],
337
+ [isSkipped ? `${stepLabel} skipped` : stepLabel],
338
+ ),
339
+ ],
340
+ ),
341
+ ],
342
+ )
343
+ }),
344
+ ),
345
+ ],
346
+ )
347
+ }
348
+
349
+ const editionOptionView = (
350
+ option: RadioGroup.OptionInfo,
351
+ h: HtmlBuilder<Message>,
352
+ ): Html => {
353
+ const selectionBadgeClassName =
354
+ 'mt-2 inline-flex text-xs font-bold uppercase tracking-wider'
355
+
356
+ return h.keyed('button')(
357
+ option.value,
358
+ [
359
+ ...option.option,
360
+ h.Class(
361
+ clsx(
362
+ 'relative cursor-pointer border-2 p-4 text-left transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-stone-900',
363
+ {
364
+ 'border-stone-900 bg-white': option.isSelected,
365
+ 'border-stone-300 bg-stone-50 hover:border-stone-500 hover:bg-white':
366
+ !option.isSelected,
367
+ },
368
+ ),
369
+ ),
370
+ ],
371
+ [
372
+ h.div(
373
+ [h.Class('flex items-start justify-between gap-4')],
374
+ [
375
+ h.div(
376
+ [],
377
+ [
378
+ h.div(
379
+ [...option.label, h.Class('text-sm font-bold text-stone-900')],
380
+ [option.value],
381
+ ),
382
+ h.p(
383
+ [
384
+ ...option.description,
385
+ h.Class('mt-1 text-xs leading-5 text-stone-600'),
386
+ ],
387
+ [editionDescription(option.value)],
388
+ ),
389
+ ],
390
+ ),
391
+ h.div(
392
+ [h.Class('text-right')],
393
+ [
394
+ h.div(
395
+ [h.Class('text-sm font-bold text-stone-900')],
396
+ [formatMoney(editionPrice(option.value))],
397
+ ),
398
+ option.isSelected
399
+ ? h.span(
400
+ [
401
+ h.Class(
402
+ clsx(
403
+ selectionBadgeClassName,
404
+ 'items-center gap-1 text-orange-800',
405
+ ),
406
+ ),
407
+ ],
408
+ [Icon.check('h-3 w-3'), 'Selected'],
409
+ )
410
+ : h.span(
411
+ [h.Class(clsx(selectionBadgeClassName, 'text-stone-600'))],
412
+ ['Choose'],
413
+ ),
414
+ ],
415
+ ),
416
+ ],
417
+ ),
418
+ ],
419
+ )
420
+ }
421
+
422
+ const cancelCheckoutButton = (h: HtmlBuilder<Message>): Html =>
423
+ Button.view(
424
+ {
425
+ onClick: Message.ClickedCancel(),
426
+ toView: attributes =>
427
+ h.button(
428
+ [...attributes.button, h.Class(cancelButtonClassName)],
429
+ ['Cancel checkout'],
430
+ ),
431
+ },
432
+ h,
433
+ )
434
+
435
+ const cartView = (
436
+ state: typeof Cart.Type,
437
+ editionRadioGroup: RadioGroup.Model,
438
+ h: HtmlBuilder<Message>,
439
+ ): Html => {
440
+ const continueLabel = state.isShippingRequired
441
+ ? 'Continue to delivery'
442
+ : 'Continue to payment'
443
+
444
+ return h.div(
445
+ [h.Class('grid gap-8')],
446
+ [
447
+ h.div(
448
+ [
449
+ h.Class(
450
+ 'grid gap-6 border-y border-stone-300 py-6 sm:grid-cols-4 sm:items-center',
451
+ ),
452
+ ],
453
+ [
454
+ bookCoverView('mx-auto sm:mx-0', false, h),
455
+ h.div(
456
+ [h.Class('sm:col-span-3')],
457
+ [
458
+ h.div(
459
+ [
460
+ h.Class(
461
+ 'text-xs font-bold uppercase tracking-widest text-orange-800',
462
+ ),
463
+ ],
464
+ ['New release'],
465
+ ),
466
+ h.h2(
467
+ [h.Class('mt-2 font-serif text-2xl text-stone-900')],
468
+ ['Designing Durable Interfaces'],
469
+ ),
470
+ h.p([h.Class('mt-1 text-sm text-stone-600')], ['By Mira Chen']),
471
+ h.p(
472
+ [h.Class('mt-4 max-w-lg text-sm leading-6 text-stone-600')],
473
+ [
474
+ 'A practical field guide to state, feedback, and resilient interaction design.',
475
+ ],
476
+ ),
477
+ ],
478
+ ),
479
+ ],
480
+ ),
481
+ h.div(
482
+ [h.Class('grid gap-3')],
483
+ [
484
+ h.div(
485
+ [h.Class('mb-3 text-sm font-bold text-stone-900')],
486
+ ['Choose an edition'],
487
+ ),
488
+ h.submodel({
489
+ slotId: editionRadioGroup.id,
490
+ model: editionRadioGroup,
491
+ view: EditionRadioGroup.view,
492
+ viewInputs: {
493
+ selectedValue: Option.some(editionName(state.isShippingRequired)),
494
+ options: EDITIONS,
495
+ ariaLabel: 'Choose an edition',
496
+ orientation: 'Horizontal',
497
+ toView: ({ group, options }) =>
498
+ h.div(
499
+ [...group, h.Class('grid gap-3 sm:grid-cols-2')],
500
+ Array.map(options, option => editionOptionView(option, h)),
501
+ ),
502
+ },
503
+ toParentMessage: message =>
504
+ Message.GotEditionRadioGroupMessage({ message }),
505
+ }),
506
+ ],
507
+ ),
508
+ h.div(
509
+ [
510
+ h.Class(
511
+ 'flex flex-col gap-4 border-t border-stone-300 pt-6 sm:flex-row sm:items-center sm:justify-between',
512
+ ),
513
+ ],
514
+ [
515
+ cancelCheckoutButton(h),
516
+ Button.view(
517
+ {
518
+ onClick: Message.ClickedContinue(),
519
+ toView: attributes =>
520
+ h.button(
521
+ [...attributes.button, h.Class(primaryButtonClassName)],
522
+ [continueLabel, Icon.arrowRight()],
523
+ ),
524
+ },
525
+ h,
526
+ ),
527
+ ],
528
+ ),
529
+ ],
530
+ )
531
+ }
532
+
533
+ const shippingView = (h: HtmlBuilder<Message>): Html =>
534
+ h.div(
535
+ [h.Class('grid gap-7')],
536
+ [
537
+ h.section(
538
+ [h.AriaLabel('Contact information')],
539
+ [
540
+ h.div(
541
+ [h.Class('border-b border-stone-300 pb-5')],
542
+ [
543
+ h.div(
544
+ [
545
+ h.Class(
546
+ 'text-xs font-bold uppercase tracking-widest text-stone-600',
547
+ ),
548
+ ],
549
+ ['Contact'],
550
+ ),
551
+ h.div(
552
+ [h.Class('mt-2 text-sm font-medium text-stone-900')],
553
+ ['alex.morgan@example.com'],
554
+ ),
555
+ ],
556
+ ),
557
+ ],
558
+ ),
559
+ h.section(
560
+ [h.AriaLabel('Delivery address')],
561
+ [
562
+ h.div(
563
+ [
564
+ h.Class(
565
+ 'grid gap-4 border-2 border-stone-900 bg-white p-5 sm:grid-cols-4',
566
+ ),
567
+ ],
568
+ [
569
+ h.div(
570
+ [h.Class('sm:col-span-3')],
571
+ [
572
+ h.div(
573
+ [h.Class('flex items-center gap-2')],
574
+ [
575
+ h.span(
576
+ [
577
+ h.Class(
578
+ 'grid h-5 w-5 place-items-center rounded-full bg-stone-900 text-white',
579
+ ),
580
+ ],
581
+ [Icon.check('h-3 w-3')],
582
+ ),
583
+ h.h2(
584
+ [h.Class('text-sm font-bold text-stone-900')],
585
+ ['Saved address'],
586
+ ),
587
+ ],
588
+ ),
589
+ h.address(
590
+ [
591
+ h.Class(
592
+ 'mt-3 text-sm not-italic leading-6 text-stone-600',
593
+ ),
594
+ ],
595
+ [
596
+ 'Alex Morgan',
597
+ h.br([]),
598
+ '42 Market Street',
599
+ h.br([]),
600
+ 'Brooklyn, NY 11201',
601
+ h.br([]),
602
+ 'United States',
603
+ ],
604
+ ),
605
+ ],
606
+ ),
607
+ h.div(
608
+ [
609
+ h.Class(
610
+ 'self-start text-xs font-bold uppercase tracking-wider text-orange-800',
611
+ ),
612
+ ],
613
+ ['Selected'],
614
+ ),
615
+ ],
616
+ ),
617
+ ],
618
+ ),
619
+ h.section(
620
+ [h.AriaLabel('Delivery method')],
621
+ [
622
+ h.h2(
623
+ [h.Class('text-sm font-bold text-stone-900')],
624
+ ['Delivery method'],
625
+ ),
626
+ h.div(
627
+ [
628
+ h.Class(
629
+ 'mt-3 flex items-center justify-between gap-4 border border-stone-300 bg-stone-50 p-4',
630
+ ),
631
+ ],
632
+ [
633
+ h.div(
634
+ [h.Class('flex items-center gap-3')],
635
+ [
636
+ Icon.truck('h-6 w-6 text-stone-600'),
637
+ h.div(
638
+ [],
639
+ [
640
+ h.div(
641
+ [h.Class('text-sm font-bold text-stone-900')],
642
+ ['Standard tracked delivery'],
643
+ ),
644
+ h.div(
645
+ [h.Class('mt-1 text-xs text-stone-600')],
646
+ ['Estimated arrival: 4–6 business days'],
647
+ ),
648
+ ],
649
+ ),
650
+ ],
651
+ ),
652
+ h.div([h.Class('text-sm font-bold text-stone-900')], ['Free']),
653
+ ],
654
+ ),
655
+ ],
656
+ ),
657
+ h.div(
658
+ [
659
+ h.Class(
660
+ 'grid gap-4 border-t border-stone-300 pt-6 sm:grid-cols-3 sm:items-center',
661
+ ),
662
+ ],
663
+ [
664
+ Button.view(
665
+ {
666
+ onClick: Message.ClickedBack(),
667
+ toView: attributes =>
668
+ h.button(
669
+ [
670
+ ...attributes.button,
671
+ h.Class(
672
+ clsx(secondaryButtonClassName, 'sm:justify-self-start'),
673
+ ),
674
+ ],
675
+ ['Back to bag'],
676
+ ),
677
+ },
678
+ h,
679
+ ),
680
+ h.div([h.Class('sm:justify-self-center')], [cancelCheckoutButton(h)]),
681
+ Button.view(
682
+ {
683
+ onClick: Message.ClickedContinue(),
684
+ toView: attributes =>
685
+ h.button(
686
+ [
687
+ ...attributes.button,
688
+ h.Class(
689
+ clsx(primaryButtonClassName, 'sm:justify-self-end'),
690
+ ),
691
+ ],
692
+ ['Continue to payment', Icon.arrowRight()],
693
+ ),
694
+ },
695
+ h,
696
+ ),
697
+ ],
698
+ ),
699
+ ],
700
+ )
701
+
702
+ const paymentView = (
703
+ state: typeof Payment.Type,
704
+ h: HtmlBuilder<Message>,
705
+ ): Html =>
706
+ h.div(
707
+ [h.Class('grid gap-7')],
708
+ [
709
+ h.div(
710
+ [
711
+ h.Class(
712
+ 'flex items-start gap-3 border border-stone-300 bg-stone-50 p-4 text-sm leading-6 text-stone-600',
713
+ ),
714
+ ],
715
+ [
716
+ Icon.lockClosed('mt-0.5 h-5 w-5 shrink-0'),
717
+ h.p(
718
+ [],
719
+ [
720
+ 'Payments are encrypted. Signal Press never stores your full card number.',
721
+ ],
722
+ ),
723
+ ],
724
+ ),
725
+ h.section(
726
+ [h.AriaLabel('Saved payment methods')],
727
+ [
728
+ h.h2(
729
+ [h.Class('text-sm font-bold text-stone-900')],
730
+ ['Saved payment method'],
731
+ ),
732
+ Checkbox.view(
733
+ {
734
+ id: 'saved-card',
735
+ isChecked: state.isPaymentMethodSelected,
736
+ onToggle: isSelected =>
737
+ Message.ToggledPaymentMethod({ isSelected }),
738
+ toView: attributes =>
739
+ h.div(
740
+ [
741
+ ...attributes.checkbox,
742
+ h.Class(
743
+ clsx(
744
+ 'mt-3 flex w-full cursor-pointer items-center justify-between gap-4 border-2 p-5 text-left focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-stone-900',
745
+ {
746
+ 'border-stone-900 bg-white':
747
+ state.isPaymentMethodSelected,
748
+ 'border-stone-300 bg-stone-50 transition-colors hover:border-stone-500 hover:bg-white':
749
+ !state.isPaymentMethodSelected,
750
+ },
751
+ ),
752
+ ),
753
+ ],
754
+ [
755
+ h.div(
756
+ [h.Class('flex items-center gap-4')],
757
+ [
758
+ h.div(
759
+ [
760
+ h.Class(
761
+ 'grid h-10 w-14 place-items-center border border-stone-300 bg-stone-50 text-xs font-bold tracking-widest text-stone-900',
762
+ ),
763
+ ],
764
+ ['MC'],
765
+ ),
766
+ h.div(
767
+ [],
768
+ [
769
+ h.div(
770
+ [
771
+ ...attributes.label,
772
+ h.Class('text-sm font-bold text-stone-900'),
773
+ ],
774
+ ['Mastercard •••• 4242'],
775
+ ),
776
+ h.div(
777
+ [
778
+ ...attributes.description,
779
+ h.Class('mt-1 text-xs text-stone-600'),
780
+ ],
781
+ ['Expires 08/29'],
782
+ ),
783
+ ],
784
+ ),
785
+ ],
786
+ ),
787
+ h.span(
788
+ [
789
+ h.Class(
790
+ clsx('h-6 w-6 rounded-full border', {
791
+ 'grid place-items-center border-stone-900 bg-stone-900 text-white':
792
+ state.isPaymentMethodSelected,
793
+ 'border-stone-500': !state.isPaymentMethodSelected,
794
+ }),
795
+ ),
796
+ ],
797
+ [
798
+ state.isPaymentMethodSelected
799
+ ? Icon.check('h-3.5 w-3.5')
800
+ : h.empty,
801
+ ],
802
+ ),
803
+ ],
804
+ ),
805
+ },
806
+ h,
807
+ ),
808
+ h.p(
809
+ [h.Class('mt-3 text-xs leading-5 text-stone-600')],
810
+ [
811
+ 'Select the card above to continue. No charge is made until you place the order.',
812
+ ],
813
+ ),
814
+ ],
815
+ ),
816
+ h.div(
817
+ [
818
+ h.Class(
819
+ 'grid gap-4 border-t border-stone-300 pt-6 sm:grid-cols-3 sm:items-center',
820
+ ),
821
+ ],
822
+ [
823
+ Button.view(
824
+ {
825
+ onClick: Message.ClickedBack(),
826
+ toView: attributes =>
827
+ h.button(
828
+ [
829
+ ...attributes.button,
830
+ h.Class(
831
+ clsx(secondaryButtonClassName, 'sm:justify-self-start'),
832
+ ),
833
+ ],
834
+ [
835
+ state.isShippingRequired
836
+ ? 'Back to delivery'
837
+ : 'Back to bag',
838
+ ],
839
+ ),
840
+ },
841
+ h,
842
+ ),
843
+ h.div([h.Class('sm:justify-self-center')], [cancelCheckoutButton(h)]),
844
+ Button.view(
845
+ {
846
+ onClick: Message.ClickedContinue(),
847
+ isDisabled: !state.isPaymentMethodSelected,
848
+ toView: attributes =>
849
+ h.button(
850
+ [
851
+ ...attributes.button,
852
+ h.Class(
853
+ clsx(primaryButtonClassName, 'sm:justify-self-end'),
854
+ ),
855
+ ],
856
+ ['Review order', Icon.arrowRight()],
857
+ ),
858
+ },
859
+ h,
860
+ ),
861
+ ],
862
+ ),
863
+ ],
864
+ )
865
+
866
+ const reviewView = (
867
+ state: typeof Review.Type,
868
+ h: HtmlBuilder<Message>,
869
+ ): Html => {
870
+ const isReadyToPlace = isReviewReady(state)
871
+ const pricing = orderPricing(
872
+ state.isShippingRequired,
873
+ promoToMaybeDiscount(state.promo),
874
+ )
875
+
876
+ return h.div(
877
+ [h.Class('grid gap-7')],
878
+ [
879
+ h.div(
880
+ [h.Class('divide-y divide-stone-300 border-y border-stone-300')],
881
+ [
882
+ h.div(
883
+ [h.Class('grid gap-2 py-5 sm:grid-cols-4 sm:items-start')],
884
+ [
885
+ h.div(
886
+ [
887
+ h.Class(
888
+ 'text-xs font-bold uppercase tracking-widest text-stone-600',
889
+ ),
890
+ ],
891
+ ['Edition'],
892
+ ),
893
+ h.div(
894
+ [h.Class('sm:col-span-2')],
895
+ [
896
+ h.div(
897
+ [h.Class('text-sm font-bold text-stone-900')],
898
+ [editionName(state.isShippingRequired)],
899
+ ),
900
+ h.div(
901
+ [h.Class('mt-1 text-xs text-stone-600')],
902
+ ['Designing Durable Interfaces'],
903
+ ),
904
+ ],
905
+ ),
906
+ h.div(
907
+ [h.Class('text-sm font-bold text-stone-900')],
908
+ [formatMoney(pricing.subtotal)],
909
+ ),
910
+ ],
911
+ ),
912
+ h.div(
913
+ [h.Class('grid gap-2 py-5 sm:grid-cols-4 sm:items-start')],
914
+ [
915
+ h.div(
916
+ [
917
+ h.Class(
918
+ 'text-xs font-bold uppercase tracking-widest text-stone-600',
919
+ ),
920
+ ],
921
+ [state.isShippingRequired ? 'Delivery' : 'Fulfillment'],
922
+ ),
923
+ h.div(
924
+ [h.Class('sm:col-span-2')],
925
+ [
926
+ h.div(
927
+ [h.Class('text-sm font-bold text-stone-900')],
928
+ [
929
+ state.isShippingRequired
930
+ ? 'Standard tracked delivery'
931
+ : 'Email delivery',
932
+ ],
933
+ ),
934
+ h.div(
935
+ [h.Class('mt-1 text-xs leading-5 text-stone-600')],
936
+ [
937
+ state.isShippingRequired
938
+ ? 'Alex Morgan · 42 Market Street · Brooklyn, NY 11201'
939
+ : 'PDF and EPUB files sent to alex.morgan@example.com',
940
+ ],
941
+ ),
942
+ ],
943
+ ),
944
+ h.div([h.Class('text-sm font-bold text-stone-900')], ['Free']),
945
+ ],
946
+ ),
947
+ h.div(
948
+ [h.Class('grid gap-2 py-5 sm:grid-cols-4 sm:items-center')],
949
+ [
950
+ h.div(
951
+ [
952
+ h.Class(
953
+ 'text-xs font-bold uppercase tracking-widest text-stone-600',
954
+ ),
955
+ ],
956
+ ['Payment'],
957
+ ),
958
+ h.div(
959
+ [
960
+ h.Class(
961
+ 'flex items-center gap-2 text-sm text-stone-900 sm:col-span-2',
962
+ ),
963
+ ],
964
+ [
965
+ Icon.creditCard(),
966
+ state.isPaymentMethodSelected
967
+ ? 'Mastercard •••• 4242'
968
+ : 'Payment method required',
969
+ ],
970
+ ),
971
+ Button.view(
972
+ {
973
+ onClick: Message.ToggledPaymentMethod({
974
+ isSelected: !state.isPaymentMethodSelected,
975
+ }),
976
+ toView: attributes =>
977
+ h.button(
978
+ [
979
+ ...attributes.button,
980
+ h.Class(
981
+ 'cursor-pointer text-xs font-bold uppercase tracking-wider text-orange-800 underline underline-offset-4',
982
+ ),
983
+ ],
984
+ [
985
+ state.isPaymentMethodSelected
986
+ ? 'Change'
987
+ : 'Use saved card',
988
+ ],
989
+ ),
990
+ },
991
+ h,
992
+ ),
993
+ ],
994
+ ),
995
+ ],
996
+ ),
997
+ h.div(
998
+ [h.Class('grid gap-2')],
999
+ [
1000
+ h.form(
1001
+ [
1002
+ h.Class('flex items-end gap-3'),
1003
+ h.OnSubmit(Message.SubmittedPromoCode()),
1004
+ ],
1005
+ [
1006
+ Input.view(
1007
+ {
1008
+ id: 'promo-code',
1009
+ value: state.promoCodeInput,
1010
+ onInput: value => Message.UpdatedPromoCode({ value }),
1011
+ toView: attributes =>
1012
+ h.div(
1013
+ [h.Class('grid flex-1 gap-2 sm:max-w-xs')],
1014
+ [
1015
+ h.label(
1016
+ [
1017
+ ...attributes.label,
1018
+ h.Class(
1019
+ 'text-xs font-bold uppercase tracking-widest text-stone-600',
1020
+ ),
1021
+ ],
1022
+ ['Promo code'],
1023
+ ),
1024
+ h.input([
1025
+ ...attributes.input,
1026
+ h.Class(
1027
+ 'min-h-12 border border-stone-500 bg-white px-3 text-sm text-stone-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-stone-900',
1028
+ ),
1029
+ ]),
1030
+ ],
1031
+ ),
1032
+ },
1033
+ h,
1034
+ ),
1035
+ Button.view(
1036
+ {
1037
+ type: 'submit',
1038
+ toView: attributes =>
1039
+ h.button(
1040
+ [...attributes.button, h.Class(secondaryButtonClassName)],
1041
+ ['Apply'],
1042
+ ),
1043
+ },
1044
+ h,
1045
+ ),
1046
+ ],
1047
+ ),
1048
+ M.value(state.promo).pipe(
1049
+ M.tagsExhaustive({
1050
+ NoPromo: () => h.p([h.Class(promoFeedbackClassName)], ['']),
1051
+ AppliedPromo: ({ discount }) =>
1052
+ h.p(
1053
+ [h.Class(clsx(promoFeedbackClassName, 'text-orange-800'))],
1054
+ [`${discount.code} applied · ${discount.percentOff}% off`],
1055
+ ),
1056
+ RejectedPromo: () =>
1057
+ h.p(
1058
+ [h.Class(clsx(promoFeedbackClassName, 'text-red-700'))],
1059
+ ["That code isn't recognized."],
1060
+ ),
1061
+ }),
1062
+ ),
1063
+ ],
1064
+ ),
1065
+ Checkbox.view(
1066
+ {
1067
+ id: 'accept-terms',
1068
+ isChecked: state.isTermsAccepted,
1069
+ onToggle: isAccepted => Message.ToggledTermsAccepted({ isAccepted }),
1070
+ toView: attributes =>
1071
+ h.div(
1072
+ [
1073
+ h.Class(
1074
+ 'flex items-start gap-3 text-sm leading-6 text-stone-600',
1075
+ ),
1076
+ ],
1077
+ [
1078
+ h.span(
1079
+ [
1080
+ ...attributes.checkbox,
1081
+ h.Class(
1082
+ clsx(
1083
+ 'mt-0.5 h-5 w-5 shrink-0 cursor-pointer border focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-stone-900',
1084
+ {
1085
+ 'grid place-items-center border-stone-900 bg-stone-900 text-white':
1086
+ state.isTermsAccepted,
1087
+ 'border-stone-500 bg-white': !state.isTermsAccepted,
1088
+ },
1089
+ ),
1090
+ ),
1091
+ ],
1092
+ [state.isTermsAccepted ? Icon.check('h-3.5 w-3.5') : h.empty],
1093
+ ),
1094
+ h.span(
1095
+ [],
1096
+ [
1097
+ h.span(
1098
+ [...attributes.label, h.Class('sr-only')],
1099
+ ['Accept terms of sale'],
1100
+ ),
1101
+ h.span(
1102
+ [
1103
+ ...attributes.description,
1104
+ h.OnClick(
1105
+ Message.ToggledTermsAccepted({
1106
+ isAccepted: !state.isTermsAccepted,
1107
+ }),
1108
+ ),
1109
+ h.Class('cursor-pointer'),
1110
+ ],
1111
+ [
1112
+ state.isShippingRequired
1113
+ ? 'I agree to the terms of sale and return policy.'
1114
+ : 'I agree to the terms of sale and understand that digital purchases are delivered immediately.',
1115
+ ],
1116
+ ),
1117
+ ],
1118
+ ),
1119
+ ],
1120
+ ),
1121
+ },
1122
+ h,
1123
+ ),
1124
+ h.div(
1125
+ [
1126
+ h.Class(
1127
+ 'grid gap-4 border-t border-stone-300 pt-6 sm:grid-cols-3 sm:items-center',
1128
+ ),
1129
+ ],
1130
+ [
1131
+ Button.view(
1132
+ {
1133
+ onClick: Message.ClickedBack(),
1134
+ toView: attributes =>
1135
+ h.button(
1136
+ [
1137
+ ...attributes.button,
1138
+ h.Class(
1139
+ clsx(secondaryButtonClassName, 'sm:justify-self-start'),
1140
+ ),
1141
+ ],
1142
+ ['Back to payment'],
1143
+ ),
1144
+ },
1145
+ h,
1146
+ ),
1147
+ h.div([h.Class('sm:justify-self-center')], [cancelCheckoutButton(h)]),
1148
+ Button.view(
1149
+ {
1150
+ onClick: Message.ClickedPlaceOrder(),
1151
+ isDisabled: !isReadyToPlace,
1152
+ toView: attributes =>
1153
+ h.button(
1154
+ [
1155
+ ...attributes.button,
1156
+ h.Class(
1157
+ clsx(primaryButtonClassName, 'sm:justify-self-end'),
1158
+ ),
1159
+ ],
1160
+ [`Place order · ${formatMoney(pricing.total)}`],
1161
+ ),
1162
+ },
1163
+ h,
1164
+ ),
1165
+ ],
1166
+ ),
1167
+ ],
1168
+ )
1169
+ }
1170
+
1171
+ const placingView = (h: HtmlBuilder<Message>): Html =>
1172
+ h.div(
1173
+ [
1174
+ h.Role('status'),
1175
+ h.AriaLive('polite'),
1176
+ h.Class('grid min-h-72 place-items-center text-center'),
1177
+ ],
1178
+ [
1179
+ h.div(
1180
+ [],
1181
+ [
1182
+ h.div([
1183
+ h.Class(
1184
+ 'mx-auto h-12 w-12 animate-spin rounded-full border-2 border-stone-300 border-t-stone-900 motion-reduce:animate-none',
1185
+ ),
1186
+ ]),
1187
+ h.p(
1188
+ [h.Class('mt-6 text-sm leading-6 text-stone-600')],
1189
+ ['Authorizing payment and reserving your copy…'],
1190
+ ),
1191
+ ],
1192
+ ),
1193
+ ],
1194
+ )
1195
+
1196
+ const confirmedView = (
1197
+ state: typeof Confirmed.Type,
1198
+ h: HtmlBuilder<Message>,
1199
+ ): Html =>
1200
+ h.div(
1201
+ [
1202
+ h.Class(
1203
+ 'grid min-h-80 content-center justify-items-center py-8 text-center',
1204
+ ),
1205
+ ],
1206
+ [
1207
+ h.div(
1208
+ [
1209
+ h.Class(
1210
+ 'grid h-14 w-14 place-items-center rounded-full bg-stone-900 text-white',
1211
+ ),
1212
+ ],
1213
+ [Icon.check('h-7 w-7')],
1214
+ ),
1215
+ h.h2(
1216
+ [h.Class('mt-6 font-serif text-3xl text-stone-900')],
1217
+ [`Order ${state.orderId} confirmed`],
1218
+ ),
1219
+ h.p(
1220
+ [h.Class('mt-3 max-w-md text-sm leading-6 text-stone-600')],
1221
+ [
1222
+ state.isShippingRequired
1223
+ ? 'We’ll email tracking details when your hardcover leaves the press.'
1224
+ : 'Your PDF and EPUB download links are on their way to alex.morgan@example.com.',
1225
+ ],
1226
+ ),
1227
+ h.div(
1228
+ [
1229
+ h.Class(
1230
+ 'mt-7 border-y border-stone-300 px-8 py-4 text-xs font-bold uppercase tracking-widest text-stone-600',
1231
+ ),
1232
+ ],
1233
+ [`Order reference · ${state.orderId}`],
1234
+ ),
1235
+ Button.view(
1236
+ {
1237
+ onClick: Message.ClickedStartOver(),
1238
+ toView: attributes =>
1239
+ h.button(
1240
+ [
1241
+ ...attributes.button,
1242
+ h.Class(clsx(primaryButtonClassName, 'mt-7')),
1243
+ ],
1244
+ ['Return to checkout'],
1245
+ ),
1246
+ },
1247
+ h,
1248
+ ),
1249
+ ],
1250
+ )
1251
+
1252
+ const cancelledView = (h: HtmlBuilder<Message>): Html =>
1253
+ h.div(
1254
+ [
1255
+ h.Class(
1256
+ 'grid min-h-72 content-center justify-items-center py-8 text-center',
1257
+ ),
1258
+ ],
1259
+ [
1260
+ h.div(
1261
+ [
1262
+ h.AriaHidden(true),
1263
+ h.Class(
1264
+ 'grid h-14 w-14 place-items-center rounded-full border border-stone-400 font-serif text-2xl text-stone-500',
1265
+ ),
1266
+ ],
1267
+ ['×'],
1268
+ ),
1269
+ h.p(
1270
+ [h.Class('mt-6 max-w-md text-sm leading-6 text-stone-600')],
1271
+ [
1272
+ 'Your checkout was cancelled before payment. You can begin again whenever you’re ready.',
1273
+ ],
1274
+ ),
1275
+ Button.view(
1276
+ {
1277
+ onClick: Message.ClickedStartOver(),
1278
+ toView: attributes =>
1279
+ h.button(
1280
+ [
1281
+ ...attributes.button,
1282
+ h.Class(clsx(primaryButtonClassName, 'mt-7')),
1283
+ ],
1284
+ ['Start checkout again'],
1285
+ ),
1286
+ },
1287
+ h,
1288
+ ),
1289
+ ],
1290
+ )
1291
+
1292
+ const checkoutContentView = (
1293
+ state: typeof CheckoutState.Type,
1294
+ editionRadioGroup: RadioGroup.Model,
1295
+ h: HtmlBuilder<Message>,
1296
+ ): Html =>
1297
+ M.value(state).pipe(
1298
+ M.tagsExhaustive({
1299
+ Cart: cartState => cartView(cartState, editionRadioGroup, h),
1300
+ Shipping: () => shippingView(h),
1301
+ Payment: paymentState => paymentView(paymentState, h),
1302
+ Review: reviewState => reviewView(reviewState, h),
1303
+ Placing: () => placingView(h),
1304
+ Confirmed: confirmedState => confirmedView(confirmedState, h),
1305
+ Cancelled: () => cancelledView(h),
1306
+ }),
1307
+ )
1308
+
1309
+ const checkoutPanelView = (
1310
+ state: typeof CheckoutState.Type,
1311
+ editionRadioGroup: RadioGroup.Model,
1312
+ h: HtmlBuilder<Message>,
1313
+ ): Html =>
1314
+ h.section(
1315
+ [
1316
+ h.Class(
1317
+ 'border border-stone-300 bg-stone-50 px-5 py-7 sm:px-8 sm:py-9 lg:col-span-2',
1318
+ ),
1319
+ ],
1320
+ [
1321
+ h.div(
1322
+ [h.Class('mb-8 max-w-2xl')],
1323
+ [
1324
+ h.div(
1325
+ [
1326
+ h.Class(
1327
+ 'text-xs font-bold uppercase tracking-widest text-orange-800',
1328
+ ),
1329
+ ],
1330
+ [stateEyebrow(state)],
1331
+ ),
1332
+ h.h1(
1333
+ [h.Class('mt-3 font-serif text-3xl text-stone-900 sm:text-4xl')],
1334
+ [stateTitle(state)],
1335
+ ),
1336
+ h.p(
1337
+ [h.Class('mt-3 text-sm leading-6 text-stone-600')],
1338
+ [stateDescription(state)],
1339
+ ),
1340
+ ],
1341
+ ),
1342
+ checkoutContentView(state, editionRadioGroup, h),
1343
+ ],
1344
+ )
1345
+
1346
+ const orderSummaryView = (
1347
+ state: typeof CheckoutState.Type,
1348
+ h: HtmlBuilder<Message>,
1349
+ ): Html => {
1350
+ const pricing = orderPricing(
1351
+ state.isShippingRequired,
1352
+ stateToMaybeDiscount(state),
1353
+ )
1354
+ const summaryRowClassName = 'flex items-center justify-between gap-4'
1355
+ const orderNoteClassName =
1356
+ 'mt-5 flex items-start gap-2 text-xs leading-5 text-stone-600'
1357
+
1358
+ return h.aside(
1359
+ [
1360
+ h.AriaLabel('Order summary'),
1361
+ h.Class(
1362
+ 'h-fit border border-stone-300 bg-stone-200 p-5 lg:sticky lg:top-6 sm:p-6',
1363
+ ),
1364
+ ],
1365
+ [
1366
+ h.h2(
1367
+ [h.Class('text-sm font-bold uppercase tracking-widest text-stone-900')],
1368
+ ['Order summary'],
1369
+ ),
1370
+ h.div(
1371
+ [h.Class('mt-5 grid grid-cols-4 gap-4 border-b border-stone-300 pb-5')],
1372
+ [
1373
+ bookCoverView('', true, h),
1374
+ h.div(
1375
+ [h.Class('col-span-3')],
1376
+ [
1377
+ h.div(
1378
+ [h.Class('font-serif text-base leading-5 text-stone-900')],
1379
+ ['Designing Durable Interfaces'],
1380
+ ),
1381
+ h.div(
1382
+ [h.Class('mt-2 text-xs text-stone-600')],
1383
+ [`${editionName(state.isShippingRequired)} · Qty 1`],
1384
+ ),
1385
+ h.div(
1386
+ [h.Class('mt-3 text-sm font-bold text-stone-900')],
1387
+ [formatMoney(pricing.subtotal)],
1388
+ ),
1389
+ ],
1390
+ ),
1391
+ ],
1392
+ ),
1393
+ h.dl(
1394
+ [h.Class('grid gap-3 border-b border-stone-300 py-5 text-sm')],
1395
+ [
1396
+ h.div(
1397
+ [h.Class(summaryRowClassName)],
1398
+ [
1399
+ h.dt([h.Class('text-stone-600')], ['Subtotal']),
1400
+ h.dd(
1401
+ [h.Class('font-medium text-stone-900')],
1402
+ [formatMoney(pricing.subtotal)],
1403
+ ),
1404
+ ],
1405
+ ),
1406
+ ...Option.match(pricing.maybeAppliedDiscount, {
1407
+ onNone: () => [],
1408
+ onSome: appliedDiscount => [
1409
+ h.div(
1410
+ [h.Class(summaryRowClassName)],
1411
+ [
1412
+ h.dt(
1413
+ [h.Class('text-stone-600')],
1414
+ [`Discount · ${appliedDiscount.code}`],
1415
+ ),
1416
+ h.dd(
1417
+ [h.Class('font-medium text-orange-800')],
1418
+ [`-${formatMoney(appliedDiscount.amount)}`],
1419
+ ),
1420
+ ],
1421
+ ),
1422
+ ],
1423
+ }),
1424
+ h.div(
1425
+ [h.Class(summaryRowClassName)],
1426
+ [
1427
+ h.dt(
1428
+ [h.Class('text-stone-600')],
1429
+ [state.isShippingRequired ? 'Shipping' : 'Email delivery'],
1430
+ ),
1431
+ h.dd([h.Class('font-medium text-stone-900')], ['Free']),
1432
+ ],
1433
+ ),
1434
+ h.div(
1435
+ [h.Class(summaryRowClassName)],
1436
+ [
1437
+ h.dt([h.Class('text-stone-600')], ['Estimated tax']),
1438
+ h.dd(
1439
+ [h.Class('font-medium text-stone-900')],
1440
+ [formatMoney(pricing.tax)],
1441
+ ),
1442
+ ],
1443
+ ),
1444
+ ],
1445
+ ),
1446
+ h.div(
1447
+ [h.Class('flex items-baseline justify-between gap-4 pt-5')],
1448
+ [
1449
+ h.div([h.Class('text-sm font-bold text-stone-900')], ['Total']),
1450
+ h.div(
1451
+ [h.Class('font-serif text-2xl text-stone-900')],
1452
+ [formatMoney(pricing.total)],
1453
+ ),
1454
+ ],
1455
+ ),
1456
+ state.isShippingRequired
1457
+ ? h.div(
1458
+ [h.Class(orderNoteClassName)],
1459
+ [
1460
+ Icon.truck('mt-0.5 h-4 w-4 shrink-0'),
1461
+ 'Free tracked shipping in the United States.',
1462
+ ],
1463
+ )
1464
+ : h.div(
1465
+ [h.Class(orderNoteClassName)],
1466
+ [
1467
+ Icon.download('mt-0.5 h-4 w-4 shrink-0'),
1468
+ 'Download links arrive immediately after purchase.',
1469
+ ],
1470
+ ),
1471
+ ],
1472
+ )
1473
+ }
1474
+
1475
+ const formatTags = (tags: ReadonlyArray<string>): string =>
1476
+ Array.match(tags, {
1477
+ onEmpty: () => 'None',
1478
+ onNonEmpty: Array.join(', '),
1479
+ })
1480
+
1481
+ const transitionLogView = (model: Model, h: HtmlBuilder<Message>): Html => {
1482
+ const transitionLogClassName =
1483
+ 'border-l-2 border-orange-700 pl-3 font-mono text-xs leading-5 text-stone-300 lg:col-span-3'
1484
+
1485
+ return Array.match(model.transitionLog, {
1486
+ onEmpty: () =>
1487
+ h.div(
1488
+ [h.Role('status'), h.Class(transitionLogClassName)],
1489
+ ['Waiting for the first checkout event'],
1490
+ ),
1491
+ onNonEmpty: transitionLog =>
1492
+ h.ol(
1493
+ [
1494
+ h.Role('log'),
1495
+ h.Class(clsx(transitionLogClassName, 'max-h-40 overflow-y-auto')),
1496
+ ],
1497
+ Array.map(transitionLog, entry =>
1498
+ h.keyed('li')(String(entry.id), [], [entry.summary]),
1499
+ ),
1500
+ ),
1501
+ })
1502
+ }
1503
+
1504
+ const DEAD_TRANSITIONS = checkoutMachine.deadTransitions()
1505
+ const UNREACHABLE_STATE_TAGS = checkoutMachine.unreachableStates()
1506
+ const MERMAID_DEFINITION = checkoutMachine.toMermaid()
1507
+
1508
+ const analysisView = (model: Model, h: HtmlBuilder<Message>): Html =>
1509
+ h.section(
1510
+ [
1511
+ h.AriaLabel('State machine inspector'),
1512
+ h.Class('border border-stone-800 bg-stone-900 text-stone-100'),
1513
+ ],
1514
+ [
1515
+ h.div(
1516
+ [h.Class('grid gap-6 px-5 py-6 sm:px-7 lg:grid-cols-3 lg:items-start')],
1517
+ [
1518
+ h.div(
1519
+ [h.Class('lg:col-span-2')],
1520
+ [
1521
+ h.div(
1522
+ [h.Class('flex items-center gap-2 text-orange-300')],
1523
+ [
1524
+ Icon.codeBracket(),
1525
+ h.div(
1526
+ [h.Class('text-xs font-bold uppercase tracking-widest')],
1527
+ ['Developer view'],
1528
+ ),
1529
+ ],
1530
+ ),
1531
+ h.h2(
1532
+ [h.Class('mt-3 font-serif text-2xl')],
1533
+ ['State machine inspector'],
1534
+ ),
1535
+ h.p(
1536
+ [h.Class('mt-2 max-w-2xl text-sm leading-6 text-stone-400')],
1537
+ [
1538
+ 'The storefront below is driven by the same explicit machine shown here. Every interaction produces a Message and a deterministic transition.',
1539
+ ],
1540
+ ),
1541
+ ],
1542
+ ),
1543
+ h.dl(
1544
+ [h.Class('grid grid-cols-2 gap-x-6 gap-y-2 text-xs')],
1545
+ [
1546
+ h.div(
1547
+ [],
1548
+ [
1549
+ h.dt([h.Class('text-stone-400')], ['Unreachable']),
1550
+ h.dd(
1551
+ [h.Class('mt-1 font-mono text-stone-100')],
1552
+ [formatTags(UNREACHABLE_STATE_TAGS)],
1553
+ ),
1554
+ ],
1555
+ ),
1556
+ h.div(
1557
+ [],
1558
+ [
1559
+ h.dt([h.Class('text-stone-400')], ['Dead transitions']),
1560
+ h.dd(
1561
+ [h.Class('mt-1 font-mono text-stone-100')],
1562
+ [String(DEAD_TRANSITIONS.length)],
1563
+ ),
1564
+ ],
1565
+ ),
1566
+ ],
1567
+ ),
1568
+ ],
1569
+ ),
1570
+ h.div(
1571
+ [h.Class('grid gap-5 border-t border-stone-700 px-5 py-5 sm:px-7')],
1572
+ [
1573
+ h.div(
1574
+ [h.Class('grid gap-3 lg:grid-cols-4 lg:items-start')],
1575
+ [
1576
+ h.div(
1577
+ [
1578
+ h.Class(
1579
+ 'text-xs font-bold uppercase tracking-widest text-stone-400',
1580
+ ),
1581
+ ],
1582
+ ['Recent transitions'],
1583
+ ),
1584
+ transitionLogView(model, h),
1585
+ ],
1586
+ ),
1587
+ h.ol(
1588
+ [h.AriaLabel('Machine states'), h.Class('flex flex-wrap gap-2')],
1589
+ Array.map(checkoutMachine.stateTags, stateTag => {
1590
+ const isActive = model.checkout._tag === stateTag
1591
+
1592
+ return h.keyed('li')(
1593
+ stateTag,
1594
+ [
1595
+ h.Class(
1596
+ clsx('border px-2.5 py-1.5 font-mono text-xs', {
1597
+ 'border-orange-300 bg-orange-950 text-stone-100':
1598
+ isActive,
1599
+ 'border-stone-700 text-stone-400': !isActive,
1600
+ }),
1601
+ ),
1602
+ ],
1603
+ [stateTag],
1604
+ )
1605
+ }),
1606
+ ),
1607
+ ],
1608
+ ),
1609
+ h.div(
1610
+ [h.Class('border-t border-stone-700')],
1611
+ [
1612
+ h.h3(
1613
+ [
1614
+ h.Class(
1615
+ 'px-5 py-4 text-xs font-bold uppercase tracking-widest text-stone-400 sm:px-7',
1616
+ ),
1617
+ ],
1618
+ ['Mermaid definition'],
1619
+ ),
1620
+ h.pre(
1621
+ [
1622
+ h.Class(
1623
+ 'max-h-96 overflow-auto border-t border-stone-700 bg-stone-950 p-5 font-mono text-xs leading-5 text-stone-300 sm:p-7',
1624
+ ),
1625
+ ],
1626
+ [MERMAID_DEFINITION],
1627
+ ),
1628
+ ],
1629
+ ),
1630
+ ],
1631
+ )
1632
+
1633
+ export const view = (model: Model, h: HtmlBuilder<Message>): Document => ({
1634
+ title: `${stateTitle(model.checkout)} | Signal Press`,
1635
+ body: h.div(
1636
+ [h.Class('min-h-screen bg-stone-100 text-stone-900')],
1637
+ [
1638
+ pageHeaderView(h),
1639
+ h.main(
1640
+ [
1641
+ h.Class(
1642
+ 'mx-auto w-full max-w-6xl px-5 pb-12 pt-6 sm:px-8 sm:pb-16 sm:pt-8',
1643
+ ),
1644
+ ],
1645
+ [
1646
+ analysisView(model, h),
1647
+ progressView(model.checkout, h),
1648
+ h.div(
1649
+ [h.Class('mt-6 grid gap-6 sm:mt-8 lg:grid-cols-3 lg:items-start')],
1650
+ [
1651
+ checkoutPanelView(model.checkout, model.editionRadioGroup, h),
1652
+ orderSummaryView(model.checkout, h),
1653
+ ],
1654
+ ),
1655
+ ],
1656
+ ),
1657
+ h.footer(
1658
+ [h.Class('border-t border-stone-300 bg-stone-50')],
1659
+ [
1660
+ h.div(
1661
+ [
1662
+ h.Class(
1663
+ 'mx-auto flex max-w-6xl flex-col gap-2 px-5 py-5 text-xs text-stone-600 sm:flex-row sm:items-center sm:justify-between sm:px-8',
1664
+ ),
1665
+ ],
1666
+ [
1667
+ h.div([], ['© Signal Press · Brooklyn, New York']),
1668
+ h.div([], ['Questions? support@signalpress.example']),
1669
+ ],
1670
+ ),
1671
+ ],
1672
+ ),
1673
+ ],
1674
+ ),
1675
+ })