create-foldkit-app 0.32.1 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/dist/templates/base/.oxfmtrc.json +28 -0
  2. package/dist/templates/base/.oxlintrc.json +4 -0
  3. package/dist/templates/base/FOLDKIT.md +1 -1
  4. package/dist/templates/base/package.json +1 -1
  5. package/dist/templates/examples/api-cache/package.json +0 -2
  6. package/dist/templates/examples/api-cache/src/scene.test.ts +1 -2
  7. package/dist/templates/examples/api-cache/src/story.test.ts +3 -4
  8. package/dist/templates/examples/auth/package.json +0 -2
  9. package/dist/templates/examples/canvas-art/package.json +0 -2
  10. package/dist/templates/examples/charting/package.json +0 -2
  11. package/dist/templates/examples/charting/src/scene.test.ts +1 -2
  12. package/dist/templates/examples/charting/src/story.test.ts +3 -4
  13. package/dist/templates/examples/counter/package.json +0 -2
  14. package/dist/templates/examples/counters/package.json +0 -2
  15. package/dist/templates/examples/crash-view/package.json +0 -2
  16. package/dist/templates/examples/embedding/package.json +0 -2
  17. package/dist/templates/examples/form/package.json +0 -2
  18. package/dist/templates/examples/generative-art/package.json +0 -2
  19. package/dist/templates/examples/interrupting-commands/package.json +0 -2
  20. package/dist/templates/examples/job-application/package.json +0 -2
  21. package/dist/templates/examples/job-application/src/main.fixture.ts +69 -0
  22. package/dist/templates/examples/job-application/src/main.ts +1 -1
  23. package/dist/templates/examples/job-application/src/scene.test.ts +6 -66
  24. package/dist/templates/examples/job-application/src/step/attachments/index.ts +2 -0
  25. package/dist/templates/examples/job-application/src/step/attachments/scene.test.ts +19 -0
  26. package/dist/templates/examples/job-application/src/step/attachments/story.test.ts +46 -0
  27. package/dist/templates/examples/job-application/src/{view/attachments.ts → step/attachments/view.ts} +4 -11
  28. package/dist/templates/examples/job-application/src/{view → step/coverLetter}/coverLetter.ts +36 -7
  29. package/dist/templates/examples/job-application/src/step/coverLetter/index.ts +1 -0
  30. package/dist/templates/examples/job-application/src/step/coverLetter/scene.test.ts +24 -0
  31. package/dist/templates/examples/job-application/src/step/coverLetter/story.test.ts +19 -0
  32. package/dist/templates/examples/job-application/src/step/education/{entry.ts → entry/entry.ts} +2 -2
  33. package/dist/templates/examples/job-application/src/step/education/entry/index.ts +11 -0
  34. package/dist/templates/examples/job-application/src/step/education/entry/story.test.ts +39 -0
  35. package/dist/templates/examples/job-application/src/step/education/entry/view.ts +160 -0
  36. package/dist/templates/examples/job-application/src/step/education/index.ts +11 -1
  37. package/dist/templates/examples/job-application/src/step/education/scene.test.ts +20 -0
  38. package/dist/templates/examples/job-application/src/step/education/story.test.ts +87 -0
  39. package/dist/templates/examples/job-application/src/{view/education.ts → step/education/view.ts} +6 -10
  40. package/dist/templates/examples/job-application/src/step/personalInfo/index.ts +11 -0
  41. package/dist/templates/examples/job-application/src/step/{personalInfo.ts → personalInfo/personalInfo.ts} +2 -2
  42. package/dist/templates/examples/job-application/src/step/personalInfo/scene.test.ts +38 -0
  43. package/dist/templates/examples/job-application/src/step/personalInfo/story.test.ts +87 -0
  44. package/dist/templates/examples/job-application/src/{view/personalInfo.ts → step/personalInfo/view.ts} +24 -34
  45. package/dist/templates/examples/job-application/src/step/skills/{entry.ts → entry/entry.ts} +2 -2
  46. package/dist/templates/examples/job-application/src/step/skills/entry/index.ts +11 -0
  47. package/dist/templates/examples/job-application/src/step/skills/entry/story.test.ts +27 -0
  48. package/dist/templates/examples/job-application/src/{view/skillEntry.ts → step/skills/entry/view.ts} +9 -12
  49. package/dist/templates/examples/job-application/src/step/skills/index.ts +11 -1
  50. package/dist/templates/examples/job-application/src/step/skills/scene.test.ts +16 -0
  51. package/dist/templates/examples/job-application/src/step/skills/story.test.ts +85 -0
  52. package/dist/templates/examples/job-application/src/step/skills/view.ts +47 -0
  53. package/dist/templates/examples/job-application/src/step/workHistory/{entry.ts → entry/entry.ts} +1 -1
  54. package/dist/templates/examples/job-application/src/step/workHistory/entry/index.ts +11 -0
  55. package/dist/templates/examples/job-application/src/step/workHistory/entry/story.test.ts +42 -0
  56. package/dist/templates/examples/job-application/src/{view/workEntry.ts → step/workHistory/entry/view.ts} +27 -42
  57. package/dist/templates/examples/job-application/src/step/workHistory/index.ts +11 -1
  58. package/dist/templates/examples/job-application/src/step/workHistory/scene.test.ts +20 -0
  59. package/dist/templates/examples/job-application/src/step/workHistory/story.test.ts +87 -0
  60. package/dist/templates/examples/job-application/src/{view/workHistory.ts → step/workHistory/view.ts} +4 -8
  61. package/dist/templates/examples/job-application/src/story.test.ts +28 -419
  62. package/dist/templates/examples/job-application/src/view/datePicker.ts +4 -4
  63. package/dist/templates/examples/job-application/src/view/field.ts +3 -3
  64. package/dist/templates/examples/job-application/src/view/index.ts +3 -358
  65. package/dist/templates/examples/job-application/src/view/stepNav.ts +2 -2
  66. package/dist/templates/examples/job-application/src/view/view.ts +359 -0
  67. package/dist/templates/examples/kanban/package.json +0 -2
  68. package/dist/templates/examples/kanban/src/scene.test.ts +1 -1
  69. package/dist/templates/examples/kanban/src/story.test.ts +22 -23
  70. package/dist/templates/examples/kanban/src/view/index.ts +1 -71
  71. package/dist/templates/examples/kanban/src/view/view.ts +71 -0
  72. package/dist/templates/examples/managed-resource-layer/package.json +0 -2
  73. package/dist/templates/examples/map/package.json +0 -2
  74. package/dist/templates/examples/pixel-art/package.json +0 -2
  75. package/dist/templates/examples/pixel-art/src/scene.test.ts +16 -9
  76. package/dist/templates/examples/pixel-art/src/story.test.ts +12 -7
  77. package/dist/templates/examples/query-sync/package.json +0 -2
  78. package/dist/templates/examples/query-sync/src/story.test.ts +4 -5
  79. package/dist/templates/examples/route-transitions/package.json +0 -2
  80. package/dist/templates/examples/routing/package.json +0 -2
  81. package/dist/templates/examples/routing/src/page/people.scene.test.ts +41 -0
  82. package/dist/templates/examples/routing/src/page/people.story.test.ts +73 -0
  83. package/dist/templates/examples/routing/src/scene.test.ts +2 -17
  84. package/dist/templates/examples/routing/src/story.test.ts +4 -34
  85. package/dist/templates/examples/shopping-cart/package.json +0 -2
  86. package/dist/templates/examples/shopping-cart/src/page/products.scene.test.ts +20 -0
  87. package/dist/templates/examples/shopping-cart/src/page/products.story.test.ts +56 -0
  88. package/dist/templates/examples/shopping-cart/src/page/products.ts +1 -1
  89. package/dist/templates/examples/shopping-cart/src/scene.test.ts +1 -4
  90. package/dist/templates/examples/shopping-cart/src/story.test.ts +3 -4
  91. package/dist/templates/examples/slow-warnings/package.json +0 -2
  92. package/dist/templates/examples/snake/package.json +0 -2
  93. package/dist/templates/examples/ssg/package.json +0 -2
  94. package/dist/templates/examples/ssr/package.json +0 -2
  95. package/dist/templates/examples/state-machine/package.json +0 -2
  96. package/dist/templates/examples/state-machine/src/main.ts +79 -87
  97. package/dist/templates/examples/state-machine/src/scene.test.ts +1 -2
  98. package/dist/templates/examples/stopwatch/package.json +0 -2
  99. package/dist/templates/examples/todo/package.json +0 -2
  100. package/dist/templates/examples/ui-showcase/package.json +0 -2
  101. package/dist/templates/examples/ui-showcase/src/story.test.ts +1 -2
  102. package/dist/templates/examples/view-transitions/package.json +0 -2
  103. package/dist/templates/examples/view-transitions/src/styles.css +3 -3
  104. package/dist/templates/examples/weather/package.json +0 -2
  105. package/dist/templates/examples/web-components/package.json +0 -2
  106. package/dist/templates/examples/websocket-chat/package.json +0 -2
  107. package/dist/templates/release.json +9 -10
  108. package/dist/templates/rendering/ssg/package.json +1 -1
  109. package/dist/templates/rendering/ssr/package.json +1 -1
  110. package/dist/utils/packages.js +1 -2
  111. package/package.json +3 -3
  112. package/dist/templates/base/.prettierignore +0 -5
  113. package/dist/templates/base/.prettierrc +0 -13
  114. package/dist/templates/examples/job-application/src/step/coverLetter.ts +0 -34
  115. package/dist/templates/examples/job-application/src/view/educationEntry.ts +0 -165
  116. package/dist/templates/examples/job-application/src/view/skills.ts +0 -49
  117. /package/dist/templates/examples/job-application/src/step/{attachments.ts → attachments/attachments.ts} +0 -0
@@ -0,0 +1,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
- RadioGroupMessage.CompletedFocusOption(),
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, DialogMessage.SucceededShowDialog()),
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, DialogMessage.SucceededShowDialog()),
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(Dialog.CloseDialog, DialogMessage.CompletedCloseDialog()),
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, DialogMessage.SucceededShowDialog()),
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(Dialog.CloseDialog, DialogMessage.CompletedCloseDialog()),
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(Dialog.CloseDialog, DialogMessage.CompletedCloseDialog()),
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, DialogMessage.SucceededShowDialog()),
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(Dialog.CloseDialog, DialogMessage.CompletedCloseDialog()),
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, DialogMessage.SucceededShowDialog()),
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, DialogMessage.SucceededShowDialog()),
471
+ Command.resolve(Dialog.ShowDialog, Dialog.Message.SucceededShowDialog()),
470
472
  message(
471
473
  Message.GotErrorDialogMessage({
472
- message: DialogMessage.RequestedClose(),
474
+ message: Dialog.Message.RequestedClose(),
473
475
  }),
474
476
  ),
475
- Command.resolve(Dialog.CloseDialog, DialogMessage.CompletedCloseDialog()),
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: ListboxMessage.Opened({
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
- ListboxMessage.CompletedFocusItems(),
147
+ Listbox.Message.CompletedFocusItems(),
149
148
  ),
150
149
  message(
151
150
  Message.GotDietListboxMessage({
152
- message: ListboxMessage.SelectedItem({ item: 'Carnivore' }),
151
+ message: Listbox.Message.SelectedItem({ item: 'Carnivore' }),
153
152
  }),
154
153
  ),
155
154
  Command.resolve(
156
155
  Listbox.FocusButton,
157
- ListboxMessage.CompletedFocusButton(),
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 lists every person', () => {
83
+ test('the People route renders the People heading', () => {
84
84
  scene(
85
85
  { update, view },
86
86
  given(people('')),
87
- expect(text('Alice Johnson')).toExist(),
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
- PeopleMessage.SucceededFetchPeople({
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('typing updates the input without recording history or firing a command', () => {
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: PeopleMessage.ChangedSearchInput({ value: 'd' }),
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('designer')
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 lists every product with an Add to Cart button', () => {
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: ProductsMessage.ClickedAddToCart({ item: apple }),
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: ProductsMessage.ClickedAddToCart({ item: apple }),
109
+ message: Products.Message.ClickedAddToCart({ item: apple }),
111
110
  }),
112
111
  ),
113
112
  message(
114
113
  Message.GotProductsMessage({
115
- message: ProductsMessage.ClickedAddToCart({ item: apple }),
114
+ message: Products.Message.ClickedAddToCart({ item: apple }),
116
115
  }),
117
116
  ),
118
117
  model(model => {