create-foldkit-app 0.32.1 → 0.33.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 (117) hide show
  1. package/dist/templates/base/.oxfmtrc.json +28 -0
  2. package/dist/templates/base/.oxlintrc.json +4 -0
  3. package/dist/templates/base/FOLDKIT.md +1 -1
  4. package/dist/templates/base/package.json +1 -1
  5. package/dist/templates/examples/api-cache/package.json +0 -2
  6. package/dist/templates/examples/api-cache/src/scene.test.ts +1 -2
  7. package/dist/templates/examples/api-cache/src/story.test.ts +3 -4
  8. package/dist/templates/examples/auth/package.json +0 -2
  9. package/dist/templates/examples/canvas-art/package.json +0 -2
  10. package/dist/templates/examples/charting/package.json +0 -2
  11. package/dist/templates/examples/charting/src/scene.test.ts +1 -2
  12. package/dist/templates/examples/charting/src/story.test.ts +3 -4
  13. package/dist/templates/examples/counter/package.json +0 -2
  14. package/dist/templates/examples/counters/package.json +0 -2
  15. package/dist/templates/examples/crash-view/package.json +0 -2
  16. package/dist/templates/examples/embedding/package.json +0 -2
  17. package/dist/templates/examples/form/package.json +0 -2
  18. package/dist/templates/examples/generative-art/package.json +0 -2
  19. package/dist/templates/examples/interrupting-commands/package.json +0 -2
  20. package/dist/templates/examples/job-application/package.json +0 -2
  21. package/dist/templates/examples/job-application/src/main.fixture.ts +69 -0
  22. package/dist/templates/examples/job-application/src/main.ts +1 -1
  23. package/dist/templates/examples/job-application/src/scene.test.ts +6 -66
  24. package/dist/templates/examples/job-application/src/step/attachments/index.ts +2 -0
  25. package/dist/templates/examples/job-application/src/step/attachments/scene.test.ts +19 -0
  26. package/dist/templates/examples/job-application/src/step/attachments/story.test.ts +46 -0
  27. package/dist/templates/examples/job-application/src/{view/attachments.ts → step/attachments/view.ts} +4 -11
  28. package/dist/templates/examples/job-application/src/{view → step/coverLetter}/coverLetter.ts +36 -7
  29. package/dist/templates/examples/job-application/src/step/coverLetter/index.ts +1 -0
  30. package/dist/templates/examples/job-application/src/step/coverLetter/scene.test.ts +24 -0
  31. package/dist/templates/examples/job-application/src/step/coverLetter/story.test.ts +19 -0
  32. package/dist/templates/examples/job-application/src/step/education/{entry.ts → entry/entry.ts} +2 -2
  33. package/dist/templates/examples/job-application/src/step/education/entry/index.ts +11 -0
  34. package/dist/templates/examples/job-application/src/step/education/entry/story.test.ts +39 -0
  35. package/dist/templates/examples/job-application/src/step/education/entry/view.ts +160 -0
  36. package/dist/templates/examples/job-application/src/step/education/index.ts +11 -1
  37. package/dist/templates/examples/job-application/src/step/education/scene.test.ts +20 -0
  38. package/dist/templates/examples/job-application/src/step/education/story.test.ts +87 -0
  39. package/dist/templates/examples/job-application/src/{view/education.ts → step/education/view.ts} +6 -10
  40. package/dist/templates/examples/job-application/src/step/personalInfo/index.ts +11 -0
  41. package/dist/templates/examples/job-application/src/step/{personalInfo.ts → personalInfo/personalInfo.ts} +2 -2
  42. package/dist/templates/examples/job-application/src/step/personalInfo/scene.test.ts +38 -0
  43. package/dist/templates/examples/job-application/src/step/personalInfo/story.test.ts +87 -0
  44. package/dist/templates/examples/job-application/src/{view/personalInfo.ts → step/personalInfo/view.ts} +24 -34
  45. package/dist/templates/examples/job-application/src/step/skills/{entry.ts → entry/entry.ts} +2 -2
  46. package/dist/templates/examples/job-application/src/step/skills/entry/index.ts +11 -0
  47. package/dist/templates/examples/job-application/src/step/skills/entry/story.test.ts +27 -0
  48. package/dist/templates/examples/job-application/src/{view/skillEntry.ts → step/skills/entry/view.ts} +9 -12
  49. package/dist/templates/examples/job-application/src/step/skills/index.ts +11 -1
  50. package/dist/templates/examples/job-application/src/step/skills/scene.test.ts +16 -0
  51. package/dist/templates/examples/job-application/src/step/skills/story.test.ts +85 -0
  52. package/dist/templates/examples/job-application/src/step/skills/view.ts +47 -0
  53. package/dist/templates/examples/job-application/src/step/workHistory/{entry.ts → entry/entry.ts} +1 -1
  54. package/dist/templates/examples/job-application/src/step/workHistory/entry/index.ts +11 -0
  55. package/dist/templates/examples/job-application/src/step/workHistory/entry/story.test.ts +42 -0
  56. package/dist/templates/examples/job-application/src/{view/workEntry.ts → step/workHistory/entry/view.ts} +27 -42
  57. package/dist/templates/examples/job-application/src/step/workHistory/index.ts +11 -1
  58. package/dist/templates/examples/job-application/src/step/workHistory/scene.test.ts +20 -0
  59. package/dist/templates/examples/job-application/src/step/workHistory/story.test.ts +87 -0
  60. package/dist/templates/examples/job-application/src/{view/workHistory.ts → step/workHistory/view.ts} +4 -8
  61. package/dist/templates/examples/job-application/src/story.test.ts +28 -419
  62. package/dist/templates/examples/job-application/src/view/datePicker.ts +4 -4
  63. package/dist/templates/examples/job-application/src/view/field.ts +3 -3
  64. package/dist/templates/examples/job-application/src/view/index.ts +3 -358
  65. package/dist/templates/examples/job-application/src/view/stepNav.ts +2 -2
  66. package/dist/templates/examples/job-application/src/view/view.ts +359 -0
  67. package/dist/templates/examples/kanban/package.json +0 -2
  68. package/dist/templates/examples/kanban/src/scene.test.ts +1 -1
  69. package/dist/templates/examples/kanban/src/story.test.ts +22 -23
  70. package/dist/templates/examples/kanban/src/view/index.ts +1 -71
  71. package/dist/templates/examples/kanban/src/view/view.ts +71 -0
  72. package/dist/templates/examples/managed-resource-layer/package.json +0 -2
  73. package/dist/templates/examples/map/package.json +0 -2
  74. package/dist/templates/examples/pixel-art/package.json +0 -2
  75. package/dist/templates/examples/pixel-art/src/scene.test.ts +16 -9
  76. package/dist/templates/examples/pixel-art/src/story.test.ts +12 -7
  77. package/dist/templates/examples/query-sync/package.json +0 -2
  78. package/dist/templates/examples/query-sync/src/story.test.ts +4 -5
  79. package/dist/templates/examples/route-transitions/package.json +0 -2
  80. package/dist/templates/examples/routing/package.json +0 -2
  81. package/dist/templates/examples/routing/src/page/people.scene.test.ts +41 -0
  82. package/dist/templates/examples/routing/src/page/people.story.test.ts +73 -0
  83. package/dist/templates/examples/routing/src/scene.test.ts +2 -17
  84. package/dist/templates/examples/routing/src/story.test.ts +4 -34
  85. package/dist/templates/examples/shopping-cart/package.json +0 -2
  86. package/dist/templates/examples/shopping-cart/src/page/products.scene.test.ts +20 -0
  87. package/dist/templates/examples/shopping-cart/src/page/products.story.test.ts +56 -0
  88. package/dist/templates/examples/shopping-cart/src/page/products.ts +1 -1
  89. package/dist/templates/examples/shopping-cart/src/scene.test.ts +1 -4
  90. package/dist/templates/examples/shopping-cart/src/story.test.ts +3 -4
  91. package/dist/templates/examples/slow-warnings/package.json +0 -2
  92. package/dist/templates/examples/snake/package.json +0 -2
  93. package/dist/templates/examples/ssg/package.json +0 -2
  94. package/dist/templates/examples/ssr/package.json +0 -2
  95. package/dist/templates/examples/state-machine/package.json +0 -2
  96. package/dist/templates/examples/state-machine/src/main.ts +79 -87
  97. package/dist/templates/examples/state-machine/src/scene.test.ts +1 -2
  98. package/dist/templates/examples/stopwatch/package.json +0 -2
  99. package/dist/templates/examples/todo/package.json +0 -2
  100. package/dist/templates/examples/ui-showcase/package.json +0 -2
  101. package/dist/templates/examples/ui-showcase/src/story.test.ts +1 -2
  102. package/dist/templates/examples/view-transitions/package.json +0 -2
  103. package/dist/templates/examples/view-transitions/src/styles.css +3 -3
  104. package/dist/templates/examples/weather/package.json +0 -2
  105. package/dist/templates/examples/web-components/package.json +0 -2
  106. package/dist/templates/examples/websocket-chat/package.json +0 -2
  107. package/dist/templates/release.json +9 -10
  108. package/dist/templates/rendering/ssg/package.json +1 -1
  109. package/dist/templates/rendering/ssr/package.json +1 -1
  110. package/dist/utils/packages.js +1 -2
  111. package/package.json +3 -3
  112. package/dist/templates/base/.prettierignore +0 -5
  113. package/dist/templates/base/.prettierrc +0 -13
  114. package/dist/templates/examples/job-application/src/step/coverLetter.ts +0 -34
  115. package/dist/templates/examples/job-application/src/view/educationEntry.ts +0 -165
  116. package/dist/templates/examples/job-application/src/view/skills.ts +0 -49
  117. /package/dist/templates/examples/job-application/src/step/{attachments.ts → attachments/attachments.ts} +0 -0
@@ -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",
@@ -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 {
@@ -26,7 +25,7 @@ import {
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', () => {
@@ -3,7 +3,6 @@ import { Command, given, message, model, story } from 'foldkit/story'
3
3
  import { expect, test } from 'vitest'
4
4
 
5
5
  import { Tabs } from '@foldkit/ui'
6
- import { Message as TabsMessage } from '@foldkit/ui/tabs'
7
6
 
8
7
  import {
9
8
  FetchPostDetail,
@@ -32,16 +31,16 @@ const postDetailTag = (model: Model, postId: string): string =>
32
31
  )
33
32
 
34
33
  const selectedPostsTab = Message.GotTabsMessage({
35
- message: TabsMessage.SelectedTab({ index: 0, value: 'Posts' }),
34
+ message: Tabs.Message.SelectedTab({ index: 0, value: 'Posts' }),
36
35
  })
37
36
 
38
37
  const selectedStatsTab = Message.GotTabsMessage({
39
- message: TabsMessage.SelectedTab({ index: 1, value: 'Stats' }),
38
+ message: Tabs.Message.SelectedTab({ index: 1, value: 'Stats' }),
40
39
  })
41
40
 
42
41
  const resolveFocusTab = Command.resolve(
43
42
  Tabs.FocusTab,
44
- TabsMessage.CompletedFocusTab(),
43
+ Tabs.Message.CompletedFocusTab(),
45
44
  )
46
45
 
47
46
  test('first visit to the Stats tab fetches stats', () => {
@@ -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",
@@ -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",
@@ -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",
@@ -12,7 +12,6 @@ import {
12
12
  import { describe, test } from 'vitest'
13
13
 
14
14
  import { RadioGroup } from '@foldkit/ui'
15
- import { Message as RadioGroupMessage } from '@foldkit/ui/radioGroup'
16
15
 
17
16
  import { SyncChart } from './command'
18
17
  import { loadingModel, readyModel, sampleTelemetry } from './main.fixtures'
@@ -34,7 +33,7 @@ const acknowledgeChartSync = Command.resolve(
34
33
 
35
34
  const resolveFocusOption = Command.resolve(
36
35
  RadioGroup.FocusOption,
37
- RadioGroupMessage.CompletedFocusOption(),
36
+ RadioGroup.Message.CompletedFocusOption(),
38
37
  )
39
38
 
40
39
  describe('view', () => {
@@ -2,7 +2,6 @@ import { Command, given, message, model, story } from 'foldkit/story'
2
2
  import { expect, test } from 'vitest'
3
3
 
4
4
  import { RadioGroup } from '@foldkit/ui'
5
- import { Message as RadioGroupMessage } from '@foldkit/ui/radioGroup'
6
5
 
7
6
  import { FetchTelemetry, SyncChart } from './command'
8
7
  import {
@@ -18,7 +17,7 @@ import { update } from './update'
18
17
 
19
18
  const selectedChartMode = (chartMode: ChartMode) =>
20
19
  Message.GotChartModeRadioGroupMessage({
21
- message: RadioGroupMessage.SelectedOption({
20
+ message: RadioGroup.Message.SelectedOption({
22
21
  index: chartModes.indexOf(chartMode),
23
22
  value: chartMode,
24
23
  }),
@@ -26,7 +25,7 @@ const selectedChartMode = (chartMode: ChartMode) =>
26
25
 
27
26
  const selectedPackage = (packageId: PackageId) =>
28
27
  Message.GotPackageRadioGroupMessage({
29
- message: RadioGroupMessage.SelectedOption({
28
+ message: RadioGroup.Message.SelectedOption({
30
29
  index: packageIds.indexOf(packageId),
31
30
  value: packageId,
32
31
  }),
@@ -34,7 +33,7 @@ const selectedPackage = (packageId: PackageId) =>
34
33
 
35
34
  const resolveChartModeFocus = Command.resolve(
36
35
  RadioGroup.FocusOption,
37
- RadioGroupMessage.CompletedFocusOption(),
36
+ RadioGroup.Message.CompletedFocusOption(),
38
37
  )
39
38
 
40
39
  test('mounting the chart syncs current telemetry into ECharts', () => {
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -0,0 +1,69 @@
1
+ import { Array } from 'effect'
2
+ import { Calendar } from 'foldkit'
3
+ import { Valid } from 'foldkit/fieldValidation'
4
+ import { evo } from 'foldkit/struct'
5
+
6
+ import { Menu, Tabs } from '@foldkit/ui'
7
+
8
+ import { type Model, Submission } from './model'
9
+ import {
10
+ Attachments,
11
+ CoverLetter,
12
+ Education,
13
+ PersonalInfo,
14
+ Skills,
15
+ WorkHistory,
16
+ } from './step'
17
+
18
+ export const today = Calendar.make(2026, 4, 16)
19
+
20
+ export const initialModel: Model = {
21
+ currentStep: 'PersonalInfo',
22
+ personalInfo: PersonalInfo.init(today),
23
+ workHistory: WorkHistory.init(today, 'work-history-entry-1'),
24
+ education: Education.init(today, 'education-entry-1'),
25
+ skills: Skills.init('skills-entry-1'),
26
+ coverLetter: CoverLetter.init(),
27
+ attachments: Attachments.init(),
28
+ isPreviewVisible: false,
29
+ submission: Submission.NotSubmitted(),
30
+ stepMenu: Menu.init({ id: 'step-menu' }),
31
+ stepTabs: Tabs.init({ id: 'step-tabs' }),
32
+ isSubmitAttempted: false,
33
+ }
34
+
35
+ export const completeModel: Model = evo(initialModel, {
36
+ personalInfo: personalInfo =>
37
+ evo(personalInfo, {
38
+ firstName: () => Valid({ value: 'Jane' }),
39
+ lastName: () => Valid({ value: 'Doe' }),
40
+ email: () => Valid({ value: 'jane@example.com' }),
41
+ }),
42
+ workHistory: workHistory =>
43
+ evo(workHistory, {
44
+ entries: Array.map(entry =>
45
+ evo(entry, {
46
+ company: () => Valid({ value: 'Foldkit' }),
47
+ title: () => Valid({ value: 'Engineer' }),
48
+ }),
49
+ ),
50
+ }),
51
+ education: education =>
52
+ evo(education, {
53
+ entries: Array.map(entry =>
54
+ evo(entry, {
55
+ school: () => Valid({ value: 'MIT' }),
56
+ degree: () => Valid({ value: 'BS' }),
57
+ fieldOfStudy: () => Valid({ value: 'CS' }),
58
+ }),
59
+ ),
60
+ }),
61
+ skills: skills =>
62
+ evo(skills, {
63
+ entries: Array.map(entry =>
64
+ evo(entry, {
65
+ name: () => Valid({ value: 'TypeScript' }),
66
+ }),
67
+ ),
68
+ }),
69
+ })
@@ -15,7 +15,7 @@ import {
15
15
  WorkHistory,
16
16
  } from './step'
17
17
  import { update } from './update'
18
- import { view } from './view'
18
+ import { view } from './view/view'
19
19
 
20
20
  // FLAGS
21
21
 
@@ -1,5 +1,4 @@
1
- import { Calendar } from 'foldkit'
2
- import { Valid, Validating } from 'foldkit/fieldValidation'
1
+ import { Validating } from 'foldkit/fieldValidation'
3
2
  import {
4
3
  Command,
5
4
  click,
@@ -12,75 +11,16 @@ import {
12
11
  } from 'foldkit/scene'
13
12
  import { describe, test } from 'vitest'
14
13
 
15
- import { Menu, Tabs } from '@foldkit/ui'
16
- import { Message as TabsMessage } from '@foldkit/ui/tabs'
14
+ import { Tabs } from '@foldkit/ui'
17
15
 
18
- import { type Model, Submission } from './model'
19
- import {
20
- Attachments,
21
- CoverLetter,
22
- Education,
23
- PersonalInfo,
24
- Skills,
25
- WorkHistory,
26
- } from './step'
16
+ import { completeModel, initialModel } from './main.fixture'
17
+ import { Submission } from './model'
27
18
  import { update } from './update'
28
- import { view } from './view'
29
-
30
- const today = Calendar.make(2026, 4, 16)
31
-
32
- const initialModel: Model = {
33
- currentStep: 'PersonalInfo',
34
- personalInfo: PersonalInfo.init(today),
35
- workHistory: WorkHistory.init(today, 'work-history-entry-1'),
36
- education: Education.init(today, 'education-entry-1'),
37
- skills: Skills.init('skills-entry-1'),
38
- coverLetter: CoverLetter.init(),
39
- attachments: Attachments.init(),
40
- isPreviewVisible: false,
41
- submission: Submission.NotSubmitted(),
42
- stepMenu: Menu.init({ id: 'step-menu' }),
43
- stepTabs: Tabs.init({ id: 'step-tabs' }),
44
- isSubmitAttempted: false,
45
- }
46
-
47
- const completeModel: Model = {
48
- ...initialModel,
49
- personalInfo: {
50
- ...initialModel.personalInfo,
51
- firstName: Valid({ value: 'Jane' }),
52
- lastName: Valid({ value: 'Doe' }),
53
- email: Valid({ value: 'jane@example.com' }),
54
- },
55
- workHistory: {
56
- ...initialModel.workHistory,
57
- entries: initialModel.workHistory.entries.map(entry => ({
58
- ...entry,
59
- company: Valid({ value: 'Foldkit' }),
60
- title: Valid({ value: 'Engineer' }),
61
- })),
62
- },
63
- education: {
64
- ...initialModel.education,
65
- entries: initialModel.education.entries.map(entry => ({
66
- ...entry,
67
- school: Valid({ value: 'MIT' }),
68
- degree: Valid({ value: 'BS' }),
69
- fieldOfStudy: Valid({ value: 'CS' }),
70
- })),
71
- },
72
- skills: {
73
- ...initialModel.skills,
74
- entries: initialModel.skills.entries.map(entry => ({
75
- ...entry,
76
- name: Valid({ value: 'TypeScript' }),
77
- })),
78
- },
79
- }
19
+ import { view } from './view/view'
80
20
 
81
21
  const resolveFocusTab = Command.resolve(
82
22
  Tabs.FocusTab,
83
- TabsMessage.CompletedFocusTab(),
23
+ Tabs.Message.CompletedFocusTab(),
84
24
  )
85
25
 
86
26
  describe('view', () => {
@@ -0,0 +1,2 @@
1
+ export { Model, Message, init, update } from './attachments'
2
+ export { view } from './view'
@@ -0,0 +1,19 @@
1
+ import { expect, given, role, scene, text } from 'foldkit/scene'
2
+ import { describe, test } from 'vitest'
3
+
4
+ import { init, update } from './attachments'
5
+ import { view } from './view'
6
+
7
+ describe('attachments', () => {
8
+ test('renders the resume and additional-files drop zones', () => {
9
+ scene(
10
+ { update, view },
11
+ given(init()),
12
+ expect(role('heading', { name: 'Resume (PDF)' })).toExist(),
13
+ expect(text('Drop your resume or click to upload')).toExist(),
14
+ expect(
15
+ role('heading', { name: 'Additional Files (optional)' }),
16
+ ).toExist(),
17
+ )
18
+ })
19
+ })
@@ -0,0 +1,46 @@
1
+ import { given, message, model, story } from 'foldkit/story'
2
+ import { describe, expect, test } from 'vitest'
3
+
4
+ import { FileDrop } from '@foldkit/ui'
5
+
6
+ import { Message, init, update } from './attachments'
7
+
8
+ describe('attachments', () => {
9
+ test('a dropped resume is stored as maybeResume', () => {
10
+ const resume = new globalThis.File(['pdf-bytes'], 'resume.pdf', {
11
+ type: 'application/pdf',
12
+ })
13
+
14
+ story(
15
+ update,
16
+ given(init()),
17
+ message(
18
+ Message.GotResumeDropMessage({
19
+ message: FileDrop.Message.DroppedFiles({ files: [resume] }),
20
+ }),
21
+ ),
22
+ model(model => {
23
+ expect(model.maybeResume._tag).toBe('Some')
24
+ }),
25
+ )
26
+ })
27
+
28
+ test('dropped additional files are appended to the list', () => {
29
+ const file = new globalThis.File(['content'], 'portfolio.pdf', {
30
+ type: 'application/pdf',
31
+ })
32
+
33
+ story(
34
+ update,
35
+ given(init()),
36
+ message(
37
+ Message.GotAdditionalFilesDropMessage({
38
+ message: FileDrop.Message.DroppedFiles({ files: [file] }),
39
+ }),
40
+ ),
41
+ model(model => {
42
+ expect(model.additionalFiles).toHaveLength(1)
43
+ }),
44
+ )
45
+ })
46
+ })
@@ -5,8 +5,7 @@ import type { Html, HtmlBuilder } from 'foldkit/html'
5
5
 
6
6
  import { Button, FileDrop } from '@foldkit/ui'
7
7
 
8
- import { Attachments } from '../step'
9
- import { Message } from '../step/attachments'
8
+ import { Message, type Model } from './attachments'
10
9
 
11
10
  const BYTES_PER_KB = 1024
12
11
  const BYTES_PER_MB = BYTES_PER_KB * BYTES_PER_KB
@@ -27,10 +26,7 @@ const dropZoneClassName =
27
26
  const fileKey = (file: File.File): string =>
28
27
  `${File.name(file)}:${File.size(file)}:${file.lastModified}`
29
28
 
30
- const resumeView = (
31
- resume: File.File,
32
- h: HtmlBuilder<Attachments.Message>,
33
- ): Html =>
29
+ const resumeView = (resume: File.File, h: HtmlBuilder<Message>): Html =>
34
30
  h.div(
35
31
  [
36
32
  h.Class(
@@ -79,7 +75,7 @@ const resumeView = (
79
75
  const additionalFileView = (
80
76
  file: File.File,
81
77
  fileIndex: number,
82
- h: HtmlBuilder<Attachments.Message>,
78
+ h: HtmlBuilder<Message>,
83
79
  ): Html =>
84
80
  h.keyed('div')(
85
81
  fileKey(file),
@@ -119,10 +115,7 @@ const additionalFileView = (
119
115
  ],
120
116
  )
121
117
 
122
- export const attachmentsView = Submodel.defineView<
123
- Attachments.Model,
124
- Attachments.Message
125
- >((model, h): Html => {
118
+ export const view = Submodel.defineView<Model, Message>((model, h): Html => {
126
119
  const { resumeDrop, maybeResume, additionalFilesDrop, additionalFiles } =
127
120
  model
128
121
 
@@ -1,19 +1,48 @@
1
1
  import clsx from 'clsx'
2
- import { Submodel } from 'foldkit'
2
+ import { Schema } from 'effect'
3
+ import { Submodel, type Update } from 'foldkit'
3
4
  import type { Html } from 'foldkit/html'
5
+ import { defineMessageUnion } from 'foldkit/message'
6
+ import { evo } from 'foldkit/struct'
4
7
 
5
8
  import { Textarea } from '@foldkit/ui'
6
9
 
7
- import { CoverLetter } from '../step'
8
- import { Message } from '../step/coverLetter'
10
+ // MODEL
11
+
12
+ export const Model = Schema.Struct({
13
+ content: Schema.String,
14
+ })
15
+ export type Model = typeof Model.Type
16
+
17
+ // MESSAGE
18
+
19
+ export const Message = defineMessageUnion({
20
+ UpdatedContent: { value: Schema.String },
21
+ })
22
+
23
+ export type Message = typeof Message.Type
24
+
25
+ // INIT
26
+
27
+ export const init = (): Model => ({
28
+ content: '',
29
+ })
30
+
31
+ // UPDATE
32
+
33
+ export const update = (model: Model, message: Message) =>
34
+ Message.match<Update.Return<Model, Message>>(message, {
35
+ UpdatedContent: ({ value }) => ({
36
+ model: evo(model, { content: () => value }),
37
+ }),
38
+ })
39
+
40
+ // VIEW
9
41
 
10
42
  const MAX_COVER_LETTER_LENGTH = 2000
11
43
  const WARNING_THRESHOLD_CHARS = 200
12
44
 
13
- export const coverLetterView = Submodel.defineView<
14
- CoverLetter.Model,
15
- CoverLetter.Message
16
- >((model, h): Html => {
45
+ export const view = Submodel.defineView<Model, Message>((model, h): Html => {
17
46
  const remaining = MAX_COVER_LETTER_LENGTH - model.content.length
18
47
  const isOverLimit = remaining < 0
19
48
  const isWarning = !isOverLimit && remaining <= WARNING_THRESHOLD_CHARS
@@ -0,0 +1 @@
1
+ export { Model, Message, init, update, view } from './coverLetter'
@@ -0,0 +1,24 @@
1
+ import { expect, given, label, scene, text, type } from 'foldkit/scene'
2
+ import { describe, test } from 'vitest'
3
+
4
+ import { init, update, view } from './coverLetter'
5
+
6
+ describe('coverLetter', () => {
7
+ test('renders the cover letter field', () => {
8
+ scene(
9
+ { update, view },
10
+ given(init()),
11
+ expect(label('Cover Letter')).toExist(),
12
+ expect(text('2000 characters remaining')).toExist(),
13
+ )
14
+ })
15
+
16
+ test('typing updates the remaining character count', () => {
17
+ scene(
18
+ { update, view },
19
+ given(init()),
20
+ type(label('Cover Letter'), 'Hello'),
21
+ expect(text('1995 characters remaining')).toExist(),
22
+ )
23
+ })
24
+ })