create-foldkit-app 0.32.1 → 0.33.0-canary.40b6ef2babe9

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 (158) hide show
  1. package/README.md +5 -5
  2. package/dist/index.js +1 -1
  3. package/dist/rendering.js +1 -1
  4. package/dist/templates/base/.oxfmtrc.json +28 -0
  5. package/dist/templates/base/.oxlintrc.json +4 -0
  6. package/dist/templates/base/FOLDKIT.md +1 -1
  7. package/dist/templates/base/package.json +1 -1
  8. package/dist/templates/examples/api-cache/package.json +0 -2
  9. package/dist/templates/examples/api-cache/src/{main.fixtures.ts → main.fixture.ts} +24 -23
  10. package/dist/templates/examples/api-cache/src/scene.test.ts +2 -3
  11. package/dist/templates/examples/api-cache/src/story.test.ts +29 -24
  12. package/dist/templates/examples/auth/package.json +0 -2
  13. package/dist/templates/examples/auth/src/page/loggedOut/page/login.scene.test.ts +5 -12
  14. package/dist/templates/examples/auth/src/page/loggedOut/page/login.story.test.ts +4 -5
  15. package/dist/templates/examples/auth/src/scene.test.ts +4 -5
  16. package/dist/templates/examples/canvas-art/package.json +0 -2
  17. package/dist/templates/examples/canvas-art/src/scene.test.ts +9 -10
  18. package/dist/templates/examples/canvas-art/src/story.test.ts +9 -10
  19. package/dist/templates/examples/charting/package.json +0 -2
  20. package/dist/templates/examples/charting/src/{main.fixtures.ts → main.fixture.ts} +4 -4
  21. package/dist/templates/examples/charting/src/scene.test.ts +14 -11
  22. package/dist/templates/examples/charting/src/story.test.ts +11 -9
  23. package/dist/templates/examples/charting/src/telemetry.test.ts +1 -1
  24. package/dist/templates/examples/counter/package.json +0 -2
  25. package/dist/templates/examples/counters/package.json +0 -2
  26. package/dist/templates/examples/crash-view/package.json +0 -2
  27. package/dist/templates/examples/embedding/package.json +0 -2
  28. package/dist/templates/examples/embedding/src/scene.test.ts +2 -1
  29. package/dist/templates/examples/embedding/src/story.test.ts +2 -1
  30. package/dist/templates/examples/form/package.json +0 -2
  31. package/dist/templates/examples/form/src/scene.test.ts +16 -19
  32. package/dist/templates/examples/form/src/story.test.ts +11 -13
  33. package/dist/templates/examples/generative-art/package.json +0 -2
  34. package/dist/templates/examples/generative-art/src/scene.test.ts +9 -7
  35. package/dist/templates/examples/generative-art/src/story.test.ts +16 -10
  36. package/dist/templates/examples/interrupting-commands/package.json +0 -2
  37. package/dist/templates/examples/job-application/package.json +0 -2
  38. package/dist/templates/examples/job-application/src/main.fixture.ts +69 -0
  39. package/dist/templates/examples/job-application/src/main.ts +1 -1
  40. package/dist/templates/examples/job-application/src/scene.test.ts +50 -108
  41. package/dist/templates/examples/job-application/src/step/attachments/index.ts +2 -0
  42. package/dist/templates/examples/job-application/src/step/attachments/scene.test.ts +19 -0
  43. package/dist/templates/examples/job-application/src/step/attachments/story.test.ts +46 -0
  44. package/dist/templates/examples/job-application/src/{view/attachments.ts → step/attachments/view.ts} +4 -11
  45. package/dist/templates/examples/job-application/src/{view → step/coverLetter}/coverLetter.ts +36 -7
  46. package/dist/templates/examples/job-application/src/step/coverLetter/index.ts +1 -0
  47. package/dist/templates/examples/job-application/src/step/coverLetter/scene.test.ts +24 -0
  48. package/dist/templates/examples/job-application/src/step/coverLetter/story.test.ts +19 -0
  49. package/dist/templates/examples/job-application/src/step/education/{entry.ts → entry/entry.ts} +2 -2
  50. package/dist/templates/examples/job-application/src/step/education/entry/index.ts +11 -0
  51. package/dist/templates/examples/job-application/src/step/education/entry/story.test.ts +39 -0
  52. package/dist/templates/examples/job-application/src/step/education/entry/view.ts +160 -0
  53. package/dist/templates/examples/job-application/src/step/education/index.ts +11 -1
  54. package/dist/templates/examples/job-application/src/step/education/scene.test.ts +20 -0
  55. package/dist/templates/examples/job-application/src/step/education/story.test.ts +87 -0
  56. package/dist/templates/examples/job-application/src/{view/education.ts → step/education/view.ts} +6 -10
  57. package/dist/templates/examples/job-application/src/step/personalInfo/index.ts +11 -0
  58. package/dist/templates/examples/job-application/src/step/{personalInfo.ts → personalInfo/personalInfo.ts} +2 -2
  59. package/dist/templates/examples/job-application/src/step/personalInfo/scene.test.ts +38 -0
  60. package/dist/templates/examples/job-application/src/step/personalInfo/story.test.ts +89 -0
  61. package/dist/templates/examples/job-application/src/{view/personalInfo.ts → step/personalInfo/view.ts} +24 -34
  62. package/dist/templates/examples/job-application/src/step/skills/{entry.ts → entry/entry.ts} +2 -2
  63. package/dist/templates/examples/job-application/src/step/skills/entry/index.ts +11 -0
  64. package/dist/templates/examples/job-application/src/step/skills/entry/story.test.ts +27 -0
  65. package/dist/templates/examples/job-application/src/{view/skillEntry.ts → step/skills/entry/view.ts} +9 -12
  66. package/dist/templates/examples/job-application/src/step/skills/index.ts +11 -1
  67. package/dist/templates/examples/job-application/src/step/skills/scene.test.ts +16 -0
  68. package/dist/templates/examples/job-application/src/step/skills/story.test.ts +85 -0
  69. package/dist/templates/examples/job-application/src/step/skills/view.ts +47 -0
  70. package/dist/templates/examples/job-application/src/step/workHistory/{entry.ts → entry/entry.ts} +1 -1
  71. package/dist/templates/examples/job-application/src/step/workHistory/entry/index.ts +11 -0
  72. package/dist/templates/examples/job-application/src/step/workHistory/entry/story.test.ts +42 -0
  73. package/dist/templates/examples/job-application/src/{view/workEntry.ts → step/workHistory/entry/view.ts} +27 -42
  74. package/dist/templates/examples/job-application/src/step/workHistory/index.ts +11 -1
  75. package/dist/templates/examples/job-application/src/step/workHistory/scene.test.ts +20 -0
  76. package/dist/templates/examples/job-application/src/step/workHistory/story.test.ts +87 -0
  77. package/dist/templates/examples/job-application/src/{view/workHistory.ts → step/workHistory/view.ts} +4 -8
  78. package/dist/templates/examples/job-application/src/story.test.ts +54 -442
  79. package/dist/templates/examples/job-application/src/view/datePicker.ts +4 -4
  80. package/dist/templates/examples/job-application/src/view/field.ts +3 -3
  81. package/dist/templates/examples/job-application/src/view/index.ts +3 -358
  82. package/dist/templates/examples/job-application/src/view/stepNav.ts +2 -2
  83. package/dist/templates/examples/job-application/src/view/view.ts +359 -0
  84. package/dist/templates/examples/kanban/package.json +0 -2
  85. package/dist/templates/examples/kanban/src/scene.test.ts +1 -1
  86. package/dist/templates/examples/kanban/src/story.test.ts +22 -23
  87. package/dist/templates/examples/kanban/src/view/index.ts +1 -71
  88. package/dist/templates/examples/kanban/src/view/view.ts +71 -0
  89. package/dist/templates/examples/managed-resource-layer/package.json +0 -2
  90. package/dist/templates/examples/managed-resource-layer/src/story.test.ts +3 -2
  91. package/dist/templates/examples/map/package.json +0 -2
  92. package/dist/templates/examples/map/src/{main.fixtures.ts → main.fixture.ts} +4 -4
  93. package/dist/templates/examples/map/src/scene.test.ts +24 -18
  94. package/dist/templates/examples/map/src/story.test.ts +17 -13
  95. package/dist/templates/examples/pixel-art/package.json +0 -2
  96. package/dist/templates/examples/pixel-art/src/comparison.bench.test.ts +2 -1
  97. package/dist/templates/examples/pixel-art/src/main.bench.ts +3 -2
  98. package/dist/templates/examples/pixel-art/src/scene.test.ts +51 -42
  99. package/dist/templates/examples/pixel-art/src/story.test.ts +32 -27
  100. package/dist/templates/examples/query-sync/package.json +0 -2
  101. package/dist/templates/examples/query-sync/src/scene.test.ts +26 -20
  102. package/dist/templates/examples/query-sync/src/story.test.ts +15 -13
  103. package/dist/templates/examples/route-transitions/package.json +0 -2
  104. package/dist/templates/examples/routing/package.json +0 -2
  105. package/dist/templates/examples/routing/src/page/people.scene.test.ts +41 -0
  106. package/dist/templates/examples/routing/src/page/people.story.test.ts +73 -0
  107. package/dist/templates/examples/routing/src/scene.test.ts +2 -17
  108. package/dist/templates/examples/routing/src/story.test.ts +4 -34
  109. package/dist/templates/examples/shopping-cart/package.json +0 -2
  110. package/dist/templates/examples/shopping-cart/src/page/products.scene.test.ts +20 -0
  111. package/dist/templates/examples/shopping-cart/src/page/products.story.test.ts +56 -0
  112. package/dist/templates/examples/shopping-cart/src/page/products.ts +1 -1
  113. package/dist/templates/examples/shopping-cart/src/scene.test.ts +23 -32
  114. package/dist/templates/examples/shopping-cart/src/story.test.ts +38 -32
  115. package/dist/templates/examples/slow-warnings/package.json +0 -2
  116. package/dist/templates/examples/slow-warnings/src/handleSlow.test.ts +4 -4
  117. package/dist/templates/examples/slow-warnings/src/scene.test.ts +15 -13
  118. package/dist/templates/examples/slow-warnings/src/story.test.ts +16 -14
  119. package/dist/templates/examples/snake/package.json +0 -2
  120. package/dist/templates/examples/snake/src/scene.test.ts +11 -9
  121. package/dist/templates/examples/snake/src/story.test.ts +10 -11
  122. package/dist/templates/examples/ssg/package.json +0 -2
  123. package/dist/templates/examples/ssr/package.json +1 -3
  124. package/dist/templates/examples/state-machine/package.json +0 -2
  125. package/dist/templates/examples/state-machine/src/main.ts +79 -87
  126. package/dist/templates/examples/state-machine/src/scene.test.ts +1 -2
  127. package/dist/templates/examples/stopwatch/package.json +0 -2
  128. package/dist/templates/examples/stopwatch/src/story.test.ts +2 -1
  129. package/dist/templates/examples/todo/package.json +0 -2
  130. package/dist/templates/examples/todo/src/scene.test.ts +8 -9
  131. package/dist/templates/examples/todo/src/story.test.ts +37 -42
  132. package/dist/templates/examples/ui-showcase/package.json +0 -2
  133. package/dist/templates/examples/ui-showcase/src/story.test.ts +11 -12
  134. package/dist/templates/examples/view-transitions/package.json +0 -2
  135. package/dist/templates/examples/view-transitions/src/styles.css +3 -3
  136. package/dist/templates/examples/weather/package.json +0 -2
  137. package/dist/templates/examples/weather/src/scene.test.ts +1 -1
  138. package/dist/templates/examples/weather/src/story.test.ts +1 -1
  139. package/dist/templates/examples/web-components/package.json +0 -2
  140. package/dist/templates/examples/websocket-chat/package.json +0 -2
  141. package/dist/templates/examples/websocket-chat/src/scene.test.ts +20 -25
  142. package/dist/templates/examples/websocket-chat/src/story.test.ts +19 -20
  143. package/dist/templates/release.json +15 -16
  144. package/dist/templates/rendering/ssg/package.json +1 -1
  145. package/dist/templates/rendering/ssr/package.json +3 -3
  146. package/dist/templates/rendering/ssr/scripts/serve.ts +155 -0
  147. package/dist/templates/rendering/ssr/tsconfig.json +1 -1
  148. package/dist/templates/rendering/ssr/vite.config.ts +1 -1
  149. package/dist/utils/packages.js +2 -3
  150. package/package.json +3 -3
  151. package/dist/templates/base/.prettierignore +0 -5
  152. package/dist/templates/base/.prettierrc +0 -13
  153. package/dist/templates/examples/job-application/src/step/coverLetter.ts +0 -34
  154. package/dist/templates/examples/job-application/src/view/educationEntry.ts +0 -165
  155. package/dist/templates/examples/job-application/src/view/skills.ts +0 -49
  156. package/dist/templates/rendering/ssr/server/main.ts +0 -205
  157. /package/dist/templates/examples/job-application/src/step/{attachments.ts → attachments/attachments.ts} +0 -0
  158. /package/dist/templates/examples/weather/src/{main.fixtures.ts → main.fixture.ts} +0 -0
package/README.md CHANGED
@@ -18,11 +18,11 @@ The CLI prompts you for a project name, rendering mode, starter example, and pac
18
18
 
19
19
  ## Rendering
20
20
 
21
- | Mode | Description |
22
- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
23
- | `spa` | Render entirely in the browser. Starts from your choice of starter example below. |
24
- | `ssg` | Prerender routes to static HTML at build time, then hydrate in the browser. Scaffolds a small routed app with a server entry and a prerender script. |
25
- | `ssr` | Render each request on a Node server, then hydrate in the browser. Scaffolds a counter whose Flags come from the request, plus an Effect HttpServer host and a `start` script. |
21
+ | Mode | Description |
22
+ | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
23
+ | `spa` | Render entirely in the browser. Starts from your choice of starter example below. |
24
+ | `ssg` | Prerender routes to static HTML at build time, then hydrate in the browser. Scaffolds a small routed app with a server entry. One `vite build` writes every route. |
25
+ | `ssr` | Render each request, then hydrate in the browser. Scaffolds a counter whose Flags come from the request. One `vite build` emits a Web `fetch` handler; `start` runs `node scripts/serve.ts`. |
26
26
 
27
27
  The starter examples apply to `spa` rendering. The `ssg` and `ssr` modes scaffold their own starter apps.
28
28
 
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ const nameSchema = Schema.String.pipe(Schema.check(Schema.makeFilter(value => Op
14
14
  onSome: message => message,
15
15
  }))));
16
16
  const name = Flag.string('name').pipe(Flag.withAlias('n'), Flag.withDescription('The name of the project to create'), Flag.withSchema(nameSchema), Flag.optional);
17
- const rendering = Flag.choice('rendering', RENDERING_VALUES).pipe(Flag.withAlias('r'), Flag.withDescription('How the application renders: spa renders entirely in the browser, ssg prerenders routes to static HTML at build time, ssr renders each request on a Node server'), Flag.optional);
17
+ const rendering = Flag.choice('rendering', RENDERING_VALUES).pipe(Flag.withAlias('r'), Flag.withDescription('How the application renders: spa renders entirely in the browser, ssg prerenders routes to static HTML at build time, ssr renders each request then hydrates'), Flag.optional);
18
18
  const example = Flag.choice('example', EXAMPLE_VALUES).pipe(Flag.withAlias('e'), Flag.withDescription("The example application to start from with spa rendering. Run with no flags for an interactive picker that shows each example's description."), Flag.optional);
19
19
  const packageManager = Flag.choice('package-manager', [
20
20
  'pnpm',
package/dist/rendering.js CHANGED
@@ -14,7 +14,7 @@ export const renderings = [
14
14
  {
15
15
  value: 'ssr',
16
16
  title: 'SSR',
17
- description: 'Render each request on a Node server, then hydrate in the browser',
17
+ description: 'Render each request, then hydrate in the browser',
18
18
  },
19
19
  ];
20
20
  export const Scaffold = Data.taggedEnum();
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
3
+ "semi": false,
4
+ "singleQuote": true,
5
+ "arrowParens": "avoid",
6
+ "trailingComma": "all",
7
+ "printWidth": 80,
8
+ "sortImports": {
9
+ "ignoreCase": false,
10
+ "customGroups": [
11
+ { "groupName": "scoped", "elementNamePattern": ["@*", "@*/**"] }
12
+ ],
13
+ "groups": [
14
+ ["builtin", "external"],
15
+ "scoped",
16
+ ["parent", "sibling", "index"],
17
+ "unknown"
18
+ ]
19
+ },
20
+ "sortPackageJson": false,
21
+ "ignorePatterns": [
22
+ "node_modules",
23
+ "dist",
24
+ "repos",
25
+ "*.tsbuildinfo",
26
+ "pnpm-lock.yaml"
27
+ ]
28
+ }
@@ -19,6 +19,10 @@
19
19
  "typescript/consistent-type-assertions": [
20
20
  "error",
21
21
  { "assertionStyle": "never" }
22
+ ],
23
+ "sort-imports": [
24
+ "error",
25
+ { "ignoreDeclarationSort": true, "ignoreCase": false }
22
26
  ]
23
27
  },
24
28
  "ignorePatterns": [
@@ -149,7 +149,7 @@ type Message = typeof Message.Type
149
149
 
150
150
  Keep the `defineMessageUnion()` declaration and `type Message` alias adjacent. Construct values through the namespace (`Message.ClickedSubmit()`) and handle the union with `Message.match`. Never destructure constructors from `Message` or `OutMessage`; the owning namespace stays visible at every call site.
151
151
 
152
- Keep each case's payload object on one line when it fits. Let Prettier wrap payloads that need more space, so the declaration remains easy to scan as one variant per line.
152
+ Keep each case's payload object on one line when it fits. Let Oxfmt wrap payloads that need more space, so the declaration remains easy to scan as one variant per line.
153
153
 
154
154
  Messages are verb-first past-tense. Common prefixes: `Clicked*`, `Updated*` (input changes and external state updates), `Submitted*`, `Pressed*`, `Selected*`, `Succeeded*` / `Failed*` (paired async results), `Completed*` (every other Command result), `Got*` (child OutMessage in the Submodel pattern).
155
155
 
@@ -7,7 +7,7 @@
7
7
  "build": "vite build",
8
8
  "preview": "vite preview",
9
9
  "typecheck": "tsc --noEmit",
10
- "format": "prettier -w .",
10
+ "format": "oxfmt",
11
11
  "test": "vitest run",
12
12
  "lint": "oxlint src"
13
13
  }
@@ -18,9 +18,7 @@
18
18
  "@foldkit/devtools": "workspace:*",
19
19
  "@foldkit/vite-plugin": "workspace:*",
20
20
  "@tailwindcss/vite": "^4.3.3",
21
- "@trivago/prettier-plugin-sort-imports": "^6.0.2",
22
21
  "happy-dom": "^20.11.13",
23
- "prettier": "^3.9.6",
24
22
  "tailwindcss": "^4.3.3",
25
23
  "typescript": "^7.0.2",
26
24
  "vite": "^8.2.2",
@@ -1,4 +1,5 @@
1
1
  import { HashMap, Option } from 'effect'
2
+ import { evo } from 'foldkit/struct'
2
3
 
3
4
  import { Tabs } from '@foldkit/ui'
4
5
 
@@ -43,28 +44,28 @@ export const loadingPostsModel: Model = {
43
44
  stats: StatsData.Idle(),
44
45
  }
45
46
 
46
- export const loadedPostsModel: Model = {
47
- ...loadingPostsModel,
48
- posts: PostsData.Success({
49
- data: { posts: fixturePosts, fetchedAt: FETCHED_AT },
50
- }),
51
- }
52
-
53
- export const cachedFirstPostModel: Model = {
54
- ...loadedPostsModel,
55
- postDetailById: HashMap.set(
56
- HashMap.empty(),
57
- 'first-post',
58
- PostDetailData.Success({
59
- data: { detail: firstPostDetail, fetchedAt: FETCHED_AT },
47
+ export const loadedPostsModel: Model = evo(loadingPostsModel, {
48
+ posts: () =>
49
+ PostsData.Success({
50
+ data: { posts: fixturePosts, fetchedAt: FETCHED_AT },
60
51
  }),
61
- ),
62
- }
52
+ })
63
53
 
64
- export const loadedStatsModel: Model = {
65
- ...loadedPostsModel,
66
- activeTab: 'Stats',
67
- stats: StatsData.Success({
68
- data: { stats: fixtureStats, fetchedAt: FETCHED_AT },
69
- }),
70
- }
54
+ export const cachedFirstPostModel: Model = evo(loadedPostsModel, {
55
+ postDetailById: () =>
56
+ HashMap.set(
57
+ HashMap.empty(),
58
+ 'first-post',
59
+ PostDetailData.Success({
60
+ data: { detail: firstPostDetail, fetchedAt: FETCHED_AT },
61
+ }),
62
+ ),
63
+ })
64
+
65
+ export const loadedStatsModel: Model = evo(loadedPostsModel, {
66
+ activeTab: () => 'Stats',
67
+ stats: () =>
68
+ StatsData.Success({
69
+ data: { stats: fixtureStats, fetchedAt: FETCHED_AT },
70
+ }),
71
+ })
@@ -12,7 +12,6 @@ import {
12
12
  import { describe, test } from 'vitest'
13
13
 
14
14
  import { Tabs } from '@foldkit/ui'
15
- import { Message as TabsMessage } from '@foldkit/ui/tabs'
16
15
 
17
16
  import { FetchPostDetail, FetchStats, Message, update, view } from './main'
18
17
  import {
@@ -22,11 +21,11 @@ import {
22
21
  fixtureStats,
23
22
  loadedPostsModel,
24
23
  loadingPostsModel,
25
- } from './main.fixtures'
24
+ } from './main.fixture'
26
25
 
27
26
  const resolveFocusTab = Command.resolve(
28
27
  Tabs.FocusTab,
29
- TabsMessage.CompletedFocusTab(),
28
+ Tabs.Message.CompletedFocusTab(),
30
29
  )
31
30
 
32
31
  describe('view', () => {
@@ -1,9 +1,9 @@
1
1
  import { HashMap, Option, Result } from 'effect'
2
2
  import { Command, given, message, model, story } from 'foldkit/story'
3
+ import { evo } from 'foldkit/struct'
3
4
  import { expect, test } from 'vitest'
4
5
 
5
6
  import { Tabs } from '@foldkit/ui'
6
- import { Message as TabsMessage } from '@foldkit/ui/tabs'
7
7
 
8
8
  import {
9
9
  FetchPostDetail,
@@ -23,7 +23,7 @@ import {
23
23
  fixtureStats,
24
24
  loadedPostsModel,
25
25
  loadedStatsModel,
26
- } from './main.fixtures'
26
+ } from './main.fixture'
27
27
 
28
28
  const postDetailTag = (model: Model, postId: string): string =>
29
29
  HashMap.get(model.postDetailById, postId).pipe(
@@ -32,16 +32,16 @@ const postDetailTag = (model: Model, postId: string): string =>
32
32
  )
33
33
 
34
34
  const selectedPostsTab = Message.GotTabsMessage({
35
- message: TabsMessage.SelectedTab({ index: 0, value: 'Posts' }),
35
+ message: Tabs.Message.SelectedTab({ index: 0, value: 'Posts' }),
36
36
  })
37
37
 
38
38
  const selectedStatsTab = Message.GotTabsMessage({
39
- message: TabsMessage.SelectedTab({ index: 1, value: 'Stats' }),
39
+ message: Tabs.Message.SelectedTab({ index: 1, value: 'Stats' }),
40
40
  })
41
41
 
42
42
  const resolveFocusTab = Command.resolve(
43
43
  Tabs.FocusTab,
44
- TabsMessage.CompletedFocusTab(),
44
+ Tabs.Message.CompletedFocusTab(),
45
45
  )
46
46
 
47
47
  test('first visit to the Stats tab fetches stats', () => {
@@ -97,7 +97,7 @@ test('a revalidation tick keeps stale stats on screen while refetching', () => {
97
97
  FetchStats,
98
98
  Message.SettledFetchStats({
99
99
  result: Result.succeed({
100
- stats: { ...fixtureStats, activeUsers: 99 },
100
+ stats: evo(fixtureStats, { activeUsers: () => 99 }),
101
101
  fetchedAt: FETCHED_AT + 5000,
102
102
  }),
103
103
  }),
@@ -135,7 +135,7 @@ test('a failed refresh keeps the stale stats on screen with the error', () => {
135
135
  test('refresh clicks during an in-flight fetch are deduplicated', () => {
136
136
  story(
137
137
  update,
138
- given({ ...loadedStatsModel, stats: StatsData.Loading() }),
138
+ given(evo(loadedStatsModel, { stats: () => StatsData.Loading() })),
139
139
  message(Message.ClickedRefreshStats()),
140
140
  Command.expectNone(),
141
141
  )
@@ -144,12 +144,14 @@ test('refresh clicks during an in-flight fetch are deduplicated', () => {
144
144
  test('a revalidation tick during a refresh is deduplicated', () => {
145
145
  story(
146
146
  update,
147
- given({
148
- ...loadedStatsModel,
149
- stats: StatsData.Refreshing({
150
- data: { stats: fixtureStats, fetchedAt: FETCHED_AT },
147
+ given(
148
+ evo(loadedStatsModel, {
149
+ stats: () =>
150
+ StatsData.Refreshing({
151
+ data: { stats: fixtureStats, fetchedAt: FETCHED_AT },
152
+ }),
151
153
  }),
152
- }),
154
+ ),
153
155
  message(Message.TickedRevalidateStats()),
154
156
  Command.expectNone(),
155
157
  )
@@ -184,10 +186,11 @@ test('invalidating posts refetches while keeping the current list', () => {
184
186
  test('retrying failed posts shows the loading state and refetches', () => {
185
187
  story(
186
188
  update,
187
- given({
188
- ...loadedPostsModel,
189
- posts: PostsData.Failure({ error: 'The server is down.' }),
190
- }),
189
+ given(
190
+ evo(loadedPostsModel, {
191
+ posts: () => PostsData.Failure({ error: 'The server is down.' }),
192
+ }),
193
+ ),
191
194
  message(Message.ClickedRetryPosts()),
192
195
  model(model => {
193
196
  expect(model.posts._tag).toBe('Loading')
@@ -270,14 +273,16 @@ test('a failed post detail fetch lands in Failure and retry refetches', () => {
270
273
  test('revisiting a post with a cached failure shows it without refetching', () => {
271
274
  story(
272
275
  update,
273
- given({
274
- ...loadedPostsModel,
275
- postDetailById: HashMap.set(
276
- HashMap.empty(),
277
- 'first-post',
278
- PostDetailData.Failure({ error: 'The connection dropped.' }),
279
- ),
280
- }),
276
+ given(
277
+ evo(loadedPostsModel, {
278
+ postDetailById: () =>
279
+ HashMap.set(
280
+ HashMap.empty(),
281
+ 'first-post',
282
+ PostDetailData.Failure({ error: 'The connection dropped.' }),
283
+ ),
284
+ }),
285
+ ),
281
286
  message(Message.ClickedPost({ postId: 'first-post' })),
282
287
  Command.expectNone(),
283
288
  model(model => {
@@ -19,9 +19,7 @@
19
19
  "@foldkit/devtools": "workspace:*",
20
20
  "@foldkit/vite-plugin": "workspace:*",
21
21
  "@tailwindcss/vite": "^4.3.3",
22
- "@trivago/prettier-plugin-sort-imports": "^6.0.2",
23
22
  "happy-dom": "^20.11.13",
24
- "prettier": "^3.9.6",
25
23
  "tailwindcss": "^4.3.3",
26
24
  "typescript": "^7.0.2",
27
25
  "vite": "^8.2.2",
@@ -11,21 +11,14 @@ import {
11
11
  type,
12
12
  within,
13
13
  } from 'foldkit/scene'
14
+ import { evo } from 'foldkit/struct'
14
15
  import { describe, test } from 'vitest'
15
16
 
16
- import {
17
- Message,
18
- Model,
19
- SimulateAuthRequest,
20
- initModel,
21
- update,
22
- view,
23
- } from './login'
17
+ import { Message, SimulateAuthRequest, initModel, update, view } from './login'
24
18
 
25
- const validModel = Model.make({
26
- ...initModel(),
27
- email: Valid({ value: 'alice@example.com' }),
28
- password: Valid({ value: 'password' }),
19
+ const validModel = evo(initModel(), {
20
+ email: () => Valid({ value: 'alice@example.com' }),
21
+ password: () => Valid({ value: 'password' }),
29
22
  })
30
23
 
31
24
  const heading = role('heading', { name: 'Sign In' })
@@ -8,21 +8,20 @@ import {
8
8
  model,
9
9
  story,
10
10
  } from 'foldkit/story'
11
+ import { evo } from 'foldkit/struct'
11
12
  import { describe, expect, test } from 'vitest'
12
13
 
13
14
  import {
14
15
  Message,
15
- Model,
16
16
  OutMessage,
17
17
  SimulateAuthRequest,
18
18
  initModel,
19
19
  update,
20
20
  } from './login'
21
21
 
22
- const validModel = Model.make({
23
- ...initModel(),
24
- email: Valid({ value: 'alice@example.com' }),
25
- password: Valid({ value: 'password' }),
22
+ const validModel = evo(initModel(), {
23
+ email: () => Valid({ value: 'alice@example.com' }),
24
+ password: () => Valid({ value: 'password' }),
26
25
  })
27
26
 
28
27
  const aliceSession = { userId: '1', email: 'alice@example.com', name: 'alice' }
@@ -8,6 +8,7 @@ import {
8
8
  submit,
9
9
  text,
10
10
  } from 'foldkit/scene'
11
+ import { evo } from 'foldkit/struct'
11
12
  import { describe, test } from 'vitest'
12
13
 
13
14
  import { SaveSession } from './command'
@@ -16,7 +17,6 @@ import { Message } from './message'
16
17
  import { LoggedOut } from './model'
17
18
  import {
18
19
  Message as LoginMessage,
19
- Model as LoginModel,
20
20
  SimulateAuthRequest,
21
21
  initModel as initLoginModel,
22
22
  } from './page/loggedOut/page/login'
@@ -26,10 +26,9 @@ import { view } from './view'
26
26
 
27
27
  const validModel = LoggedOut.Model({
28
28
  route: AppRoute.Login(),
29
- loginModel: LoginModel.make({
30
- ...initLoginModel(),
31
- email: Valid({ value: 'alice@example.com' }),
32
- password: Valid({ value: 'password' }),
29
+ loginModel: evo(initLoginModel(), {
30
+ email: () => Valid({ value: 'alice@example.com' }),
31
+ password: () => Valid({ value: 'password' }),
33
32
  }),
34
33
  })
35
34
 
@@ -18,9 +18,7 @@
18
18
  "@foldkit/devtools": "workspace:*",
19
19
  "@foldkit/vite-plugin": "workspace:*",
20
20
  "@tailwindcss/vite": "^4.3.3",
21
- "@trivago/prettier-plugin-sort-imports": "^6.0.2",
22
21
  "happy-dom": "^20.11.13",
23
- "prettier": "^3.9.6",
24
22
  "tailwindcss": "^4.3.3",
25
23
  "typescript": "^7.0.2",
26
24
  "vite": "^8.2.2",
@@ -1,4 +1,5 @@
1
1
  import { click, expect, given, role, scene, text } from 'foldkit/scene'
2
+ import { evo } from 'foldkit/struct'
2
3
  import { describe, test } from 'vitest'
3
4
 
4
5
  import { type Model, update, view } from './main'
@@ -33,15 +34,14 @@ describe('view', () => {
33
34
  })
34
35
 
35
36
  test('the ball count reflects spawned balls in the Model', () => {
36
- const populatedModel: Model = {
37
- ...emptyModel,
38
- balls: [
37
+ const populatedModel: Model = evo(emptyModel, {
38
+ balls: () => [
39
39
  { id: 0, x: 10, y: 20, vx: 1, vy: 1, radius: 8, color: '#ff2d55' },
40
40
  { id: 1, x: 30, y: 40, vx: -1, vy: 1, radius: 12, color: '#5ac8fa' },
41
41
  { id: 2, x: 50, y: 60, vx: 1, vy: -1, radius: 16, color: '#34c759' },
42
42
  ],
43
- nextId: 3,
44
- }
43
+ nextId: () => 3,
44
+ })
45
45
 
46
46
  scene(
47
47
  { update, view },
@@ -51,13 +51,12 @@ describe('view', () => {
51
51
  })
52
52
 
53
53
  test('Clear empties the rendered ball count', () => {
54
- const populatedModel: Model = {
55
- ...emptyModel,
56
- balls: [
54
+ const populatedModel: Model = evo(emptyModel, {
55
+ balls: () => [
57
56
  { id: 0, x: 10, y: 20, vx: 1, vy: 1, radius: 8, color: '#ff2d55' },
58
57
  ],
59
- nextId: 1,
60
- }
58
+ nextId: () => 1,
59
+ })
61
60
 
62
61
  scene(
63
62
  { update, view },
@@ -1,4 +1,5 @@
1
1
  import { Command, given, message, model, story } from 'foldkit/story'
2
+ import { evo } from 'foldkit/struct'
2
3
  import { describe, expect, test } from 'vitest'
3
4
 
4
5
  import { GenerateBall, Message, type Model, update } from './main'
@@ -9,14 +10,13 @@ const emptyModel: Model = {
9
10
  isRunning: true,
10
11
  }
11
12
 
12
- const populatedModel: Model = {
13
- ...emptyModel,
14
- balls: [
13
+ const populatedModel: Model = evo(emptyModel, {
14
+ balls: () => [
15
15
  { id: 0, x: 100, y: 100, vx: 50, vy: 50, radius: 10, color: '#ff2d55' },
16
16
  { id: 1, x: 200, y: 200, vx: -50, vy: -50, radius: 15, color: '#5ac8fa' },
17
17
  ],
18
- nextId: 2,
19
- }
18
+ nextId: () => 2,
19
+ })
20
20
 
21
21
  describe('update', () => {
22
22
  describe('spawning balls', () => {
@@ -97,9 +97,8 @@ describe('update', () => {
97
97
  })
98
98
 
99
99
  test('bounces a ball off the canvas edges, flipping its velocity', () => {
100
- const movingRightModel: Model = {
101
- ...emptyModel,
102
- balls: [
100
+ const movingRightModel: Model = evo(emptyModel, {
101
+ balls: () => [
103
102
  {
104
103
  id: 0,
105
104
  x: 595,
@@ -110,8 +109,8 @@ describe('update', () => {
110
109
  color: '#fff',
111
110
  },
112
111
  ],
113
- nextId: 1,
114
- }
112
+ nextId: () => 1,
113
+ })
115
114
 
116
115
  story(
117
116
  update,
@@ -20,9 +20,7 @@
20
20
  "@foldkit/devtools": "workspace:*",
21
21
  "@foldkit/vite-plugin": "workspace:*",
22
22
  "@tailwindcss/vite": "^4.3.3",
23
- "@trivago/prettier-plugin-sort-imports": "^6.0.2",
24
23
  "happy-dom": "^20.11.13",
25
- "prettier": "^3.9.6",
26
24
  "tailwindcss": "^4.3.3",
27
25
  "typescript": "^7.0.2",
28
26
  "vite": "^8.2.2",
@@ -1,4 +1,5 @@
1
1
  import { Option } from 'effect'
2
+ import { evo } from 'foldkit/struct'
2
3
 
3
4
  import { RadioGroup } from '@foldkit/ui'
4
5
 
@@ -119,10 +120,9 @@ export const loadingModel = Model.make({
119
120
  maybeSelectedDatumId: Option.none(),
120
121
  })
121
122
 
122
- export const readyModel = Model.make({
123
- ...loadingModel,
124
- telemetry: TelemetryAsyncData.Success({ data: sampleTelemetry }),
125
- maybeChartHostId: Option.some('test-chart-host'),
123
+ export const readyModel = evo(loadingModel, {
124
+ telemetry: () => TelemetryAsyncData.Success({ data: sampleTelemetry }),
125
+ maybeChartHostId: () => Option.some('test-chart-host'),
126
126
  })
127
127
 
128
128
  export const mockGitHubRepository = {
@@ -9,13 +9,13 @@ import {
9
9
  scene,
10
10
  text,
11
11
  } from 'foldkit/scene'
12
+ import { evo } from 'foldkit/struct'
12
13
  import { describe, test } from 'vitest'
13
14
 
14
15
  import { RadioGroup } from '@foldkit/ui'
15
- import { Message as RadioGroupMessage } from '@foldkit/ui/radioGroup'
16
16
 
17
17
  import { SyncChart } from './command'
18
- import { loadingModel, readyModel, sampleTelemetry } from './main.fixtures'
18
+ import { loadingModel, readyModel, sampleTelemetry } from './main.fixture'
19
19
  import { Message } from './message'
20
20
  import { TelemetryAsyncData } from './model'
21
21
  import { update } from './update'
@@ -34,7 +34,7 @@ const acknowledgeChartSync = Command.resolve(
34
34
 
35
35
  const resolveFocusOption = Command.resolve(
36
36
  RadioGroup.FocusOption,
37
- RadioGroupMessage.CompletedFocusOption(),
37
+ RadioGroup.Message.CompletedFocusOption(),
38
38
  )
39
39
 
40
40
  describe('view', () => {
@@ -78,10 +78,12 @@ describe('view', () => {
78
78
  test('refreshing state keeps the dashboard visible', () => {
79
79
  scene(
80
80
  { update, view },
81
- given({
82
- ...readyModel,
83
- telemetry: TelemetryAsyncData.Refreshing({ data: sampleTelemetry }),
84
- }),
81
+ given(
82
+ evo(readyModel, {
83
+ telemetry: () =>
84
+ TelemetryAsyncData.Refreshing({ data: sampleTelemetry }),
85
+ }),
86
+ ),
85
87
  acknowledgeChartMount,
86
88
  acknowledgeChartSync,
87
89
  expect(text('Refreshing public data')).toExist(),
@@ -92,10 +94,11 @@ describe('view', () => {
92
94
  test('failure without stale data shows retry', () => {
93
95
  scene(
94
96
  { update, view },
95
- given({
96
- ...loadingModel,
97
- telemetry: TelemetryAsyncData.Failure({ error: 'offline' }),
98
- }),
97
+ given(
98
+ evo(loadingModel, {
99
+ telemetry: () => TelemetryAsyncData.Failure({ error: 'offline' }),
100
+ }),
101
+ ),
99
102
  expect(label('Telemetry failed')).toExist(),
100
103
  expect(role('button', { name: 'Retry' })).toExist(),
101
104
  )
@@ -1,8 +1,8 @@
1
1
  import { Command, given, message, model, story } from 'foldkit/story'
2
+ import { evo } from 'foldkit/struct'
2
3
  import { expect, test } from 'vitest'
3
4
 
4
5
  import { RadioGroup } from '@foldkit/ui'
5
- import { Message as RadioGroupMessage } from '@foldkit/ui/radioGroup'
6
6
 
7
7
  import { FetchTelemetry, SyncChart } from './command'
8
8
  import {
@@ -11,14 +11,14 @@ import {
11
11
  chartModes,
12
12
  packageIds,
13
13
  } from './domain'
14
- import { loadingModel, readyModel, sampleTelemetry } from './main.fixtures'
14
+ import { loadingModel, readyModel, sampleTelemetry } from './main.fixture'
15
15
  import { Message } from './message'
16
16
  import { TelemetryAsyncData } from './model'
17
17
  import { update } from './update'
18
18
 
19
19
  const selectedChartMode = (chartMode: ChartMode) =>
20
20
  Message.GotChartModeRadioGroupMessage({
21
- message: RadioGroupMessage.SelectedOption({
21
+ message: RadioGroup.Message.SelectedOption({
22
22
  index: chartModes.indexOf(chartMode),
23
23
  value: chartMode,
24
24
  }),
@@ -26,7 +26,7 @@ const selectedChartMode = (chartMode: ChartMode) =>
26
26
 
27
27
  const selectedPackage = (packageId: PackageId) =>
28
28
  Message.GotPackageRadioGroupMessage({
29
- message: RadioGroupMessage.SelectedOption({
29
+ message: RadioGroup.Message.SelectedOption({
30
30
  index: packageIds.indexOf(packageId),
31
31
  value: packageId,
32
32
  }),
@@ -34,7 +34,7 @@ const selectedPackage = (packageId: PackageId) =>
34
34
 
35
35
  const resolveChartModeFocus = Command.resolve(
36
36
  RadioGroup.FocusOption,
37
- RadioGroupMessage.CompletedFocusOption(),
37
+ RadioGroup.Message.CompletedFocusOption(),
38
38
  )
39
39
 
40
40
  test('mounting the chart syncs current telemetry into ECharts', () => {
@@ -102,10 +102,12 @@ test('refreshing with data keeps the old dashboard while fetching', () => {
102
102
  test('a failed refresh preserves stale data in the stale state', () => {
103
103
  story(
104
104
  update,
105
- given({
106
- ...readyModel,
107
- telemetry: TelemetryAsyncData.Refreshing({ data: sampleTelemetry }),
108
- }),
105
+ given(
106
+ evo(readyModel, {
107
+ telemetry: () =>
108
+ TelemetryAsyncData.Refreshing({ data: sampleTelemetry }),
109
+ }),
110
+ ),
109
111
  message(Message.FailedFetchTelemetry({ error: 'rate limited' })),
110
112
  model(model => {
111
113
  expect(model.telemetry._tag).toBe('Stale')