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,71 @@
|
|
|
1
|
+
import { Array, Option, pipe } from 'effect'
|
|
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
|
+
})
|
|
@@ -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",
|
|
@@ -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",
|
|
@@ -3,8 +3,6 @@ import { Command, click, expect, given, role, scene, text } from 'foldkit/scene'
|
|
|
3
3
|
import { describe, test } from 'vitest'
|
|
4
4
|
|
|
5
5
|
import { Dialog, Listbox, RadioGroup } from '@foldkit/ui'
|
|
6
|
-
import { Message as DialogMessage } from '@foldkit/ui/dialog'
|
|
7
|
-
import { Message as RadioGroupMessage } from '@foldkit/ui/radioGroup'
|
|
8
6
|
|
|
9
7
|
import { ExportPng, SaveCanvas } from './command'
|
|
10
8
|
import { createEmptyGrid } from './grid'
|
|
@@ -15,7 +13,7 @@ import { view } from './view'
|
|
|
15
13
|
|
|
16
14
|
const resolveFocusOption = Command.resolve(
|
|
17
15
|
RadioGroup.FocusOption,
|
|
18
|
-
|
|
16
|
+
RadioGroup.Message.CompletedFocusOption(),
|
|
19
17
|
)
|
|
20
18
|
|
|
21
19
|
const createTestModel = (): Model => ({
|
|
@@ -69,7 +67,7 @@ describe('export workflow', () => {
|
|
|
69
67
|
ExportPng,
|
|
70
68
|
Message.FailedExportPng({ error: 'Canvas 2D context not available' }),
|
|
71
69
|
),
|
|
72
|
-
Command.resolve(Dialog.ShowDialog,
|
|
70
|
+
Command.resolve(Dialog.ShowDialog, Dialog.Message.SucceededShowDialog()),
|
|
73
71
|
expect(text('Export Failed')).toExist(),
|
|
74
72
|
expect(text('Canvas 2D context not available')).toExist(),
|
|
75
73
|
expect(role('button', { name: 'Dismiss' })).toExist(),
|
|
@@ -85,10 +83,13 @@ describe('export workflow', () => {
|
|
|
85
83
|
ExportPng,
|
|
86
84
|
Message.FailedExportPng({ error: 'Canvas 2D context not available' }),
|
|
87
85
|
),
|
|
88
|
-
Command.resolve(Dialog.ShowDialog,
|
|
86
|
+
Command.resolve(Dialog.ShowDialog, Dialog.Message.SucceededShowDialog()),
|
|
89
87
|
expect(text('Export Failed')).toExist(),
|
|
90
88
|
click(role('button', { name: 'Dismiss' })),
|
|
91
|
-
Command.resolve(
|
|
89
|
+
Command.resolve(
|
|
90
|
+
Dialog.CloseDialog,
|
|
91
|
+
Dialog.Message.CompletedCloseDialog(),
|
|
92
|
+
),
|
|
92
93
|
expect(text('Export Failed')).toBeAbsent(),
|
|
93
94
|
)
|
|
94
95
|
})
|
|
@@ -196,7 +197,7 @@ describe('grid size change', () => {
|
|
|
196
197
|
given(createPaintedModel()),
|
|
197
198
|
click(role('radio', { name: '8' })),
|
|
198
199
|
resolveFocusOption,
|
|
199
|
-
Command.resolve(Dialog.ShowDialog,
|
|
200
|
+
Command.resolve(Dialog.ShowDialog, Dialog.Message.SucceededShowDialog()),
|
|
200
201
|
expect(text('Change to 8\u00d78?')).toExist(),
|
|
201
202
|
expect(
|
|
202
203
|
text('This will clear your canvas and reset undo history.'),
|
|
@@ -222,7 +223,10 @@ describe('grid size change', () => {
|
|
|
222
223
|
given(modelWithPendingResize),
|
|
223
224
|
expect(text('Change to 8\u00d78?')).toExist(),
|
|
224
225
|
click(role('button', { name: 'Clear and Resize' })),
|
|
225
|
-
Command.resolve(
|
|
226
|
+
Command.resolve(
|
|
227
|
+
Dialog.CloseDialog,
|
|
228
|
+
Dialog.Message.CompletedCloseDialog(),
|
|
229
|
+
),
|
|
226
230
|
Command.resolve(SaveCanvas, Message.CompletedSaveCanvas()),
|
|
227
231
|
expect(text('Change to 8\u00d78?')).toBeAbsent(),
|
|
228
232
|
)
|
|
@@ -243,7 +247,10 @@ describe('grid size change', () => {
|
|
|
243
247
|
given(modelWithPendingResize),
|
|
244
248
|
expect(text('Change to 8\u00d78?')).toExist(),
|
|
245
249
|
click(role('button', { name: 'Cancel' })),
|
|
246
|
-
Command.resolve(
|
|
250
|
+
Command.resolve(
|
|
251
|
+
Dialog.CloseDialog,
|
|
252
|
+
Dialog.Message.CompletedCloseDialog(),
|
|
253
|
+
),
|
|
247
254
|
expect(text('Change to 8\u00d78?')).toBeAbsent(),
|
|
248
255
|
)
|
|
249
256
|
})
|
|
@@ -3,7 +3,6 @@ import { Command, given, message, model, story } from 'foldkit/story'
|
|
|
3
3
|
import { describe, expect, test } from 'vitest'
|
|
4
4
|
|
|
5
5
|
import { Dialog, Listbox, RadioGroup } from '@foldkit/ui'
|
|
6
|
-
import { Message as DialogMessage } from '@foldkit/ui/dialog'
|
|
7
6
|
|
|
8
7
|
import { ExportPng, SaveCanvas } from './command'
|
|
9
8
|
import { createEmptyGrid } from './grid'
|
|
@@ -304,7 +303,7 @@ describe('grid size', () => {
|
|
|
304
303
|
update,
|
|
305
304
|
given(paintedModel),
|
|
306
305
|
message(Message.SelectedGridSize({ size: 8 })),
|
|
307
|
-
Command.resolve(Dialog.ShowDialog,
|
|
306
|
+
Command.resolve(Dialog.ShowDialog, Dialog.Message.SucceededShowDialog()),
|
|
308
307
|
model(model => {
|
|
309
308
|
expect(model.maybePendingGridSize).toEqual(Option.some(8))
|
|
310
309
|
expect(model.gridSizeConfirmDialog.isOpen).toBe(true)
|
|
@@ -328,7 +327,10 @@ describe('grid size', () => {
|
|
|
328
327
|
update,
|
|
329
328
|
given(modelWithPending),
|
|
330
329
|
message(Message.ConfirmedGridSizeChange()),
|
|
331
|
-
Command.resolve(
|
|
330
|
+
Command.resolve(
|
|
331
|
+
Dialog.CloseDialog,
|
|
332
|
+
Dialog.Message.CompletedCloseDialog(),
|
|
333
|
+
),
|
|
332
334
|
Command.resolve(SaveCanvas, Message.CompletedSaveCanvas()),
|
|
333
335
|
model(model => {
|
|
334
336
|
expect(model.gridSize).toBe(8)
|
|
@@ -449,7 +451,7 @@ describe('export failure', () => {
|
|
|
449
451
|
message(
|
|
450
452
|
Message.FailedExportPng({ error: 'Canvas 2D context not available' }),
|
|
451
453
|
),
|
|
452
|
-
Command.resolve(Dialog.ShowDialog,
|
|
454
|
+
Command.resolve(Dialog.ShowDialog, Dialog.Message.SucceededShowDialog()),
|
|
453
455
|
model(model => {
|
|
454
456
|
expect(model.maybeExportError).toEqual(
|
|
455
457
|
Option.some('Canvas 2D context not available'),
|
|
@@ -466,13 +468,16 @@ describe('export failure', () => {
|
|
|
466
468
|
message(
|
|
467
469
|
Message.FailedExportPng({ error: 'Canvas 2D context not available' }),
|
|
468
470
|
),
|
|
469
|
-
Command.resolve(Dialog.ShowDialog,
|
|
471
|
+
Command.resolve(Dialog.ShowDialog, Dialog.Message.SucceededShowDialog()),
|
|
470
472
|
message(
|
|
471
473
|
Message.GotErrorDialogMessage({
|
|
472
|
-
message:
|
|
474
|
+
message: Dialog.Message.RequestedClose(),
|
|
473
475
|
}),
|
|
474
476
|
),
|
|
475
|
-
Command.resolve(
|
|
477
|
+
Command.resolve(
|
|
478
|
+
Dialog.CloseDialog,
|
|
479
|
+
Dialog.Message.CompletedCloseDialog(),
|
|
480
|
+
),
|
|
476
481
|
model(model => {
|
|
477
482
|
expect(model.maybeExportError).toEqual(Option.none())
|
|
478
483
|
expect(model.errorDialog.isOpen).toBe(false)
|
|
@@ -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",
|
|
@@ -4,7 +4,6 @@ import { fromString } from 'foldkit/url'
|
|
|
4
4
|
import { describe, expect, test } from 'vitest'
|
|
5
5
|
|
|
6
6
|
import { Listbox } from '@foldkit/ui'
|
|
7
|
-
import { Message as ListboxMessage } from '@foldkit/ui/listbox'
|
|
8
7
|
|
|
9
8
|
import {
|
|
10
9
|
AppRoute,
|
|
@@ -138,23 +137,23 @@ describe('update', () => {
|
|
|
138
137
|
given(browseModel),
|
|
139
138
|
message(
|
|
140
139
|
Message.GotDietListboxMessage({
|
|
141
|
-
message:
|
|
140
|
+
message: Listbox.Message.Opened({
|
|
142
141
|
maybeActiveItemIndex: Option.none(),
|
|
143
142
|
}),
|
|
144
143
|
}),
|
|
145
144
|
),
|
|
146
145
|
Command.resolve(
|
|
147
146
|
Listbox.FocusItems,
|
|
148
|
-
|
|
147
|
+
Listbox.Message.CompletedFocusItems(),
|
|
149
148
|
),
|
|
150
149
|
message(
|
|
151
150
|
Message.GotDietListboxMessage({
|
|
152
|
-
message:
|
|
151
|
+
message: Listbox.Message.SelectedItem({ item: 'Carnivore' }),
|
|
153
152
|
}),
|
|
154
153
|
),
|
|
155
154
|
Command.resolve(
|
|
156
155
|
Listbox.FocusButton,
|
|
157
|
-
|
|
156
|
+
Listbox.Message.CompletedFocusButton(),
|
|
158
157
|
),
|
|
159
158
|
Command.expectHas(ReplaceFilters),
|
|
160
159
|
Command.resolve(ReplaceFilters, Message.CompletedReplaceFilters()),
|
|
@@ -17,9 +17,7 @@
|
|
|
17
17
|
"@foldkit/devtools": "workspace:*",
|
|
18
18
|
"@foldkit/vite-plugin": "workspace:*",
|
|
19
19
|
"@tailwindcss/vite": "^4.3.3",
|
|
20
|
-
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
|
21
20
|
"happy-dom": "^20.11.13",
|
|
22
|
-
"prettier": "^3.9.6",
|
|
23
21
|
"tailwindcss": "^4.3.3",
|
|
24
22
|
"typescript": "^7.0.2",
|
|
25
23
|
"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",
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Array, String } from 'effect'
|
|
2
|
+
import { expect, given, role, scene, text } from 'foldkit/scene'
|
|
3
|
+
import { describe, test } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { Model, SearchResults, searchPeople, update, view } from './people'
|
|
6
|
+
|
|
7
|
+
const loaded = (searchInput: string) =>
|
|
8
|
+
Model.make({
|
|
9
|
+
searchInput,
|
|
10
|
+
searchHistory: Array.liftPredicate(String.isNonEmpty)(searchInput),
|
|
11
|
+
results: SearchResults.Loaded({
|
|
12
|
+
query: searchInput,
|
|
13
|
+
people: searchPeople(searchInput),
|
|
14
|
+
}),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('people', () => {
|
|
18
|
+
test('lists every person', () => {
|
|
19
|
+
scene(
|
|
20
|
+
{ update, view },
|
|
21
|
+
given(loaded('')),
|
|
22
|
+
expect(role('heading', { name: 'People' })).toExist(),
|
|
23
|
+
expect(text('Alice Johnson')).toExist(),
|
|
24
|
+
expect(text('Bob Smith')).toExist(),
|
|
25
|
+
expect(text('Carol Davis')).toExist(),
|
|
26
|
+
expect(text('David Wilson')).toExist(),
|
|
27
|
+
expect(text('Eva Brown')).toExist(),
|
|
28
|
+
)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('a search filters matches by name or role', () => {
|
|
32
|
+
scene(
|
|
33
|
+
{ update, view },
|
|
34
|
+
given(loaded('designer')),
|
|
35
|
+
expect(text('Alice Johnson')).toExist(),
|
|
36
|
+
expect(text('Eva Brown')).toExist(),
|
|
37
|
+
expect(text('Bob Smith')).toBeAbsent(),
|
|
38
|
+
expect(text('2 results', { exact: false })).toExist(),
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Array, String } from 'effect'
|
|
2
|
+
import { Command, given, message, model, story } from 'foldkit/story'
|
|
3
|
+
import { describe, expect, test } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Message,
|
|
7
|
+
Model,
|
|
8
|
+
PushSearchUrl,
|
|
9
|
+
SearchResults,
|
|
10
|
+
searchPeople,
|
|
11
|
+
update,
|
|
12
|
+
} from './people'
|
|
13
|
+
|
|
14
|
+
const givenLoaded = (searchInput: string) =>
|
|
15
|
+
given(
|
|
16
|
+
Model.make({
|
|
17
|
+
searchInput,
|
|
18
|
+
searchHistory: Array.liftPredicate(String.isNonEmpty)(searchInput),
|
|
19
|
+
results: SearchResults.Loaded({
|
|
20
|
+
query: searchInput,
|
|
21
|
+
people: searchPeople(searchInput),
|
|
22
|
+
}),
|
|
23
|
+
}),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
describe('people', () => {
|
|
27
|
+
test('ChangedSearchInput updates the input without recording history or fetching', () => {
|
|
28
|
+
story(
|
|
29
|
+
update,
|
|
30
|
+
givenLoaded(''),
|
|
31
|
+
message(Message.ChangedSearchInput({ value: 'd' })),
|
|
32
|
+
message(Message.ChangedSearchInput({ value: 'de' })),
|
|
33
|
+
message(Message.ChangedSearchInput({ value: 'designer' })),
|
|
34
|
+
Command.expectNone(),
|
|
35
|
+
model(model => {
|
|
36
|
+
expect(model.searchInput).toBe('designer')
|
|
37
|
+
expect(model.searchHistory).toStrictEqual([])
|
|
38
|
+
}),
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('SubmittedSearch pushes the current input to the URL', () => {
|
|
43
|
+
story(
|
|
44
|
+
update,
|
|
45
|
+
givenLoaded('designer'),
|
|
46
|
+
message(Message.SubmittedSearch()),
|
|
47
|
+
Command.expectHas(PushSearchUrl),
|
|
48
|
+
Command.resolve(PushSearchUrl, Message.CompletedPushSearchUrl()),
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('SucceededFetchPeople stores the loaded results', () => {
|
|
53
|
+
const people = searchPeople('designer')
|
|
54
|
+
|
|
55
|
+
story(
|
|
56
|
+
update,
|
|
57
|
+
givenLoaded(''),
|
|
58
|
+
message(
|
|
59
|
+
Message.SucceededFetchPeople({
|
|
60
|
+
query: 'designer',
|
|
61
|
+
people,
|
|
62
|
+
}),
|
|
63
|
+
),
|
|
64
|
+
model(model => {
|
|
65
|
+
expect(model.results._tag).toBe('Loaded')
|
|
66
|
+
if (model.results._tag === 'Loaded') {
|
|
67
|
+
expect(model.results.query).toBe('designer')
|
|
68
|
+
expect(model.results.people).toStrictEqual(people)
|
|
69
|
+
}
|
|
70
|
+
}),
|
|
71
|
+
)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -80,26 +80,11 @@ describe('view', () => {
|
|
|
80
80
|
)
|
|
81
81
|
})
|
|
82
82
|
|
|
83
|
-
test('the People route
|
|
83
|
+
test('the People route renders the People heading', () => {
|
|
84
84
|
scene(
|
|
85
85
|
{ update, view },
|
|
86
86
|
given(people('')),
|
|
87
|
-
expect(
|
|
88
|
-
expect(text('Bob Smith')).toExist(),
|
|
89
|
-
expect(text('Carol Davis')).toExist(),
|
|
90
|
-
expect(text('David Wilson')).toExist(),
|
|
91
|
-
expect(text('Eva Brown')).toExist(),
|
|
92
|
-
)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
test('a search filters People to matches by name or role', () => {
|
|
96
|
-
scene(
|
|
97
|
-
{ update, view },
|
|
98
|
-
given(people('designer')),
|
|
99
|
-
expect(text('Alice Johnson')).toExist(),
|
|
100
|
-
expect(text('Eva Brown')).toExist(),
|
|
101
|
-
expect(text('Bob Smith')).toBeAbsent(),
|
|
102
|
-
expect(text('2 results', { exact: false })).toExist(),
|
|
87
|
+
expect(role('heading', { name: 'People' })).toExist(),
|
|
103
88
|
)
|
|
104
89
|
})
|
|
105
90
|
|
|
@@ -5,7 +5,6 @@ import { describe, expect, test } from 'vitest'
|
|
|
5
5
|
|
|
6
6
|
import { AppRoute, Message, Model, update } from './main'
|
|
7
7
|
import { People } from './page'
|
|
8
|
-
import { Message as PeopleMessage } from './page/people'
|
|
9
8
|
|
|
10
9
|
const peoplePageWith = (searchInput: string) =>
|
|
11
10
|
People.Model.make({
|
|
@@ -41,7 +40,7 @@ const urlOrThrow = (raw: string) =>
|
|
|
41
40
|
const resolveFetch = (searchText: string) =>
|
|
42
41
|
Command.resolve(
|
|
43
42
|
People.FetchPeople,
|
|
44
|
-
|
|
43
|
+
People.Message.SucceededFetchPeople({
|
|
45
44
|
query: searchText,
|
|
46
45
|
people: People.searchPeople(searchText),
|
|
47
46
|
}),
|
|
@@ -198,48 +197,19 @@ describe('update', () => {
|
|
|
198
197
|
})
|
|
199
198
|
|
|
200
199
|
describe('GotPeopleMessage', () => {
|
|
201
|
-
test('
|
|
200
|
+
test('GotPeopleMessage writes through to peoplePage', () => {
|
|
202
201
|
story(
|
|
203
202
|
update,
|
|
204
203
|
given(onPeople('')),
|
|
205
204
|
message(
|
|
206
205
|
Message.GotPeopleMessage({
|
|
207
|
-
message:
|
|
206
|
+
message: People.Message.ChangedSearchInput({ value: 'd' }),
|
|
208
207
|
}),
|
|
209
208
|
),
|
|
210
|
-
message(
|
|
211
|
-
Message.GotPeopleMessage({
|
|
212
|
-
message: PeopleMessage.ChangedSearchInput({ value: 'de' }),
|
|
213
|
-
}),
|
|
214
|
-
),
|
|
215
|
-
message(
|
|
216
|
-
Message.GotPeopleMessage({
|
|
217
|
-
message: PeopleMessage.ChangedSearchInput({ value: 'designer' }),
|
|
218
|
-
}),
|
|
219
|
-
),
|
|
220
|
-
Command.expectNone(),
|
|
221
209
|
model(model => {
|
|
222
|
-
expect(model.peoplePage.searchInput).toBe('
|
|
223
|
-
expect(model.peoplePage.searchHistory).toStrictEqual([])
|
|
210
|
+
expect(model.peoplePage.searchInput).toBe('d')
|
|
224
211
|
}),
|
|
225
212
|
)
|
|
226
213
|
})
|
|
227
|
-
|
|
228
|
-
test('submitting the search pushes the current input to the URL', () => {
|
|
229
|
-
story(
|
|
230
|
-
update,
|
|
231
|
-
given(onPeople('designer')),
|
|
232
|
-
message(
|
|
233
|
-
Message.GotPeopleMessage({
|
|
234
|
-
message: PeopleMessage.SubmittedSearch(),
|
|
235
|
-
}),
|
|
236
|
-
),
|
|
237
|
-
Command.expectHas(People.PushSearchUrl),
|
|
238
|
-
Command.resolve(
|
|
239
|
-
People.PushSearchUrl,
|
|
240
|
-
PeopleMessage.CompletedPushSearchUrl(),
|
|
241
|
-
),
|
|
242
|
-
)
|
|
243
|
-
})
|
|
244
214
|
})
|
|
245
215
|
})
|
|
@@ -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",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { expect, given, role, scene, text, withViewInputs } from 'foldkit/scene'
|
|
2
|
+
import { describe, test } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { products } from '../data/products'
|
|
5
|
+
import { init, update, view } from './products'
|
|
6
|
+
|
|
7
|
+
const productsView = withViewInputs(view, { cart: [] })
|
|
8
|
+
|
|
9
|
+
describe('products', () => {
|
|
10
|
+
test('lists every product with an Add to Cart button', () => {
|
|
11
|
+
scene(
|
|
12
|
+
{ update, view: productsView() },
|
|
13
|
+
given(init(products)),
|
|
14
|
+
expect(role('heading', { name: 'Products' })).toExist(),
|
|
15
|
+
expect(text('Apple')).toExist(),
|
|
16
|
+
expect(text('Banana')).toExist(),
|
|
17
|
+
expect(role('button', { name: 'Add to Cart' })).toExist(),
|
|
18
|
+
)
|
|
19
|
+
})
|
|
20
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Command,
|
|
3
|
+
expectOutMessage,
|
|
4
|
+
given,
|
|
5
|
+
message,
|
|
6
|
+
model,
|
|
7
|
+
story,
|
|
8
|
+
} from 'foldkit/story'
|
|
9
|
+
import { describe, expect, test } from 'vitest'
|
|
10
|
+
|
|
11
|
+
import { products } from '../data/products'
|
|
12
|
+
import { Message, OutMessage, ReplaceSearchUrl, init, update } from './products'
|
|
13
|
+
|
|
14
|
+
const apple = { id: '1', name: 'Apple', price: 1.5 }
|
|
15
|
+
|
|
16
|
+
describe('products', () => {
|
|
17
|
+
test('ChangedSearchInput stores the query and replaces the search URL', () => {
|
|
18
|
+
story(
|
|
19
|
+
update,
|
|
20
|
+
given(init(products)),
|
|
21
|
+
message(Message.ChangedSearchInput({ value: 'app' })),
|
|
22
|
+
model(model => {
|
|
23
|
+
expect(model.searchText).toBe('app')
|
|
24
|
+
}),
|
|
25
|
+
Command.expectHas(ReplaceSearchUrl),
|
|
26
|
+
Command.resolve(ReplaceSearchUrl, Message.CompletedReplaceSearchUrl()),
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('ClickedAddToCart emits AddedToCart', () => {
|
|
31
|
+
story(
|
|
32
|
+
update,
|
|
33
|
+
given(init(products)),
|
|
34
|
+
message(Message.ClickedAddToCart({ item: apple })),
|
|
35
|
+
expectOutMessage(OutMessage.AddedToCart({ item: apple })),
|
|
36
|
+
)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('ClickedIncrementQuantity emits IncrementedQuantity', () => {
|
|
40
|
+
story(
|
|
41
|
+
update,
|
|
42
|
+
given(init(products)),
|
|
43
|
+
message(Message.ClickedIncrementQuantity({ itemId: '1' })),
|
|
44
|
+
expectOutMessage(OutMessage.IncrementedQuantity({ itemId: '1' })),
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('ClickedDecrementQuantity emits DecrementedQuantity', () => {
|
|
49
|
+
story(
|
|
50
|
+
update,
|
|
51
|
+
given(init(products)),
|
|
52
|
+
message(Message.ClickedDecrementQuantity({ itemId: '1' })),
|
|
53
|
+
expectOutMessage(OutMessage.DecrementedQuantity({ itemId: '1' })),
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -52,7 +52,7 @@ export const init = (products: ReadonlyArray<Item.Item>): Model => ({
|
|
|
52
52
|
|
|
53
53
|
// COMMAND
|
|
54
54
|
|
|
55
|
-
const ReplaceSearchUrl = Command.define('ReplaceSearchUrl', {
|
|
55
|
+
export const ReplaceSearchUrl = Command.define('ReplaceSearchUrl', {
|
|
56
56
|
args: { url: Schema.String },
|
|
57
57
|
messages: [Message.CompletedReplaceSearchUrl],
|
|
58
58
|
execute: ({ url }) =>
|
|
@@ -49,14 +49,11 @@ describe('view', () => {
|
|
|
49
49
|
)
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
test('the Products route
|
|
52
|
+
test('the Products route renders the Products heading', () => {
|
|
53
53
|
scene(
|
|
54
54
|
{ update, view },
|
|
55
55
|
given(baseModel),
|
|
56
56
|
expect(role('heading', { name: 'Products' })).toExist(),
|
|
57
|
-
expect(text('Apple')).toExist(),
|
|
58
|
-
expect(text('Banana')).toExist(),
|
|
59
|
-
expect(role('button', { name: 'Add to Cart' })).toExist(),
|
|
60
57
|
)
|
|
61
58
|
})
|
|
62
59
|
|
|
@@ -6,7 +6,6 @@ import { describe, expect, test } from 'vitest'
|
|
|
6
6
|
import { products } from './data/products'
|
|
7
7
|
import { Message, type Model, update } from './main'
|
|
8
8
|
import { Products } from './page'
|
|
9
|
-
import { Message as ProductsMessage } from './page/products'
|
|
10
9
|
import { AppRoute } from './route'
|
|
11
10
|
|
|
12
11
|
const apple = { id: '1', name: 'Apple', price: 1.5 }
|
|
@@ -90,7 +89,7 @@ describe('update', () => {
|
|
|
90
89
|
given(baseModel),
|
|
91
90
|
message(
|
|
92
91
|
Message.GotProductsMessage({
|
|
93
|
-
message:
|
|
92
|
+
message: Products.Message.ClickedAddToCart({ item: apple }),
|
|
94
93
|
}),
|
|
95
94
|
),
|
|
96
95
|
model(model => {
|
|
@@ -107,12 +106,12 @@ describe('update', () => {
|
|
|
107
106
|
given(baseModel),
|
|
108
107
|
message(
|
|
109
108
|
Message.GotProductsMessage({
|
|
110
|
-
message:
|
|
109
|
+
message: Products.Message.ClickedAddToCart({ item: apple }),
|
|
111
110
|
}),
|
|
112
111
|
),
|
|
113
112
|
message(
|
|
114
113
|
Message.GotProductsMessage({
|
|
115
|
-
message:
|
|
114
|
+
message: Products.Message.ClickedAddToCart({ item: apple }),
|
|
116
115
|
}),
|
|
117
116
|
),
|
|
118
117
|
model(model => {
|