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
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import clsx from 'clsx'
|
|
2
|
+
import { Array, Equal, HashSet, Match, Option, pipe } from 'effect'
|
|
3
|
+
import type { ChildAttribute, Document, Html, HtmlBuilder } from 'foldkit/html'
|
|
4
|
+
|
|
5
|
+
import { Button, Tabs } from '@foldkit/ui'
|
|
6
|
+
|
|
7
|
+
import { Step } from '../domain'
|
|
8
|
+
import { Message } from '../message'
|
|
9
|
+
import { type Model } from '../model'
|
|
10
|
+
import {
|
|
11
|
+
Attachments,
|
|
12
|
+
CoverLetter,
|
|
13
|
+
Education,
|
|
14
|
+
PersonalInfo,
|
|
15
|
+
Skills,
|
|
16
|
+
WorkHistory,
|
|
17
|
+
} from '../step'
|
|
18
|
+
import { preview } from './preview'
|
|
19
|
+
import { review } from './review'
|
|
20
|
+
import { stepMenu, stepTabButton } from './stepNav'
|
|
21
|
+
|
|
22
|
+
const StepTabs = Tabs.create<Step.Step>()
|
|
23
|
+
|
|
24
|
+
const stepHasErrors =
|
|
25
|
+
(model: Model) =>
|
|
26
|
+
(step: Step.Step): boolean =>
|
|
27
|
+
Match.value(step).pipe(
|
|
28
|
+
Match.when('PersonalInfo', () =>
|
|
29
|
+
PersonalInfo.hasErrors(model.personalInfo),
|
|
30
|
+
),
|
|
31
|
+
Match.when('WorkHistory', () => WorkHistory.hasErrors(model.workHistory)),
|
|
32
|
+
Match.when('Education', () => Education.hasErrors(model.education)),
|
|
33
|
+
Match.when('Skills', () => Skills.hasErrors(model.skills)),
|
|
34
|
+
Match.orElse(() => false),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const stepIsComplete =
|
|
38
|
+
(model: Model) =>
|
|
39
|
+
(step: Step.Step): boolean =>
|
|
40
|
+
Match.value(step).pipe(
|
|
41
|
+
Match.when('PersonalInfo', () =>
|
|
42
|
+
PersonalInfo.isComplete(model.personalInfo),
|
|
43
|
+
),
|
|
44
|
+
Match.when('WorkHistory', () =>
|
|
45
|
+
WorkHistory.isComplete(model.workHistory),
|
|
46
|
+
),
|
|
47
|
+
Match.when('Education', () => Education.isComplete(model.education)),
|
|
48
|
+
Match.when('Skills', () => Skills.isComplete(model.skills)),
|
|
49
|
+
Match.orElse(() => true),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const stepNeedsAttention =
|
|
53
|
+
(model: Model) =>
|
|
54
|
+
(step: Step.Step): boolean =>
|
|
55
|
+
stepHasErrors(model)(step) ||
|
|
56
|
+
(model.isSubmitAttempted && !stepIsComplete(model)(step))
|
|
57
|
+
|
|
58
|
+
const stepsNeedingAttention = (model: Model): ReadonlyArray<Step.Step> =>
|
|
59
|
+
Array.filter(Step.all, stepNeedsAttention(model))
|
|
60
|
+
|
|
61
|
+
const stepContent = (
|
|
62
|
+
model: Model,
|
|
63
|
+
attentionSteps: ReadonlyArray<Step.Step>,
|
|
64
|
+
h: HtmlBuilder<Message>,
|
|
65
|
+
): Html =>
|
|
66
|
+
Match.value(model.currentStep).pipe(
|
|
67
|
+
Match.when('PersonalInfo', () =>
|
|
68
|
+
h.submodel({
|
|
69
|
+
slotId: 'personal-info',
|
|
70
|
+
model: model.personalInfo,
|
|
71
|
+
view: PersonalInfo.view,
|
|
72
|
+
toParentMessage: message => Message.GotPersonalInfoMessage({ message }),
|
|
73
|
+
}),
|
|
74
|
+
),
|
|
75
|
+
Match.when('WorkHistory', () =>
|
|
76
|
+
h.submodel({
|
|
77
|
+
slotId: 'work-history',
|
|
78
|
+
model: model.workHistory,
|
|
79
|
+
view: WorkHistory.view,
|
|
80
|
+
toParentMessage: message => Message.GotWorkHistoryMessage({ message }),
|
|
81
|
+
}),
|
|
82
|
+
),
|
|
83
|
+
Match.when('Education', () =>
|
|
84
|
+
h.submodel({
|
|
85
|
+
slotId: 'education',
|
|
86
|
+
model: model.education,
|
|
87
|
+
view: Education.view,
|
|
88
|
+
toParentMessage: message => Message.GotEducationMessage({ message }),
|
|
89
|
+
}),
|
|
90
|
+
),
|
|
91
|
+
Match.when('Skills', () =>
|
|
92
|
+
h.submodel({
|
|
93
|
+
slotId: 'skills',
|
|
94
|
+
model: model.skills,
|
|
95
|
+
view: Skills.view,
|
|
96
|
+
toParentMessage: message => Message.GotSkillsMessage({ message }),
|
|
97
|
+
}),
|
|
98
|
+
),
|
|
99
|
+
Match.when('CoverLetter', () =>
|
|
100
|
+
h.submodel({
|
|
101
|
+
slotId: 'cover-letter',
|
|
102
|
+
model: model.coverLetter,
|
|
103
|
+
view: CoverLetter.view,
|
|
104
|
+
toParentMessage: message => Message.GotCoverLetterMessage({ message }),
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
107
|
+
Match.when('Attachments', () =>
|
|
108
|
+
h.submodel({
|
|
109
|
+
slotId: 'attachments',
|
|
110
|
+
model: model.attachments,
|
|
111
|
+
view: Attachments.view,
|
|
112
|
+
toParentMessage: message => Message.GotAttachmentsMessage({ message }),
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
115
|
+
Match.when('Review', () => review(model, attentionSteps, h)),
|
|
116
|
+
Match.exhaustive,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const isFirstStep = (model: Model): boolean =>
|
|
120
|
+
pipe(Step.all, Array.head, Option.exists(Equal.equals(model.currentStep)))
|
|
121
|
+
|
|
122
|
+
const isLastStep = (model: Model): boolean =>
|
|
123
|
+
pipe(Step.all, Array.last, Option.exists(Equal.equals(model.currentStep)))
|
|
124
|
+
|
|
125
|
+
const navigationButtons = (model: Model, h: HtmlBuilder<Message>): Html =>
|
|
126
|
+
h.div(
|
|
127
|
+
[h.Class('flex justify-between pt-6 mt-8 border-t border-gray-200')],
|
|
128
|
+
[
|
|
129
|
+
...(isFirstStep(model)
|
|
130
|
+
? [h.empty]
|
|
131
|
+
: [
|
|
132
|
+
Button.view(
|
|
133
|
+
{
|
|
134
|
+
onClick: Message.ClickedPrevious(),
|
|
135
|
+
toView: attributes =>
|
|
136
|
+
h.button(
|
|
137
|
+
[
|
|
138
|
+
...attributes.button,
|
|
139
|
+
h.Class(
|
|
140
|
+
'rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition cursor-pointer',
|
|
141
|
+
),
|
|
142
|
+
],
|
|
143
|
+
['← Previous'],
|
|
144
|
+
),
|
|
145
|
+
},
|
|
146
|
+
h,
|
|
147
|
+
),
|
|
148
|
+
]),
|
|
149
|
+
...(isLastStep(model)
|
|
150
|
+
? []
|
|
151
|
+
: [
|
|
152
|
+
Button.view(
|
|
153
|
+
{
|
|
154
|
+
onClick: Message.ClickedNext(),
|
|
155
|
+
toView: attributes =>
|
|
156
|
+
h.button(
|
|
157
|
+
[
|
|
158
|
+
...attributes.button,
|
|
159
|
+
h.Class(
|
|
160
|
+
'rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 transition cursor-pointer',
|
|
161
|
+
),
|
|
162
|
+
],
|
|
163
|
+
['Next →'],
|
|
164
|
+
),
|
|
165
|
+
},
|
|
166
|
+
h,
|
|
167
|
+
),
|
|
168
|
+
]),
|
|
169
|
+
],
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
const pageHeader = (h: HtmlBuilder<Message>): Html =>
|
|
173
|
+
h.div(
|
|
174
|
+
[h.Class('mb-6')],
|
|
175
|
+
[
|
|
176
|
+
h.h1(
|
|
177
|
+
[h.Class('text-2xl font-bold text-gray-900')],
|
|
178
|
+
['Apply to Work on Foldkit'],
|
|
179
|
+
),
|
|
180
|
+
h.p(
|
|
181
|
+
[h.Class('text-sm text-gray-500 mt-1')],
|
|
182
|
+
['Fill out the form below and watch your resume build in real time.'],
|
|
183
|
+
),
|
|
184
|
+
],
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
const stepContentPanel = (
|
|
188
|
+
model: Model,
|
|
189
|
+
attentionSteps: ReadonlyArray<Step.Step>,
|
|
190
|
+
panelAttributes: ReadonlyArray<ChildAttribute>,
|
|
191
|
+
h: HtmlBuilder<Message>,
|
|
192
|
+
): Html =>
|
|
193
|
+
h.div(
|
|
194
|
+
[...panelAttributes, h.Class('flex-1 min-w-0')],
|
|
195
|
+
[
|
|
196
|
+
h.h2(
|
|
197
|
+
[h.Class('text-lg font-semibold text-gray-900 mb-6')],
|
|
198
|
+
[Step.show(model.currentStep)],
|
|
199
|
+
),
|
|
200
|
+
h.div(
|
|
201
|
+
[h.Class('min-h-[400px]')],
|
|
202
|
+
[stepContent(model, attentionSteps, h)],
|
|
203
|
+
),
|
|
204
|
+
...(model.currentStep !== 'Review' ? [navigationButtons(model, h)] : []),
|
|
205
|
+
],
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
const stepTabsLayout = (
|
|
209
|
+
model: Model,
|
|
210
|
+
attentionSteps: ReadonlyArray<Step.Step>,
|
|
211
|
+
attentionStepSet: HashSet.HashSet<Step.Step>,
|
|
212
|
+
h: HtmlBuilder<Message>,
|
|
213
|
+
): Html =>
|
|
214
|
+
h.submodel({
|
|
215
|
+
slotId: model.stepTabs.id,
|
|
216
|
+
model: model.stepTabs,
|
|
217
|
+
view: StepTabs.view,
|
|
218
|
+
viewInputs: {
|
|
219
|
+
tabs: Step.all,
|
|
220
|
+
selectedValue: model.currentStep,
|
|
221
|
+
ariaLabel: 'Application steps',
|
|
222
|
+
orientation: 'Vertical',
|
|
223
|
+
toView: ({ tablist, tabs, activeIndex }) =>
|
|
224
|
+
h.div(
|
|
225
|
+
[h.Class('lg:flex lg:gap-8')],
|
|
226
|
+
[
|
|
227
|
+
h.div(
|
|
228
|
+
[h.Class('hidden w-60 shrink-0 lg:block')],
|
|
229
|
+
[
|
|
230
|
+
h.div(
|
|
231
|
+
[h.Class('sticky top-8')],
|
|
232
|
+
[
|
|
233
|
+
h.div(
|
|
234
|
+
[...tablist, h.Class('space-y-0.5')],
|
|
235
|
+
Array.map(tabs, tab =>
|
|
236
|
+
stepTabButton(
|
|
237
|
+
tab,
|
|
238
|
+
model.currentStep,
|
|
239
|
+
attentionStepSet,
|
|
240
|
+
h,
|
|
241
|
+
),
|
|
242
|
+
),
|
|
243
|
+
),
|
|
244
|
+
],
|
|
245
|
+
),
|
|
246
|
+
],
|
|
247
|
+
),
|
|
248
|
+
...pipe(
|
|
249
|
+
tabs,
|
|
250
|
+
Array.filter(tab => tab.index === activeIndex),
|
|
251
|
+
Array.map(tab =>
|
|
252
|
+
stepContentPanel(model, attentionSteps, tab.panel, h),
|
|
253
|
+
),
|
|
254
|
+
),
|
|
255
|
+
desktopPreviewSidebar(model, h),
|
|
256
|
+
mobilePreviewToggle(model, h),
|
|
257
|
+
...(model.isPreviewVisible ? [mobilePreviewOverlay(model, h)] : []),
|
|
258
|
+
],
|
|
259
|
+
),
|
|
260
|
+
},
|
|
261
|
+
toParentMessage: message => Message.GotStepTabsMessage({ message }),
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
const desktopPreviewSidebar = (model: Model, h: HtmlBuilder<Message>): Html =>
|
|
265
|
+
h.div(
|
|
266
|
+
[h.Class('hidden w-80 shrink-0 xl:block')],
|
|
267
|
+
[
|
|
268
|
+
h.div(
|
|
269
|
+
[h.Class('sticky top-8')],
|
|
270
|
+
[
|
|
271
|
+
h.div(
|
|
272
|
+
[h.Class('mb-2 flex items-center justify-between')],
|
|
273
|
+
[
|
|
274
|
+
h.h2(
|
|
275
|
+
[h.Class('text-sm font-semibold text-gray-700')],
|
|
276
|
+
['Live Preview'],
|
|
277
|
+
),
|
|
278
|
+
],
|
|
279
|
+
),
|
|
280
|
+
h.div(
|
|
281
|
+
[
|
|
282
|
+
h.Class(
|
|
283
|
+
'rounded-xl border border-gray-200 bg-white p-6 shadow-sm',
|
|
284
|
+
),
|
|
285
|
+
],
|
|
286
|
+
[preview(model)],
|
|
287
|
+
),
|
|
288
|
+
],
|
|
289
|
+
),
|
|
290
|
+
],
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
const mobilePreviewToggle = (model: Model, h: HtmlBuilder<Message>): Html =>
|
|
294
|
+
h.div(
|
|
295
|
+
[h.Class('fixed top-4 right-4 xl:hidden')],
|
|
296
|
+
[
|
|
297
|
+
Button.view(
|
|
298
|
+
{
|
|
299
|
+
onClick: Message.ToggledPreview(),
|
|
300
|
+
toView: attributes =>
|
|
301
|
+
h.button(
|
|
302
|
+
[
|
|
303
|
+
...attributes.button,
|
|
304
|
+
h.Class(
|
|
305
|
+
clsx(
|
|
306
|
+
'rounded-full px-4 py-2 text-sm font-medium shadow-lg transition cursor-pointer',
|
|
307
|
+
model.isPreviewVisible
|
|
308
|
+
? 'bg-gray-800 text-white'
|
|
309
|
+
: 'bg-indigo-600 text-white',
|
|
310
|
+
),
|
|
311
|
+
),
|
|
312
|
+
],
|
|
313
|
+
[model.isPreviewVisible ? 'Hide Preview' : 'Preview'],
|
|
314
|
+
),
|
|
315
|
+
},
|
|
316
|
+
h,
|
|
317
|
+
),
|
|
318
|
+
],
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
const mobilePreviewOverlay = (model: Model, h: HtmlBuilder<Message>): Html =>
|
|
322
|
+
h.div(
|
|
323
|
+
[
|
|
324
|
+
h.Class(
|
|
325
|
+
'fixed inset-x-4 top-16 bottom-4 overflow-y-auto rounded-xl border border-gray-200 bg-white p-6 shadow-2xl xl:hidden',
|
|
326
|
+
),
|
|
327
|
+
],
|
|
328
|
+
[preview(model)],
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
export const view = (model: Model, h: HtmlBuilder<Message>): Document => {
|
|
332
|
+
const attentionSteps = stepsNeedingAttention(model)
|
|
333
|
+
const attentionStepSet = HashSet.fromIterable(attentionSteps)
|
|
334
|
+
const body = h.div(
|
|
335
|
+
[h.Class('min-h-screen bg-gray-50')],
|
|
336
|
+
[
|
|
337
|
+
h.div(
|
|
338
|
+
[h.Class('mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8')],
|
|
339
|
+
[
|
|
340
|
+
pageHeader(h),
|
|
341
|
+
h.div(
|
|
342
|
+
[h.Class('mb-6 lg:hidden')],
|
|
343
|
+
[
|
|
344
|
+
stepMenu(
|
|
345
|
+
model,
|
|
346
|
+
attentionStepSet,
|
|
347
|
+
message => Message.GotStepMenuMessage({ message }),
|
|
348
|
+
h,
|
|
349
|
+
),
|
|
350
|
+
],
|
|
351
|
+
),
|
|
352
|
+
stepTabsLayout(model, attentionSteps, attentionStepSet, h),
|
|
353
|
+
],
|
|
354
|
+
),
|
|
355
|
+
],
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
return { title: 'Job Application', body }
|
|
359
|
+
}
|
|
@@ -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",
|
|
@@ -23,7 +23,7 @@ import type { Column } from './domain/column'
|
|
|
23
23
|
import { Message } from './message'
|
|
24
24
|
import type { Model } from './model'
|
|
25
25
|
import { update } from './update'
|
|
26
|
-
import { view } from './view
|
|
26
|
+
import { view } from './view'
|
|
27
27
|
|
|
28
28
|
const card = (id: string, title: string, sortKey: string): Card => ({
|
|
29
29
|
id,
|
|
@@ -4,7 +4,6 @@ import { generateKeyBetween } from 'fractional-indexing'
|
|
|
4
4
|
import { describe, expect, test } from 'vitest'
|
|
5
5
|
|
|
6
6
|
import { DragAndDrop } from '@foldkit/ui'
|
|
7
|
-
import { Message as DragAndDropMessage } from '@foldkit/ui/dragAndDrop'
|
|
8
7
|
|
|
9
8
|
import { FocusAddCardInput, GenerateCardId, SaveBoard } from './command'
|
|
10
9
|
import { Column } from './domain'
|
|
@@ -156,7 +155,7 @@ describe('update', () => {
|
|
|
156
155
|
given(emptyModel),
|
|
157
156
|
message(
|
|
158
157
|
Message.GotDragAndDropMessage({
|
|
159
|
-
message:
|
|
158
|
+
message: DragAndDrop.Message.PressedDraggable({
|
|
160
159
|
itemId: firstCardId,
|
|
161
160
|
containerId: 'todo',
|
|
162
161
|
index: 0,
|
|
@@ -167,7 +166,7 @@ describe('update', () => {
|
|
|
167
166
|
),
|
|
168
167
|
message(
|
|
169
168
|
Message.GotDragAndDropMessage({
|
|
170
|
-
message:
|
|
169
|
+
message: DragAndDrop.Message.MovedPointer({
|
|
171
170
|
screenX: 100,
|
|
172
171
|
screenY: 200,
|
|
173
172
|
clientX: 100,
|
|
@@ -178,7 +177,7 @@ describe('update', () => {
|
|
|
178
177
|
),
|
|
179
178
|
message(
|
|
180
179
|
Message.GotDragAndDropMessage({
|
|
181
|
-
message:
|
|
180
|
+
message: DragAndDrop.Message.ReleasedPointer(),
|
|
182
181
|
}),
|
|
183
182
|
),
|
|
184
183
|
Command.resolve(SaveBoard, Message.CompletedSaveBoard()),
|
|
@@ -197,7 +196,7 @@ describe('update', () => {
|
|
|
197
196
|
given(emptyModel),
|
|
198
197
|
message(
|
|
199
198
|
Message.GotDragAndDropMessage({
|
|
200
|
-
message:
|
|
199
|
+
message: DragAndDrop.Message.PressedDraggable({
|
|
201
200
|
itemId: cardId,
|
|
202
201
|
containerId: 'todo',
|
|
203
202
|
index: 0,
|
|
@@ -208,7 +207,7 @@ describe('update', () => {
|
|
|
208
207
|
),
|
|
209
208
|
message(
|
|
210
209
|
Message.GotDragAndDropMessage({
|
|
211
|
-
message:
|
|
210
|
+
message: DragAndDrop.Message.MovedPointer({
|
|
212
211
|
screenX: 300,
|
|
213
212
|
screenY: 100,
|
|
214
213
|
clientX: 300,
|
|
@@ -222,7 +221,7 @@ describe('update', () => {
|
|
|
222
221
|
),
|
|
223
222
|
message(
|
|
224
223
|
Message.GotDragAndDropMessage({
|
|
225
|
-
message:
|
|
224
|
+
message: DragAndDrop.Message.ReleasedPointer(),
|
|
226
225
|
}),
|
|
227
226
|
),
|
|
228
227
|
Command.resolve(SaveBoard, Message.CompletedSaveBoard()),
|
|
@@ -246,7 +245,7 @@ describe('update', () => {
|
|
|
246
245
|
given(emptyModel),
|
|
247
246
|
message(
|
|
248
247
|
Message.GotDragAndDropMessage({
|
|
249
|
-
message:
|
|
248
|
+
message: DragAndDrop.Message.ActivatedKeyboardDrag({
|
|
250
249
|
itemId: firstCardId,
|
|
251
250
|
containerId: 'todo',
|
|
252
251
|
index: 0,
|
|
@@ -255,7 +254,7 @@ describe('update', () => {
|
|
|
255
254
|
),
|
|
256
255
|
message(
|
|
257
256
|
Message.GotDragAndDropMessage({
|
|
258
|
-
message:
|
|
257
|
+
message: DragAndDrop.Message.CompletedResolveKeyboardMove({
|
|
259
258
|
targetContainerId: 'todo',
|
|
260
259
|
targetIndex: 2,
|
|
261
260
|
}),
|
|
@@ -263,16 +262,16 @@ describe('update', () => {
|
|
|
263
262
|
),
|
|
264
263
|
Command.resolve(
|
|
265
264
|
DragAndDrop.FocusItem({ itemId: firstCardId }),
|
|
266
|
-
|
|
265
|
+
DragAndDrop.Message.CompletedFocusItem(),
|
|
267
266
|
),
|
|
268
267
|
message(
|
|
269
268
|
Message.GotDragAndDropMessage({
|
|
270
|
-
message:
|
|
269
|
+
message: DragAndDrop.Message.ConfirmedKeyboardDrop(),
|
|
271
270
|
}),
|
|
272
271
|
),
|
|
273
272
|
Command.resolve(
|
|
274
273
|
DragAndDrop.FocusItem,
|
|
275
|
-
|
|
274
|
+
DragAndDrop.Message.CompletedFocusItem(),
|
|
276
275
|
),
|
|
277
276
|
Command.resolve(SaveBoard, Message.CompletedSaveBoard()),
|
|
278
277
|
model(model => {
|
|
@@ -290,7 +289,7 @@ describe('update', () => {
|
|
|
290
289
|
given(emptyModel),
|
|
291
290
|
message(
|
|
292
291
|
Message.GotDragAndDropMessage({
|
|
293
|
-
message:
|
|
292
|
+
message: DragAndDrop.Message.ActivatedKeyboardDrag({
|
|
294
293
|
itemId: cardId,
|
|
295
294
|
containerId: 'todo',
|
|
296
295
|
index: 0,
|
|
@@ -299,7 +298,7 @@ describe('update', () => {
|
|
|
299
298
|
),
|
|
300
299
|
message(
|
|
301
300
|
Message.GotDragAndDropMessage({
|
|
302
|
-
message:
|
|
301
|
+
message: DragAndDrop.Message.CompletedResolveKeyboardMove({
|
|
303
302
|
targetContainerId: 'in-progress',
|
|
304
303
|
targetIndex: 0,
|
|
305
304
|
}),
|
|
@@ -307,16 +306,16 @@ describe('update', () => {
|
|
|
307
306
|
),
|
|
308
307
|
Command.resolve(
|
|
309
308
|
DragAndDrop.FocusItem({ itemId: cardId }),
|
|
310
|
-
|
|
309
|
+
DragAndDrop.Message.CompletedFocusItem(),
|
|
311
310
|
),
|
|
312
311
|
message(
|
|
313
312
|
Message.GotDragAndDropMessage({
|
|
314
|
-
message:
|
|
313
|
+
message: DragAndDrop.Message.ConfirmedKeyboardDrop(),
|
|
315
314
|
}),
|
|
316
315
|
),
|
|
317
316
|
Command.resolve(
|
|
318
317
|
DragAndDrop.FocusItem,
|
|
319
|
-
|
|
318
|
+
DragAndDrop.Message.CompletedFocusItem(),
|
|
320
319
|
),
|
|
321
320
|
Command.resolve(SaveBoard, Message.CompletedSaveBoard()),
|
|
322
321
|
model(model => {
|
|
@@ -338,7 +337,7 @@ describe('update', () => {
|
|
|
338
337
|
given(emptyModel),
|
|
339
338
|
message(
|
|
340
339
|
Message.GotDragAndDropMessage({
|
|
341
|
-
message:
|
|
340
|
+
message: DragAndDrop.Message.ActivatedKeyboardDrag({
|
|
342
341
|
itemId: emptyModel.columns[0]!.cards[0]!.id,
|
|
343
342
|
containerId: 'todo',
|
|
344
343
|
index: 0,
|
|
@@ -347,12 +346,12 @@ describe('update', () => {
|
|
|
347
346
|
),
|
|
348
347
|
message(
|
|
349
348
|
Message.GotDragAndDropMessage({
|
|
350
|
-
message:
|
|
349
|
+
message: DragAndDrop.Message.CancelledDrag(),
|
|
351
350
|
}),
|
|
352
351
|
),
|
|
353
352
|
Command.resolve(
|
|
354
353
|
DragAndDrop.FocusItem,
|
|
355
|
-
|
|
354
|
+
DragAndDrop.Message.CompletedFocusItem(),
|
|
356
355
|
),
|
|
357
356
|
model(model => {
|
|
358
357
|
expect(model.columns).toStrictEqual(emptyModel.columns)
|
|
@@ -366,7 +365,7 @@ describe('update', () => {
|
|
|
366
365
|
given(emptyModel),
|
|
367
366
|
message(
|
|
368
367
|
Message.GotDragAndDropMessage({
|
|
369
|
-
message:
|
|
368
|
+
message: DragAndDrop.Message.PressedDraggable({
|
|
370
369
|
itemId: emptyModel.columns[0]!.cards[0]!.id,
|
|
371
370
|
containerId: 'todo',
|
|
372
371
|
index: 0,
|
|
@@ -377,7 +376,7 @@ describe('update', () => {
|
|
|
377
376
|
),
|
|
378
377
|
message(
|
|
379
378
|
Message.GotDragAndDropMessage({
|
|
380
|
-
message:
|
|
379
|
+
message: DragAndDrop.Message.MovedPointer({
|
|
381
380
|
screenX: 100,
|
|
382
381
|
screenY: 200,
|
|
383
382
|
clientX: 100,
|
|
@@ -388,7 +387,7 @@ describe('update', () => {
|
|
|
388
387
|
),
|
|
389
388
|
message(
|
|
390
389
|
Message.GotDragAndDropMessage({
|
|
391
|
-
message:
|
|
390
|
+
message: DragAndDrop.Message.CancelledDrag(),
|
|
392
391
|
}),
|
|
393
392
|
),
|
|
394
393
|
model(model => {
|
|
@@ -1,71 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { type Document, HtmlBuilder } from 'foldkit/html'
|
|
3
|
-
|
|
4
|
-
import { DragAndDrop } from '@foldkit/ui'
|
|
5
|
-
|
|
6
|
-
import { Message } from '../message'
|
|
7
|
-
import type { Model } from '../model'
|
|
8
|
-
import { ghostCardView } from './card'
|
|
9
|
-
import { columnView } from './column'
|
|
10
|
-
|
|
11
|
-
const findDraggedCard = (model: Model) =>
|
|
12
|
-
pipe(
|
|
13
|
-
model.dragAndDrop,
|
|
14
|
-
DragAndDrop.maybeDraggedItemId,
|
|
15
|
-
Option.flatMap(cardId =>
|
|
16
|
-
pipe(
|
|
17
|
-
model.columns,
|
|
18
|
-
Array.flatMap(({ cards }) => cards),
|
|
19
|
-
Array.findFirst(({ id }) => id === cardId),
|
|
20
|
-
),
|
|
21
|
-
),
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
const ghostElement = (model: Model, h: HtmlBuilder<Message>) =>
|
|
25
|
-
pipe(
|
|
26
|
-
DragAndDrop.ghostStyle(model.dragAndDrop),
|
|
27
|
-
Option.flatMap(ghostStyle =>
|
|
28
|
-
Option.map(findDraggedCard(model), card => ({ ghostStyle, card })),
|
|
29
|
-
),
|
|
30
|
-
Option.match({
|
|
31
|
-
onNone: () => h.empty,
|
|
32
|
-
onSome: ({ ghostStyle, card }) =>
|
|
33
|
-
h.div(
|
|
34
|
-
[h.Style(ghostStyle), h.Class('w-64'), h.AriaHidden(true)],
|
|
35
|
-
[ghostCardView(card, h)],
|
|
36
|
-
),
|
|
37
|
-
}),
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
export const view = (model: Model, h: HtmlBuilder<Message>): Document => ({
|
|
41
|
-
title: 'Kanban Board',
|
|
42
|
-
body: h.div(
|
|
43
|
-
[h.Class('flex flex-col min-h-screen bg-gray-100')],
|
|
44
|
-
[
|
|
45
|
-
h.div(
|
|
46
|
-
[h.Class('px-6 py-4 bg-white border-b border-gray-200')],
|
|
47
|
-
[
|
|
48
|
-
h.h1(
|
|
49
|
-
[h.Class('text-lg font-semibold text-gray-900')],
|
|
50
|
-
['Kanban Board'],
|
|
51
|
-
),
|
|
52
|
-
],
|
|
53
|
-
),
|
|
54
|
-
h.div(
|
|
55
|
-
[
|
|
56
|
-
h.Class(
|
|
57
|
-
'flex-1 p-6 grid grid-cols-[repeat(auto-fit,minmax(16rem,1fr))] gap-6 items-start',
|
|
58
|
-
),
|
|
59
|
-
],
|
|
60
|
-
Array.map(model.columns, column =>
|
|
61
|
-
columnView(model, column, message => message, h),
|
|
62
|
-
),
|
|
63
|
-
),
|
|
64
|
-
ghostElement(model, h),
|
|
65
|
-
h.div(
|
|
66
|
-
[h.Class('sr-only'), h.AriaLive('assertive')],
|
|
67
|
-
[model.announcement],
|
|
68
|
-
),
|
|
69
|
-
],
|
|
70
|
-
),
|
|
71
|
-
})
|
|
1
|
+
export { view } from './view'
|