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.
- package/dist/templates/base/.oxfmtrc.json +28 -0
- package/dist/templates/base/.oxlintrc.json +4 -0
- package/dist/templates/base/FOLDKIT.md +1 -1
- package/dist/templates/base/package.json +1 -1
- package/dist/templates/examples/api-cache/package.json +0 -2
- package/dist/templates/examples/api-cache/src/scene.test.ts +1 -2
- package/dist/templates/examples/api-cache/src/story.test.ts +3 -4
- package/dist/templates/examples/auth/package.json +0 -2
- package/dist/templates/examples/canvas-art/package.json +0 -2
- package/dist/templates/examples/charting/package.json +0 -2
- package/dist/templates/examples/charting/src/scene.test.ts +1 -2
- package/dist/templates/examples/charting/src/story.test.ts +3 -4
- package/dist/templates/examples/counter/package.json +0 -2
- package/dist/templates/examples/counters/package.json +0 -2
- package/dist/templates/examples/crash-view/package.json +0 -2
- package/dist/templates/examples/embedding/package.json +0 -2
- package/dist/templates/examples/form/package.json +0 -2
- package/dist/templates/examples/generative-art/package.json +0 -2
- package/dist/templates/examples/interrupting-commands/package.json +0 -2
- package/dist/templates/examples/job-application/package.json +0 -2
- package/dist/templates/examples/job-application/src/main.fixture.ts +69 -0
- package/dist/templates/examples/job-application/src/main.ts +1 -1
- package/dist/templates/examples/job-application/src/scene.test.ts +6 -66
- package/dist/templates/examples/job-application/src/step/attachments/index.ts +2 -0
- package/dist/templates/examples/job-application/src/step/attachments/scene.test.ts +19 -0
- package/dist/templates/examples/job-application/src/step/attachments/story.test.ts +46 -0
- package/dist/templates/examples/job-application/src/{view/attachments.ts → step/attachments/view.ts} +4 -11
- package/dist/templates/examples/job-application/src/{view → step/coverLetter}/coverLetter.ts +36 -7
- package/dist/templates/examples/job-application/src/step/coverLetter/index.ts +1 -0
- package/dist/templates/examples/job-application/src/step/coverLetter/scene.test.ts +24 -0
- package/dist/templates/examples/job-application/src/step/coverLetter/story.test.ts +19 -0
- package/dist/templates/examples/job-application/src/step/education/{entry.ts → entry/entry.ts} +2 -2
- package/dist/templates/examples/job-application/src/step/education/entry/index.ts +11 -0
- package/dist/templates/examples/job-application/src/step/education/entry/story.test.ts +39 -0
- package/dist/templates/examples/job-application/src/step/education/entry/view.ts +160 -0
- package/dist/templates/examples/job-application/src/step/education/index.ts +11 -1
- package/dist/templates/examples/job-application/src/step/education/scene.test.ts +20 -0
- package/dist/templates/examples/job-application/src/step/education/story.test.ts +87 -0
- package/dist/templates/examples/job-application/src/{view/education.ts → step/education/view.ts} +6 -10
- package/dist/templates/examples/job-application/src/step/personalInfo/index.ts +11 -0
- package/dist/templates/examples/job-application/src/step/{personalInfo.ts → personalInfo/personalInfo.ts} +2 -2
- package/dist/templates/examples/job-application/src/step/personalInfo/scene.test.ts +38 -0
- package/dist/templates/examples/job-application/src/step/personalInfo/story.test.ts +87 -0
- package/dist/templates/examples/job-application/src/{view/personalInfo.ts → step/personalInfo/view.ts} +24 -34
- package/dist/templates/examples/job-application/src/step/skills/{entry.ts → entry/entry.ts} +2 -2
- package/dist/templates/examples/job-application/src/step/skills/entry/index.ts +11 -0
- package/dist/templates/examples/job-application/src/step/skills/entry/story.test.ts +27 -0
- package/dist/templates/examples/job-application/src/{view/skillEntry.ts → step/skills/entry/view.ts} +9 -12
- package/dist/templates/examples/job-application/src/step/skills/index.ts +11 -1
- package/dist/templates/examples/job-application/src/step/skills/scene.test.ts +16 -0
- package/dist/templates/examples/job-application/src/step/skills/story.test.ts +85 -0
- package/dist/templates/examples/job-application/src/step/skills/view.ts +47 -0
- package/dist/templates/examples/job-application/src/step/workHistory/{entry.ts → entry/entry.ts} +1 -1
- package/dist/templates/examples/job-application/src/step/workHistory/entry/index.ts +11 -0
- package/dist/templates/examples/job-application/src/step/workHistory/entry/story.test.ts +42 -0
- package/dist/templates/examples/job-application/src/{view/workEntry.ts → step/workHistory/entry/view.ts} +27 -42
- package/dist/templates/examples/job-application/src/step/workHistory/index.ts +11 -1
- package/dist/templates/examples/job-application/src/step/workHistory/scene.test.ts +20 -0
- package/dist/templates/examples/job-application/src/step/workHistory/story.test.ts +87 -0
- package/dist/templates/examples/job-application/src/{view/workHistory.ts → step/workHistory/view.ts} +4 -8
- package/dist/templates/examples/job-application/src/story.test.ts +28 -419
- package/dist/templates/examples/job-application/src/view/datePicker.ts +4 -4
- package/dist/templates/examples/job-application/src/view/field.ts +3 -3
- package/dist/templates/examples/job-application/src/view/index.ts +3 -358
- package/dist/templates/examples/job-application/src/view/stepNav.ts +2 -2
- package/dist/templates/examples/job-application/src/view/view.ts +359 -0
- package/dist/templates/examples/kanban/package.json +0 -2
- package/dist/templates/examples/kanban/src/scene.test.ts +1 -1
- package/dist/templates/examples/kanban/src/story.test.ts +22 -23
- package/dist/templates/examples/kanban/src/view/index.ts +1 -71
- package/dist/templates/examples/kanban/src/view/view.ts +71 -0
- package/dist/templates/examples/managed-resource-layer/package.json +0 -2
- package/dist/templates/examples/map/package.json +0 -2
- package/dist/templates/examples/pixel-art/package.json +0 -2
- package/dist/templates/examples/pixel-art/src/scene.test.ts +16 -9
- package/dist/templates/examples/pixel-art/src/story.test.ts +12 -7
- package/dist/templates/examples/query-sync/package.json +0 -2
- package/dist/templates/examples/query-sync/src/story.test.ts +4 -5
- package/dist/templates/examples/route-transitions/package.json +0 -2
- package/dist/templates/examples/routing/package.json +0 -2
- package/dist/templates/examples/routing/src/page/people.scene.test.ts +41 -0
- package/dist/templates/examples/routing/src/page/people.story.test.ts +73 -0
- package/dist/templates/examples/routing/src/scene.test.ts +2 -17
- package/dist/templates/examples/routing/src/story.test.ts +4 -34
- package/dist/templates/examples/shopping-cart/package.json +0 -2
- package/dist/templates/examples/shopping-cart/src/page/products.scene.test.ts +20 -0
- package/dist/templates/examples/shopping-cart/src/page/products.story.test.ts +56 -0
- package/dist/templates/examples/shopping-cart/src/page/products.ts +1 -1
- package/dist/templates/examples/shopping-cart/src/scene.test.ts +1 -4
- package/dist/templates/examples/shopping-cart/src/story.test.ts +3 -4
- package/dist/templates/examples/slow-warnings/package.json +0 -2
- package/dist/templates/examples/snake/package.json +0 -2
- package/dist/templates/examples/ssg/package.json +0 -2
- package/dist/templates/examples/ssr/package.json +0 -2
- package/dist/templates/examples/state-machine/package.json +0 -2
- package/dist/templates/examples/state-machine/src/main.ts +79 -87
- package/dist/templates/examples/state-machine/src/scene.test.ts +1 -2
- package/dist/templates/examples/stopwatch/package.json +0 -2
- package/dist/templates/examples/todo/package.json +0 -2
- package/dist/templates/examples/ui-showcase/package.json +0 -2
- package/dist/templates/examples/ui-showcase/src/story.test.ts +1 -2
- package/dist/templates/examples/view-transitions/package.json +0 -2
- package/dist/templates/examples/view-transitions/src/styles.css +3 -3
- package/dist/templates/examples/weather/package.json +0 -2
- package/dist/templates/examples/web-components/package.json +0 -2
- package/dist/templates/examples/websocket-chat/package.json +0 -2
- package/dist/templates/release.json +9 -10
- package/dist/templates/rendering/ssg/package.json +1 -1
- package/dist/templates/rendering/ssr/package.json +1 -1
- package/dist/utils/packages.js +1 -2
- package/package.json +3 -3
- package/dist/templates/base/.prettierignore +0 -5
- package/dist/templates/base/.prettierrc +0 -13
- package/dist/templates/examples/job-application/src/step/coverLetter.ts +0 -34
- package/dist/templates/examples/job-application/src/view/educationEntry.ts +0 -165
- package/dist/templates/examples/job-application/src/view/skills.ts +0 -49
- /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
|
|
7
|
+
import { DatePicker as UiDatePicker } from '@foldkit/ui'
|
|
8
8
|
|
|
9
|
-
import { PronounOption } from '
|
|
10
|
-
import {
|
|
11
|
-
import { Message } from '
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
185
|
+
model: UiDatePicker.Model,
|
|
197
186
|
maybeSelectedDate: Option.Option<CalendarDate>,
|
|
198
|
-
h: HtmlBuilder<
|
|
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:
|
|
199
|
+
view: UiDatePicker.view,
|
|
211
200
|
viewInputs: {
|
|
212
201
|
anchor: { placement: 'bottom-start', gap: 4, padding: 8 },
|
|
213
202
|
maybeSelectedDate,
|
|
214
|
-
triggerContent: maybeDate =>
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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 '
|
|
17
|
-
import { revealFieldErrors } from '
|
|
16
|
+
import { ProficiencyLevel } from '../../../domain'
|
|
17
|
+
import { revealFieldErrors } from '../../validation'
|
|
18
18
|
|
|
19
19
|
// FIELD VALIDATION
|
|
20
20
|
|
|
@@ -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
|
+
})
|
package/dist/templates/examples/job-application/src/{view/skillEntry.ts → step/skills/entry/view.ts}
RENAMED
|
@@ -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 '
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { ProficiencyLevel } from '../../../domain'
|
|
9
|
+
import { Field } from '../../../view'
|
|
10
|
+
import { Message, type Model, ProficiencyRadioGroup } from './entry'
|
|
11
11
|
|
|
12
|
-
export const
|
|
13
|
-
|
|
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 =>
|
|
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:
|
|
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
|
-
|
|
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:
|
|
81
|
+
onClick: Message.ClickedRemoveSelf(),
|
|
85
82
|
toView: attributes =>
|
|
86
83
|
h.button(
|
|
87
84
|
[
|
|
@@ -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
|
+
)
|
|
@@ -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 {
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
+
Message.ToggledCurrentlyEmployed({ isChecked }),
|
|
121
107
|
},
|
|
122
108
|
h,
|
|
123
109
|
),
|
|
124
|
-
|
|
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:
|
|
126
|
+
onClick: Message.ClickedRemoveSelf(),
|
|
142
127
|
toView: attributes =>
|
|
143
128
|
h.button(
|
|
144
129
|
[
|
|
@@ -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
|
+
})
|