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
@@ -4,27 +4,13 @@ import { type CalendarDate } from 'foldkit/calendar'
4
4
  import { Valid } from 'foldkit/fieldValidation'
5
5
  import { type Html, type HtmlBuilder, childAttributes } from 'foldkit/html'
6
6
 
7
- import { DatePicker, Listbox } from '@foldkit/ui'
7
+ import { DatePicker as UiDatePicker } from '@foldkit/ui'
8
8
 
9
- import { PronounOption } from '../domain'
10
- import { PersonalInfo } from '../step'
11
- import { Message } from '../step/personalInfo'
12
- import {
13
- backdropClassName,
14
- calendarView,
15
- panelClassName,
16
- triggerClassName,
17
- triggerContent,
18
- } from './datePicker'
19
- import { inputField } from './field'
20
- import { chevronDown } from './icon'
9
+ import { PronounOption } from '../../domain'
10
+ import { DatePicker, Field, Icon } from '../../view'
11
+ import { Message, type Model, PronounsListbox } from './personalInfo'
21
12
 
22
- const PronounsListbox = Listbox.create<string>()
23
-
24
- export const personalInfoView = Submodel.defineView<
25
- PersonalInfo.Model,
26
- PersonalInfo.Message
27
- >((model, h): Html => {
13
+ export const view = Submodel.defineView<Model, Message>((model, h): Html => {
28
14
  const {
29
15
  firstName,
30
16
  lastName,
@@ -53,7 +39,7 @@ export const personalInfoView = Submodel.defineView<
53
39
  h.div(
54
40
  [h.Class('grid grid-cols-2 gap-4')],
55
41
  [
56
- inputField(
42
+ Field.input(
57
43
  {
58
44
  id: 'first-name',
59
45
  label: 'First Name',
@@ -63,7 +49,7 @@ export const personalInfoView = Submodel.defineView<
63
49
  },
64
50
  h,
65
51
  ),
66
- inputField(
52
+ Field.input(
67
53
  {
68
54
  id: 'last-name',
69
55
  label: 'Last Name',
@@ -75,7 +61,7 @@ export const personalInfoView = Submodel.defineView<
75
61
  ),
76
62
  ],
77
63
  ),
78
- inputField(
64
+ Field.input(
79
65
  {
80
66
  id: 'email',
81
67
  label: 'Email',
@@ -86,7 +72,7 @@ export const personalInfoView = Submodel.defineView<
86
72
  },
87
73
  h,
88
74
  ),
89
- inputField(
75
+ Field.input(
90
76
  {
91
77
  id: 'phone',
92
78
  label: 'Phone (optional)',
@@ -144,7 +130,10 @@ export const personalInfoView = Submodel.defineView<
144
130
  ],
145
131
  [selectedPronounLabel],
146
132
  ),
147
- h.span([h.Class('text-gray-400')], [chevronDown('w-4 h-4')]),
133
+ h.span(
134
+ [h.Class('text-gray-400')],
135
+ [Icon.chevronDown('w-4 h-4')],
136
+ ),
148
137
  ],
149
138
  ),
150
139
  buttonAttributes: childAttributes([
@@ -165,7 +154,7 @@ export const personalInfoView = Submodel.defineView<
165
154
  ),
166
155
  ...(isOtherSelected
167
156
  ? [
168
- inputField(
157
+ Field.input(
169
158
  {
170
159
  id: 'custom-pronouns',
171
160
  label: 'Custom Pronouns',
@@ -177,7 +166,7 @@ export const personalInfoView = Submodel.defineView<
177
166
  ),
178
167
  ]
179
168
  : []),
180
- inputField(
169
+ Field.input(
181
170
  {
182
171
  id: 'portfolio-url',
183
172
  label: 'Portfolio URL (optional)',
@@ -193,9 +182,9 @@ export const personalInfoView = Submodel.defineView<
193
182
  })
194
183
 
195
184
  const availableDatePickerView = (
196
- model: DatePicker.Model,
185
+ model: UiDatePicker.Model,
197
186
  maybeSelectedDate: Option.Option<CalendarDate>,
198
- h: HtmlBuilder<PersonalInfo.Message>,
187
+ h: HtmlBuilder<Message>,
199
188
  ): Html =>
200
189
  h.div(
201
190
  [h.Class('space-y-1')],
@@ -207,15 +196,16 @@ const availableDatePickerView = (
207
196
  h.submodel({
208
197
  slotId: model.id,
209
198
  model,
210
- view: DatePicker.view,
199
+ view: UiDatePicker.view,
211
200
  viewInputs: {
212
201
  anchor: { placement: 'bottom-start', gap: 4, padding: 8 },
213
202
  maybeSelectedDate,
214
- triggerContent: maybeDate => triggerContent(maybeDate, 'Pick a date'),
215
- triggerClassName,
216
- panelClassName,
217
- backdropClassName,
218
- toCalendarView: calendarView,
203
+ triggerContent: maybeDate =>
204
+ DatePicker.triggerContent(maybeDate, 'Pick a date'),
205
+ triggerClassName: DatePicker.triggerClassName,
206
+ panelClassName: DatePicker.panelClassName,
207
+ backdropClassName: DatePicker.backdropClassName,
208
+ toCalendarView: DatePicker.calendarView,
219
209
  },
220
210
  toParentMessage: message =>
221
211
  Message.GotAvailableDateMessage({ message }),
@@ -13,8 +13,8 @@ import { evo } from 'foldkit/struct'
13
13
 
14
14
  import { RadioGroup } from '@foldkit/ui'
15
15
 
16
- import { ProficiencyLevel } from '../../domain'
17
- import { revealFieldErrors } from '../validation'
16
+ import { ProficiencyLevel } from '../../../domain'
17
+ import { revealFieldErrors } from '../../validation'
18
18
 
19
19
  // FIELD VALIDATION
20
20
 
@@ -0,0 +1,11 @@
1
+ export {
2
+ Model,
3
+ Message,
4
+ OutMessage,
5
+ init,
6
+ update,
7
+ hasErrors,
8
+ isComplete,
9
+ revealErrors,
10
+ } from './entry'
11
+ export { view } from './view'
@@ -0,0 +1,27 @@
1
+ import { expectOutMessage, given, message, model, story } from 'foldkit/story'
2
+ import { describe, expect, test } from 'vitest'
3
+
4
+ import { Message, OutMessage, init, update } from './entry'
5
+
6
+ describe('skills entry', () => {
7
+ test('UpdatedName stores a valid skill name', () => {
8
+ story(
9
+ update,
10
+ given(init('entry-1')),
11
+ message(Message.UpdatedName({ value: 'TypeScript' })),
12
+ model(model => {
13
+ expect(model.name.value).toBe('TypeScript')
14
+ expect(model.name._tag).toBe('Valid')
15
+ }),
16
+ )
17
+ })
18
+
19
+ test('ClickedRemoveSelf emits Removed', () => {
20
+ story(
21
+ update,
22
+ given(init('entry-1')),
23
+ message(Message.ClickedRemoveSelf()),
24
+ expectOutMessage(OutMessage.Removed()),
25
+ )
26
+ })
27
+ })
@@ -5,20 +5,17 @@ import type { Html } from 'foldkit/html'
5
5
 
6
6
  import { Button } from '@foldkit/ui'
7
7
 
8
- import { ProficiencyLevel } from '../domain'
9
- import { Skills } from '../step'
10
- import { inputField } from './field'
8
+ import { ProficiencyLevel } from '../../../domain'
9
+ import { Field } from '../../../view'
10
+ import { Message, type Model, ProficiencyRadioGroup } from './entry'
11
11
 
12
- export const skillEntryView = Submodel.defineView<
13
- Skills.Entry.Model,
14
- Skills.Entry.Message
15
- >((model, h): Html => {
16
- const nameView = inputField(
12
+ export const view = Submodel.defineView<Model, Message>((model, h): Html => {
13
+ const nameView = Field.input(
17
14
  {
18
15
  id: `${model.id}-name`,
19
16
  label: 'Skill',
20
17
  field: model.name,
21
- onInput: value => Skills.Entry.Message.UpdatedName({ value }),
18
+ onInput: value => Message.UpdatedName({ value }),
22
19
  placeholder: 'e.g. TypeScript, React, Effect-TS',
23
20
  },
24
21
  h,
@@ -27,7 +24,7 @@ export const skillEntryView = Submodel.defineView<
27
24
  const proficiencyView = h.submodel({
28
25
  slotId: model.proficiencyRadioGroup.id,
29
26
  model: model.proficiencyRadioGroup,
30
- view: Skills.Entry.ProficiencyRadioGroup.view,
27
+ view: ProficiencyRadioGroup.view,
31
28
  viewInputs: {
32
29
  selectedValue: Option.some(model.proficiency),
33
30
  options: ProficiencyLevel.all,
@@ -58,7 +55,7 @@ export const skillEntryView = Submodel.defineView<
58
55
  ),
59
56
  },
60
57
  toParentMessage: message =>
61
- Skills.Entry.Message.GotProficiencyRadioGroupMessage({ message }),
58
+ Message.GotProficiencyRadioGroupMessage({ message }),
62
59
  })
63
60
 
64
61
  return h.keyed('div')(
@@ -81,7 +78,7 @@ export const skillEntryView = Submodel.defineView<
81
78
  [
82
79
  Button.view(
83
80
  {
84
- onClick: Skills.Entry.Message.ClickedRemoveSelf(),
81
+ onClick: Message.ClickedRemoveSelf(),
85
82
  toView: attributes =>
86
83
  h.button(
87
84
  [
@@ -1,2 +1,12 @@
1
- export * from './skills'
1
+ export {
2
+ Model,
3
+ Message,
4
+ init,
5
+ update,
6
+ GenerateEntryId,
7
+ hasErrors,
8
+ isComplete,
9
+ revealErrors,
10
+ } from './skills'
2
11
  export * as Entry from './entry'
12
+ export { view } from './view'
@@ -0,0 +1,16 @@
1
+ import { expect, given, label, role, scene } from 'foldkit/scene'
2
+ import { describe, test } from 'vitest'
3
+
4
+ import { init, update } from './skills'
5
+ import { view } from './view'
6
+
7
+ describe('skills', () => {
8
+ test('renders the first skill field and the add control', () => {
9
+ scene(
10
+ { update, view },
11
+ given(init('skills-entry-1')),
12
+ expect(label('Skill')).toExist(),
13
+ expect(role('button', { name: '+ Add Skill' })).toExist(),
14
+ )
15
+ })
16
+ })
@@ -0,0 +1,85 @@
1
+ import { Array, Option, pipe } from 'effect'
2
+ import { Command, given, message, model, story } from 'foldkit/story'
3
+ import { describe, expect, test } from 'vitest'
4
+
5
+ import * as Entry from './entry'
6
+ import { GenerateEntryId, Message, init, update } from './skills'
7
+
8
+ const givenInitial = given(init('skills-entry-1'))
9
+
10
+ describe('skills', () => {
11
+ test('ClickedAddEntry generates an id and appends an entry', () => {
12
+ story(
13
+ update,
14
+ givenInitial,
15
+ message(Message.ClickedAddEntry()),
16
+ Command.expectHas(GenerateEntryId),
17
+ Command.resolve(
18
+ GenerateEntryId,
19
+ Message.SucceededGenerateEntryId({ entryId: 'test-skill-1' }),
20
+ ),
21
+ model(model => {
22
+ expect(model.entries).toHaveLength(2)
23
+ }),
24
+ )
25
+ })
26
+
27
+ test('FailedGenerateEntryId leaves the entries unchanged', () => {
28
+ story(
29
+ update,
30
+ givenInitial,
31
+ message(Message.FailedGenerateEntryId()),
32
+ Command.expectNone(),
33
+ model(model => {
34
+ expect(model).toEqual(init('skills-entry-1'))
35
+ }),
36
+ )
37
+ })
38
+
39
+ test('an entry OutMessage.Removed removes that entry', () => {
40
+ const firstEntry = Option.getOrThrow(
41
+ Array.head(init('skills-entry-1').entries),
42
+ )
43
+
44
+ story(
45
+ update,
46
+ givenInitial,
47
+ message(
48
+ Message.GotEntryMessage({
49
+ entryId: firstEntry.id,
50
+ message: Entry.Message.ClickedRemoveSelf(),
51
+ }),
52
+ ),
53
+ model(model => {
54
+ expect(model.entries).toHaveLength(0)
55
+ }),
56
+ )
57
+ })
58
+
59
+ test('GotEntryMessage folds a name update into the matching entry', () => {
60
+ const firstEntry = Option.getOrThrow(
61
+ Array.head(init('skills-entry-1').entries),
62
+ )
63
+
64
+ story(
65
+ update,
66
+ givenInitial,
67
+ message(
68
+ Message.GotEntryMessage({
69
+ entryId: firstEntry.id,
70
+ message: Entry.Message.UpdatedName({ value: 'TypeScript' }),
71
+ }),
72
+ ),
73
+ model(model => {
74
+ expect(
75
+ pipe(
76
+ model.entries,
77
+ Array.head,
78
+ Option.map(entry => entry.name.value),
79
+ Option.getOrThrow,
80
+ ),
81
+ ).toBe('TypeScript')
82
+ }),
83
+ )
84
+ })
85
+ })
@@ -0,0 +1,47 @@
1
+ import { Submodel } from 'foldkit'
2
+ import type { Html } from 'foldkit/html'
3
+
4
+ import { Button } from '@foldkit/ui'
5
+
6
+ import * as Entry from './entry'
7
+ import { Message, type Model } from './skills'
8
+
9
+ export const view = Submodel.defineView<Model, Message>((model, h): Html =>
10
+ h.div(
11
+ [h.Class('space-y-6')],
12
+ [
13
+ h.p(
14
+ [h.Class('text-sm text-gray-500')],
15
+ ['Add your technical and professional skills.'],
16
+ ),
17
+ h.div(
18
+ [h.Class('divide-y divide-gray-200')],
19
+ model.entries.map(entry =>
20
+ h.submodel({
21
+ slotId: entry.id,
22
+ model: entry,
23
+ view: Entry.view,
24
+ toParentMessage: message =>
25
+ Message.GotEntryMessage({ entryId: entry.id, message }),
26
+ }),
27
+ ),
28
+ ),
29
+ Button.view(
30
+ {
31
+ onClick: Message.ClickedAddEntry(),
32
+ toView: attributes =>
33
+ h.button(
34
+ [
35
+ ...attributes.button,
36
+ h.Class(
37
+ 'w-full rounded-lg border-2 border-dashed border-gray-300 px-4 py-3 text-sm font-medium text-gray-600 hover:border-indigo-400 hover:text-indigo-600 transition cursor-pointer',
38
+ ),
39
+ ],
40
+ ['+ Add Skill'],
41
+ ),
42
+ },
43
+ h,
44
+ ),
45
+ ],
46
+ ),
47
+ )
@@ -14,7 +14,7 @@ import { evo } from 'foldkit/struct'
14
14
 
15
15
  import { DatePicker } from '@foldkit/ui'
16
16
 
17
- import { revealFieldErrors } from '../validation'
17
+ import { revealFieldErrors } from '../../validation'
18
18
 
19
19
  // FIELD VALIDATION
20
20
 
@@ -0,0 +1,11 @@
1
+ export {
2
+ Model,
3
+ Message,
4
+ OutMessage,
5
+ init,
6
+ update,
7
+ hasErrors,
8
+ isComplete,
9
+ revealErrors,
10
+ } from './entry'
11
+ export { view } from './view'
@@ -0,0 +1,42 @@
1
+ import { Calendar } from 'foldkit'
2
+ import { expectOutMessage, given, message, model, story } from 'foldkit/story'
3
+ import { describe, expect, test } from 'vitest'
4
+
5
+ import { Message, OutMessage, init, update } from './entry'
6
+
7
+ const today = Calendar.make(2026, 4, 16)
8
+
9
+ describe('workHistory entry', () => {
10
+ test('UpdatedCompany stores a valid company', () => {
11
+ story(
12
+ update,
13
+ given(init('entry-1', today)),
14
+ message(Message.UpdatedCompany({ value: 'Foldkit Inc.' })),
15
+ model(model => {
16
+ expect(model.company.value).toBe('Foldkit Inc.')
17
+ expect(model.company._tag).toBe('Valid')
18
+ }),
19
+ )
20
+ })
21
+
22
+ test('clearing company after a value makes the field Invalid', () => {
23
+ story(
24
+ update,
25
+ given(init('entry-1', today)),
26
+ message(Message.UpdatedCompany({ value: 'Foldkit Inc.' })),
27
+ message(Message.UpdatedCompany({ value: '' })),
28
+ model(model => {
29
+ expect(model.company._tag).toBe('Invalid')
30
+ }),
31
+ )
32
+ })
33
+
34
+ test('ClickedRemoveSelf emits Removed', () => {
35
+ story(
36
+ update,
37
+ given(init('entry-1', today)),
38
+ message(Message.ClickedRemoveSelf()),
39
+ expectOutMessage(OutMessage.Removed()),
40
+ )
41
+ })
42
+ })
@@ -1,24 +1,14 @@
1
1
  import { Submodel } from 'foldkit'
2
2
  import type { Html } from 'foldkit/html'
3
3
 
4
- import { Button, DatePicker } from '@foldkit/ui'
4
+ import { Button, DatePicker as UiDatePicker } from '@foldkit/ui'
5
5
 
6
- import { WorkHistory } from '../step'
7
- import {
8
- backdropClassName,
9
- calendarView,
10
- panelClassName,
11
- triggerClassName,
12
- triggerContent,
13
- } from './datePicker'
14
- import { checkboxField, inputField, textareaField } from './field'
6
+ import { DatePicker, Field } from '../../../view'
7
+ import { Message, type Model } from './entry'
15
8
 
16
9
  const ANCHOR = { placement: 'bottom-start' as const, gap: 4, padding: 8 }
17
10
 
18
- export const workEntryView = Submodel.defineView<
19
- WorkHistory.Entry.Model,
20
- WorkHistory.Entry.Message
21
- >((model, h): Html => {
11
+ export const view = Submodel.defineView<Model, Message>((model, h): Html => {
22
12
  const showEndDate = !model.isCurrentlyEmployed
23
13
 
24
14
  const startDatePicker = h.div(
@@ -31,19 +21,18 @@ export const workEntryView = Submodel.defineView<
31
21
  h.submodel({
32
22
  slotId: model.startDate.id,
33
23
  model: model.startDate,
34
- view: DatePicker.view,
24
+ view: UiDatePicker.view,
35
25
  viewInputs: {
36
26
  anchor: ANCHOR,
37
27
  maybeSelectedDate: model.maybeStartDate,
38
28
  triggerContent: maybeDate =>
39
- triggerContent(maybeDate, 'Select start date'),
40
- toCalendarView: calendarView,
41
- triggerClassName,
42
- panelClassName,
43
- backdropClassName,
29
+ DatePicker.triggerContent(maybeDate, 'Select start date'),
30
+ toCalendarView: DatePicker.calendarView,
31
+ triggerClassName: DatePicker.triggerClassName,
32
+ panelClassName: DatePicker.panelClassName,
33
+ backdropClassName: DatePicker.backdropClassName,
44
34
  },
45
- toParentMessage: message =>
46
- WorkHistory.Entry.Message.GotStartDateMessage({ message }),
35
+ toParentMessage: message => Message.GotStartDateMessage({ message }),
47
36
  }),
48
37
  ],
49
38
  )
@@ -59,19 +48,18 @@ export const workEntryView = Submodel.defineView<
59
48
  h.submodel({
60
49
  slotId: model.endDate.id,
61
50
  model: model.endDate,
62
- view: DatePicker.view,
51
+ view: UiDatePicker.view,
63
52
  viewInputs: {
64
53
  anchor: ANCHOR,
65
54
  maybeSelectedDate: model.maybeEndDate,
66
55
  triggerContent: maybeDate =>
67
- triggerContent(maybeDate, 'Select end date'),
68
- toCalendarView: calendarView,
69
- triggerClassName,
70
- panelClassName,
71
- backdropClassName,
56
+ DatePicker.triggerContent(maybeDate, 'Select end date'),
57
+ toCalendarView: DatePicker.calendarView,
58
+ triggerClassName: DatePicker.triggerClassName,
59
+ panelClassName: DatePicker.panelClassName,
60
+ backdropClassName: DatePicker.backdropClassName,
72
61
  },
73
- toParentMessage: message =>
74
- WorkHistory.Entry.Message.GotEndDateMessage({ message }),
62
+ toParentMessage: message => Message.GotEndDateMessage({ message }),
75
63
  }),
76
64
  ],
77
65
  )
@@ -83,24 +71,22 @@ export const workEntryView = Submodel.defineView<
83
71
  h.div(
84
72
  [h.Class('grid grid-cols-2 gap-3')],
85
73
  [
86
- inputField(
74
+ Field.input(
87
75
  {
88
76
  id: `${model.id}-company`,
89
77
  label: 'Company',
90
78
  field: model.company,
91
- onInput: value =>
92
- WorkHistory.Entry.Message.UpdatedCompany({ value }),
79
+ onInput: value => Message.UpdatedCompany({ value }),
93
80
  placeholder: 'e.g. Acme Corp',
94
81
  },
95
82
  h,
96
83
  ),
97
- inputField(
84
+ Field.input(
98
85
  {
99
86
  id: `${model.id}-title`,
100
87
  label: 'Job Title',
101
88
  field: model.title,
102
- onInput: value =>
103
- WorkHistory.Entry.Message.UpdatedTitle({ value }),
89
+ onInput: value => Message.UpdatedTitle({ value }),
104
90
  placeholder: 'e.g. Senior Engineer',
105
91
  },
106
92
  h,
@@ -111,23 +97,22 @@ export const workEntryView = Submodel.defineView<
111
97
  [h.Class('grid grid-cols-2 gap-3')],
112
98
  [startDatePicker, ...(showEndDate ? [endDatePicker] : [])],
113
99
  ),
114
- checkboxField(
100
+ Field.checkbox(
115
101
  {
116
102
  id: `${model.id}-current`,
117
103
  label: 'I currently work here',
118
104
  isChecked: model.isCurrentlyEmployed,
119
105
  onToggle: isChecked =>
120
- WorkHistory.Entry.Message.ToggledCurrentlyEmployed({ isChecked }),
106
+ Message.ToggledCurrentlyEmployed({ isChecked }),
121
107
  },
122
108
  h,
123
109
  ),
124
- textareaField(
110
+ Field.textarea(
125
111
  {
126
112
  id: `${model.id}-description`,
127
113
  label: 'Description',
128
114
  value: model.description,
129
- onInput: value =>
130
- WorkHistory.Entry.Message.UpdatedDescription({ value }),
115
+ onInput: value => Message.UpdatedDescription({ value }),
131
116
  rows: 3,
132
117
  placeholder: 'Describe your role and key accomplishments...',
133
118
  },
@@ -138,7 +123,7 @@ export const workEntryView = Submodel.defineView<
138
123
  [
139
124
  Button.view(
140
125
  {
141
- onClick: WorkHistory.Entry.Message.ClickedRemoveSelf(),
126
+ onClick: Message.ClickedRemoveSelf(),
142
127
  toView: attributes =>
143
128
  h.button(
144
129
  [
@@ -1,2 +1,12 @@
1
- export * from './workHistory'
1
+ export {
2
+ Model,
3
+ Message,
4
+ init,
5
+ update,
6
+ GenerateEntryId,
7
+ hasErrors,
8
+ isComplete,
9
+ revealErrors,
10
+ } from './workHistory'
2
11
  export * as Entry from './entry'
12
+ export { view } from './view'
@@ -0,0 +1,20 @@
1
+ import { Calendar } from 'foldkit'
2
+ import { expect, given, label, role, scene } from 'foldkit/scene'
3
+ import { describe, test } from 'vitest'
4
+
5
+ import { view } from './view'
6
+ import { init, update } from './workHistory'
7
+
8
+ const today = Calendar.make(2026, 4, 16)
9
+
10
+ describe('workHistory', () => {
11
+ test('renders the first position fields and the add control', () => {
12
+ scene(
13
+ { update, view },
14
+ given(init(today, 'work-history-entry-1')),
15
+ expect(label('Company')).toExist(),
16
+ expect(label('Job Title')).toExist(),
17
+ expect(role('button', { name: '+ Add Position' })).toExist(),
18
+ )
19
+ })
20
+ })