create-foldkit-app 0.30.1 → 0.31.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 (59) hide show
  1. package/dist/templates/base/FOLDKIT.md +3 -1
  2. package/dist/templates/examples/auth/src/main.ts +3 -3
  3. package/dist/templates/examples/auth/src/page/loggedIn/model.ts +2 -2
  4. package/dist/templates/examples/auth/src/page/loggedOut/model.ts +2 -2
  5. package/dist/templates/examples/auth/src/route.ts +19 -29
  6. package/dist/templates/examples/auth/src/scene.test.ts +2 -2
  7. package/dist/templates/examples/auth/src/update.ts +3 -4
  8. package/dist/templates/examples/form/src/main.ts +12 -21
  9. package/dist/templates/examples/interrupting-commands/src/scene.test.ts +2 -2
  10. package/dist/templates/examples/interrupting-commands/src/story.test.ts +7 -7
  11. package/dist/templates/examples/job-application/src/main.ts +2 -2
  12. package/dist/templates/examples/job-application/src/model.ts +7 -12
  13. package/dist/templates/examples/job-application/src/scene.test.ts +5 -11
  14. package/dist/templates/examples/job-application/src/story.test.ts +4 -4
  15. package/dist/templates/examples/job-application/src/update.ts +8 -4
  16. package/dist/templates/examples/job-application/src/view/review.ts +92 -94
  17. package/dist/templates/examples/managed-resource-layer/src/main.ts +23 -27
  18. package/dist/templates/examples/managed-resource-layer/src/scene.test.ts +3 -4
  19. package/dist/templates/examples/managed-resource-layer/src/story.test.ts +11 -20
  20. package/dist/templates/examples/map/src/main.fixtures.ts +2 -2
  21. package/dist/templates/examples/map/src/main.ts +23 -21
  22. package/dist/templates/examples/map/src/scene.test.ts +4 -2
  23. package/dist/templates/examples/map/src/story.test.ts +9 -10
  24. package/dist/templates/examples/query-sync/src/main.ts +30 -29
  25. package/dist/templates/examples/query-sync/src/scene.test.ts +8 -15
  26. package/dist/templates/examples/query-sync/src/story.test.ts +21 -8
  27. package/dist/templates/examples/route-transitions/src/main.ts +15 -25
  28. package/dist/templates/examples/route-transitions/src/route.ts +21 -26
  29. package/dist/templates/examples/route-transitions/src/story.test.ts +20 -26
  30. package/dist/templates/examples/routing/src/fileTree.ts +3 -3
  31. package/dist/templates/examples/routing/src/main.ts +2 -12
  32. package/dist/templates/examples/routing/src/page/people.ts +18 -16
  33. package/dist/templates/examples/routing/src/route.ts +18 -32
  34. package/dist/templates/examples/routing/src/scene.test.ts +12 -20
  35. package/dist/templates/examples/routing/src/story.test.ts +9 -6
  36. package/dist/templates/examples/shopping-cart/src/route.ts +11 -19
  37. package/dist/templates/examples/shopping-cart/src/scene.test.ts +10 -8
  38. package/dist/templates/examples/shopping-cart/src/story.test.ts +2 -2
  39. package/dist/templates/examples/ssg/src/route.ts +9 -8
  40. package/dist/templates/examples/state-machine/src/main.ts +68 -63
  41. package/dist/templates/examples/state-machine/src/story.test.ts +8 -7
  42. package/dist/templates/examples/state-machine/src/view.ts +7 -13
  43. package/dist/templates/examples/todo/src/main.ts +11 -17
  44. package/dist/templates/examples/todo/src/scene.test.ts +2 -2
  45. package/dist/templates/examples/todo/src/story.test.ts +11 -12
  46. package/dist/templates/examples/ui-showcase/src/main.ts +64 -85
  47. package/dist/templates/examples/ui-showcase/src/scene.test.ts +14 -30
  48. package/dist/templates/examples/ui-showcase/src/story.test.ts +2 -2
  49. package/dist/templates/examples/ui-showcase/src/ui/message.ts +0 -70
  50. package/dist/templates/examples/view-transitions/src/main.ts +1 -1
  51. package/dist/templates/examples/view-transitions/src/route.ts +9 -12
  52. package/dist/templates/examples/view-transitions/src/scene.test.ts +5 -14
  53. package/dist/templates/examples/websocket-chat/src/main.ts +29 -37
  54. package/dist/templates/examples/websocket-chat/src/scene.test.ts +18 -10
  55. package/dist/templates/examples/websocket-chat/src/story.test.ts +19 -15
  56. package/dist/templates/release.json +13 -13
  57. package/dist/templates/rendering/ssg/src/route.ts +9 -8
  58. package/dist/templates/rendering/ssg/src/scene.test.ts +2 -2
  59. package/package.json +1 -1
@@ -12,7 +12,7 @@ import {
12
12
  import { Document, Html, HtmlBuilder } from 'foldkit/html'
13
13
  import { defineMessageUnion } from 'foldkit/message'
14
14
  import { UrlRequest, load, pushUrl } from 'foldkit/navigation'
15
- import { literal, r } from 'foldkit/route'
15
+ import { defineRouteUnion, literal } from 'foldkit/route'
16
16
  import { evo } from 'foldkit/struct'
17
17
  import { Url, toString as urlToString } from 'foldkit/url'
18
18
 
@@ -28,103 +28,82 @@ import * as View from './ui/view'
28
28
 
29
29
  // ROUTE
30
30
 
31
- export const HomeRoute = r('Home')
32
- export const ButtonRoute = r('Button')
33
- export const CalendarRoute = r('Calendar')
34
- export const CheckboxRoute = r('Checkbox')
35
- export const ComboboxRoute = r('Combobox')
36
- export const DatePickerRoute = r('DatePicker')
37
- export const DialogRoute = r('Dialog')
38
- export const DisclosureRoute = r('Disclosure')
39
- export const DragAndDropRoute = r('DragAndDrop')
40
- export const FieldsetRoute = r('Fieldset')
41
- export const FileDropRoute = r('FileDrop')
42
- export const InputRoute = r('Input')
43
- export const ListboxRoute = r('Listbox')
44
- export const MenuRoute = r('Menu')
45
- export const PopoverRoute = r('Popover')
46
- export const RadioGroupRoute = r('RadioGroup')
47
- export const SelectRoute = r('Select')
48
- export const SliderRoute = r('Slider')
49
- export const SwitchRoute = r('Switch')
50
- export const TabsRoute = r('Tabs')
51
- export const TextareaRoute = r('Textarea')
52
- export const ToastRoute = r('Toast')
53
- export const TooltipRoute = r('Tooltip')
54
- export const AnimationRoute = r('Animation')
55
- export const VirtualListRoute = r('VirtualList')
56
- export const NotFoundRoute = r('NotFound', { path: S.String })
57
-
58
- const AppRoute = S.Union([
59
- HomeRoute,
60
- ButtonRoute,
61
- CalendarRoute,
62
- CheckboxRoute,
63
- ComboboxRoute,
64
- DatePickerRoute,
65
- DialogRoute,
66
- DisclosureRoute,
67
- DragAndDropRoute,
68
- FieldsetRoute,
69
- FileDropRoute,
70
- InputRoute,
71
- ListboxRoute,
72
- MenuRoute,
73
- PopoverRoute,
74
- RadioGroupRoute,
75
- SelectRoute,
76
- SliderRoute,
77
- SwitchRoute,
78
- TabsRoute,
79
- TextareaRoute,
80
- ToastRoute,
81
- TooltipRoute,
82
- AnimationRoute,
83
- VirtualListRoute,
84
- NotFoundRoute,
85
- ])
86
-
87
- type AppRoute = typeof AppRoute.Type
88
-
89
- const homeRouter = pipe(Route.root, Route.mapTo(HomeRoute))
90
- const buttonRouter = pipe(literal('button'), Route.mapTo(ButtonRoute))
91
- const calendarRouter = pipe(literal('calendar'), Route.mapTo(CalendarRoute))
92
- const checkboxRouter = pipe(literal('checkbox'), Route.mapTo(CheckboxRoute))
93
- const comboboxRouter = pipe(literal('combobox'), Route.mapTo(ComboboxRoute))
31
+ export const AppRoute = defineRouteUnion({
32
+ Home: {},
33
+ Button: {},
34
+ Calendar: {},
35
+ Checkbox: {},
36
+ Combobox: {},
37
+ DatePicker: {},
38
+ Dialog: {},
39
+ Disclosure: {},
40
+ DragAndDrop: {},
41
+ Fieldset: {},
42
+ FileDrop: {},
43
+ Input: {},
44
+ Listbox: {},
45
+ Menu: {},
46
+ Popover: {},
47
+ RadioGroup: {},
48
+ Select: {},
49
+ Slider: {},
50
+ Switch: {},
51
+ Tabs: {},
52
+ Textarea: {},
53
+ Toast: {},
54
+ Tooltip: {},
55
+ Animation: {},
56
+ VirtualList: {},
57
+ NotFound: { path: S.String },
58
+ })
59
+
60
+ export type AppRoute = typeof AppRoute.Type
61
+
62
+ const homeRouter = pipe(Route.root, Route.mapTo(AppRoute.Home))
63
+ const buttonRouter = pipe(literal('button'), Route.mapTo(AppRoute.Button))
64
+ const calendarRouter = pipe(literal('calendar'), Route.mapTo(AppRoute.Calendar))
65
+ const checkboxRouter = pipe(literal('checkbox'), Route.mapTo(AppRoute.Checkbox))
66
+ const comboboxRouter = pipe(literal('combobox'), Route.mapTo(AppRoute.Combobox))
94
67
  const datePickerRouter = pipe(
95
68
  literal('date-picker'),
96
- Route.mapTo(DatePickerRoute),
69
+ Route.mapTo(AppRoute.DatePicker),
97
70
  )
98
- const dialogRouter = pipe(literal('dialog'), Route.mapTo(DialogRoute))
71
+ const dialogRouter = pipe(literal('dialog'), Route.mapTo(AppRoute.Dialog))
99
72
  const disclosureRouter = pipe(
100
73
  literal('disclosure'),
101
- Route.mapTo(DisclosureRoute),
74
+ Route.mapTo(AppRoute.Disclosure),
102
75
  )
103
76
  const dragAndDropRouter = pipe(
104
77
  literal('drag-and-drop'),
105
- Route.mapTo(DragAndDropRoute),
78
+ Route.mapTo(AppRoute.DragAndDrop),
106
79
  )
107
- const fieldsetRouter = pipe(literal('fieldset'), Route.mapTo(FieldsetRoute))
108
- const fileDropRouter = pipe(literal('file-drop'), Route.mapTo(FileDropRoute))
109
- const inputRouter = pipe(literal('input'), Route.mapTo(InputRoute))
110
- const listboxRouter = pipe(literal('listbox'), Route.mapTo(ListboxRoute))
111
- const menuRouter = pipe(literal('menu'), Route.mapTo(MenuRoute))
112
- const popoverRouter = pipe(literal('popover'), Route.mapTo(PopoverRoute))
80
+ const fieldsetRouter = pipe(literal('fieldset'), Route.mapTo(AppRoute.Fieldset))
81
+ const fileDropRouter = pipe(
82
+ literal('file-drop'),
83
+ Route.mapTo(AppRoute.FileDrop),
84
+ )
85
+ const inputRouter = pipe(literal('input'), Route.mapTo(AppRoute.Input))
86
+ const listboxRouter = pipe(literal('listbox'), Route.mapTo(AppRoute.Listbox))
87
+ const menuRouter = pipe(literal('menu'), Route.mapTo(AppRoute.Menu))
88
+ const popoverRouter = pipe(literal('popover'), Route.mapTo(AppRoute.Popover))
113
89
  const radioGroupRouter = pipe(
114
90
  literal('radio-group'),
115
- Route.mapTo(RadioGroupRoute),
91
+ Route.mapTo(AppRoute.RadioGroup),
92
+ )
93
+ const selectRouter = pipe(literal('select'), Route.mapTo(AppRoute.Select))
94
+ const sliderRouter = pipe(literal('slider'), Route.mapTo(AppRoute.Slider))
95
+ const switchRouter = pipe(literal('switch'), Route.mapTo(AppRoute.Switch))
96
+ const tabsRouter = pipe(literal('tabs'), Route.mapTo(AppRoute.Tabs))
97
+ const textareaRouter = pipe(literal('textarea'), Route.mapTo(AppRoute.Textarea))
98
+ const toastRouter = pipe(literal('toast'), Route.mapTo(AppRoute.Toast))
99
+ const tooltipRouter = pipe(literal('tooltip'), Route.mapTo(AppRoute.Tooltip))
100
+ const animationRouter = pipe(
101
+ literal('animation'),
102
+ Route.mapTo(AppRoute.Animation),
116
103
  )
117
- const selectRouter = pipe(literal('select'), Route.mapTo(SelectRoute))
118
- const sliderRouter = pipe(literal('slider'), Route.mapTo(SliderRoute))
119
- const switchRouter = pipe(literal('switch'), Route.mapTo(SwitchRoute))
120
- const tabsRouter = pipe(literal('tabs'), Route.mapTo(TabsRoute))
121
- const textareaRouter = pipe(literal('textarea'), Route.mapTo(TextareaRoute))
122
- const toastRouter = pipe(literal('toast'), Route.mapTo(ToastRoute))
123
- const tooltipRouter = pipe(literal('tooltip'), Route.mapTo(TooltipRoute))
124
- const animationRouter = pipe(literal('animation'), Route.mapTo(AnimationRoute))
125
104
  const virtualListRouter = pipe(
126
105
  literal('virtual-list'),
127
- Route.mapTo(VirtualListRoute),
106
+ Route.mapTo(AppRoute.VirtualList),
128
107
  )
129
108
 
130
109
  const routeParser = Route.oneOf(
@@ -155,7 +134,7 @@ const routeParser = Route.oneOf(
155
134
  homeRouter,
156
135
  )
157
136
 
158
- const urlToAppRoute = Route.parseUrlWithFallback(routeParser, NotFoundRoute)
137
+ const urlToAppRoute = Route.parseUrlWithFallback(routeParser, AppRoute.NotFound)
159
138
 
160
139
  // MODEL
161
140
 
@@ -2,23 +2,7 @@ import { Calendar } from 'foldkit'
2
2
  import { expect, given, role, scene, text } from 'foldkit/scene'
3
3
  import { describe, test } from 'vitest'
4
4
 
5
- import {
6
- AnimationRoute,
7
- ButtonRoute,
8
- CheckboxRoute,
9
- DisclosureRoute,
10
- FieldsetRoute,
11
- HomeRoute,
12
- InputRoute,
13
- type Model,
14
- NotFoundRoute,
15
- RadioGroupRoute,
16
- SelectRoute,
17
- SwitchRoute,
18
- TextareaRoute,
19
- update,
20
- view,
21
- } from './main'
5
+ import { AppRoute, type Model, update, view } from './main'
22
6
  import { uiInit } from './ui/init'
23
7
 
24
8
  const today = Calendar.make(2026, 4, 16)
@@ -29,7 +13,7 @@ const modelForRoute = (route: Model['route']): Model => ({
29
13
  uiModel: uiInit_.model,
30
14
  })
31
15
 
32
- const homeModel = modelForRoute(HomeRoute())
16
+ const homeModel = modelForRoute(AppRoute.Home())
33
17
 
34
18
  describe('view', () => {
35
19
  test('the sidebar nav lists a sample of every component link', () => {
@@ -59,16 +43,16 @@ describe('view', () => {
59
43
 
60
44
  test('simple component routes render the sidebar nav', () => {
61
45
  const routes: ReadonlyArray<Model['route']> = [
62
- ButtonRoute(),
63
- CheckboxRoute(),
64
- DisclosureRoute(),
65
- FieldsetRoute(),
66
- InputRoute(),
67
- RadioGroupRoute(),
68
- SelectRoute(),
69
- SwitchRoute(),
70
- TextareaRoute(),
71
- AnimationRoute(),
46
+ AppRoute.Button(),
47
+ AppRoute.Checkbox(),
48
+ AppRoute.Disclosure(),
49
+ AppRoute.Fieldset(),
50
+ AppRoute.Input(),
51
+ AppRoute.RadioGroup(),
52
+ AppRoute.Select(),
53
+ AppRoute.Switch(),
54
+ AppRoute.Textarea(),
55
+ AppRoute.Animation(),
72
56
  ]
73
57
 
74
58
  routes.forEach(route => {
@@ -83,7 +67,7 @@ describe('view', () => {
83
67
  test('the Disclosure panel stays mounted while collapsed so it can animate', () => {
84
68
  scene(
85
69
  { update, view },
86
- given(modelForRoute(DisclosureRoute())),
70
+ given(modelForRoute(AppRoute.Disclosure())),
87
71
  expect(
88
72
  text('Foldkit is an Elm-inspired UI framework', { exact: false }),
89
73
  ).toExist(),
@@ -93,7 +77,7 @@ describe('view', () => {
93
77
  test('the NotFound route renders the 404 panel and a Go Home link', () => {
94
78
  scene(
95
79
  { update, view },
96
- given(modelForRoute(NotFoundRoute({ path: '/oops' }))),
80
+ given(modelForRoute(AppRoute.NotFound({ path: '/oops' }))),
97
81
  expect(role('heading', { name: '404 — Page Not Found' })).toExist(),
98
82
  expect(text('The path "/oops" was not found.')).toExist(),
99
83
  expect(role('link', { name: 'Go Home' })).toExist(),
@@ -7,14 +7,14 @@ import { describe, expect, test } from 'vitest'
7
7
  import { Dialog } from '@foldkit/ui'
8
8
  import { Message as DialogMessage } from '@foldkit/ui/dialog'
9
9
 
10
- import { HomeRoute, Message, type Model, update } from './main'
10
+ import { AppRoute, Message, type Model, update } from './main'
11
11
  import { uiInit } from './ui/init'
12
12
 
13
13
  const today = Calendar.make(2026, 4, 16)
14
14
  const uiInit_ = uiInit(today)
15
15
 
16
16
  const initialModel: Model = {
17
- route: HomeRoute(),
17
+ route: AppRoute.Home(),
18
18
  uiModel: uiInit_.model,
19
19
  }
20
20
 
@@ -91,74 +91,4 @@ export const UiMessage = defineMessageUnion({
91
91
  ClickedVirtualListVariableScrollToMiddle: {},
92
92
  })
93
93
 
94
- export const {
95
- GotMobileMenuDialogMessage,
96
- ClickedOpenMobileMenu,
97
- ClickedButtonDemo,
98
- GotAnimationDemoMessage,
99
- ToggledAnimationDemo,
100
- UpdatedInputDemoValue,
101
- UpdatedTextareaDemoValue,
102
- UpdatedFieldsetInputValue,
103
- UpdatedFieldsetTextareaValue,
104
- ToggledFieldsetCheckboxDemo,
105
- ToggledCheckboxBasicDemo,
106
- ToggledCheckboxAllDemo,
107
- ToggledCheckboxOptionADemo,
108
- ToggledCheckboxOptionBDemo,
109
- GotComboboxDemoMessage,
110
- GotComboboxNullableDemoMessage,
111
- GotComboboxMultiDemoMessage,
112
- GotComboboxPlacementLockDemoMessage,
113
- GotComboboxSelectOnFocusDemoMessage,
114
- GotDialogDemoMessage,
115
- GotDialogAnimatedDemoMessage,
116
- GotOverlayDialogDemoMessage,
117
- GotOverlayComboboxDemoMessage,
118
- GotNestedDialogParentDemoMessage,
119
- GotNestedDialogChildDemoMessage,
120
- ClickedDeleteProject,
121
- ClickedOpenDialog,
122
- ClickedOpenAnimatedDialog,
123
- ClickedEditFilters,
124
- ClickedOpenProjectSettings,
125
- ToggledDisclosureBasicDemo,
126
- ToggledDisclosureAnimatedDemo,
127
- GotCalendarBasicDemoMessage,
128
- GotDatePickerBasicDemoMessage,
129
- GotDragAndDropDemoMessage,
130
- GotFileDropBasicDemoMessage,
131
- ClickedRemoveFileDropDemoFile,
132
- GotListboxDemoMessage,
133
- GotListboxMultiDemoMessage,
134
- GotListboxGroupedDemoMessage,
135
- GotMenuBasicDemoMessage,
136
- GotMenuAnimatedDemoMessage,
137
- GotPopoverBasicDemoMessage,
138
- GotPopoverAnimatedDemoMessage,
139
- GotPopoverNestedParentDemoMessage,
140
- GotPopoverNestedChildDemoMessage,
141
- GotVerticalRadioGroupDemoMessage,
142
- GotHorizontalRadioGroupDemoMessage,
143
- UpdatedSelectDemoValue,
144
- GotSliderRatingDemoMessage,
145
- GotSliderVolumeDemoMessage,
146
- ToggledSwitchDemo,
147
- GotHorizontalTabsDemoMessage,
148
- GotVerticalTabsDemoMessage,
149
- GotToastDemoMessage,
150
- ClickedShowInfoToast,
151
- ClickedShowSuccessToast,
152
- ClickedShowWarningToast,
153
- ClickedShowErrorToast,
154
- ClickedShowStickyToast,
155
- ClickedDismissAllToasts,
156
- GotTooltipBasicDemoMessage,
157
- GotTooltipNoDelayDemoMessage,
158
- GotVirtualListDemoMessage,
159
- ClickedVirtualListScrollToMiddle,
160
- GotVirtualListVariableDemoMessage,
161
- ClickedVirtualListVariableScrollToMiddle,
162
- } = UiMessage
163
-
164
94
  export type UiMessage = typeof UiMessage.Type
@@ -10,7 +10,7 @@ import { Url, toString as urlToString } from 'foldkit/url'
10
10
  import { Artwork, artworks, findArtwork } from './artwork'
11
11
  import { AppRoute, artworkRouter, galleryRouter, urlToAppRoute } from './route'
12
12
 
13
- export { ArtworkRoute, GalleryRoute, NotFoundRoute } from './route'
13
+ export { AppRoute } from './route'
14
14
 
15
15
  // MODEL
16
16
 
@@ -1,29 +1,26 @@
1
1
  import { Schema as S, pipe } from 'effect'
2
2
  import { Route } from 'foldkit'
3
- import { int, literal, r, slash } from 'foldkit/route'
3
+ import { defineRouteUnion, int, literal, slash } from 'foldkit/route'
4
4
 
5
- export const GalleryRoute = r('Gallery')
6
- export const ArtworkRoute = r('Artwork', { artworkId: S.Number })
7
- export const NotFoundRoute = r('NotFound', { path: S.String })
5
+ export const AppRoute = defineRouteUnion({
6
+ Gallery: {},
7
+ Artwork: { artworkId: S.Number },
8
+ NotFound: { path: S.String },
9
+ })
8
10
 
9
- export const AppRoute = S.Union([GalleryRoute, ArtworkRoute, NotFoundRoute])
10
-
11
- export type GalleryRoute = typeof GalleryRoute.Type
12
- export type ArtworkRoute = typeof ArtworkRoute.Type
13
- export type NotFoundRoute = typeof NotFoundRoute.Type
14
11
  export type AppRoute = typeof AppRoute.Type
15
12
 
16
- export const galleryRouter = pipe(Route.root, Route.mapTo(GalleryRoute))
13
+ export const galleryRouter = pipe(Route.root, Route.mapTo(AppRoute.Gallery))
17
14
 
18
15
  export const artworkRouter = pipe(
19
16
  literal('artwork'),
20
17
  slash(int('artworkId')),
21
- Route.mapTo(ArtworkRoute),
18
+ Route.mapTo(AppRoute.Artwork),
22
19
  )
23
20
 
24
21
  const routeParser = Route.oneOf(artworkRouter, galleryRouter)
25
22
 
26
23
  export const urlToAppRoute = Route.parseUrlWithFallback(
27
24
  routeParser,
28
- NotFoundRoute,
25
+ AppRoute.NotFound,
29
26
  )
@@ -3,22 +3,13 @@ import { Scene } from 'foldkit'
3
3
  import { fromString } from 'foldkit/url'
4
4
  import { describe, expect, test } from 'vitest'
5
5
 
6
- import {
7
- ArtworkRoute,
8
- GalleryRoute,
9
- Message,
10
- Model,
11
- NotFoundRoute,
12
- update,
13
- view,
14
- viewTransition,
15
- } from './main'
16
-
17
- const gallery = Model.make({ route: GalleryRoute(), filterText: '' })
6
+ import { AppRoute, Message, Model, update, view, viewTransition } from './main'
7
+
8
+ const gallery = Model.make({ route: AppRoute.Gallery(), filterText: '' })
18
9
  const artworkDetail = (artworkId: number) =>
19
- Model.make({ route: ArtworkRoute({ artworkId }), filterText: '' })
10
+ Model.make({ route: AppRoute.Artwork({ artworkId }), filterText: '' })
20
11
  const notFound = Model.make({
21
- route: NotFoundRoute({ path: '/nowhere' }),
12
+ route: AppRoute.NotFound({ path: '/nowhere' }),
22
13
  filterText: '',
23
14
  })
24
15
 
@@ -19,7 +19,7 @@ import {
19
19
  } from 'foldkit'
20
20
  import { Document, Html, HtmlBuilder } from 'foldkit/html'
21
21
  import { defineMessageUnion } from 'foldkit/message'
22
- import { ts } from 'foldkit/schema'
22
+ import { defineTaggedUnion } from 'foldkit/schema'
23
23
  import { evo } from 'foldkit/struct'
24
24
 
25
25
  import { Button, Input } from '@foldkit/ui'
@@ -44,18 +44,13 @@ type ChatMessage = typeof ChatMessage.Type
44
44
  const ChatSocket = ManagedResource.tag<WebSocket>()('ChatSocket')
45
45
  type ChatSocketService = ManagedResource.ServiceOf<typeof ChatSocket>
46
46
 
47
- export const ConnectionDisconnected = ts('ConnectionDisconnected')
48
- export const ConnectionConnecting = ts('ConnectionConnecting')
49
- export const ConnectionConnected = ts('ConnectionConnected')
50
- export const ConnectionError = ts('ConnectionError', { error: S.String })
51
-
52
- const ConnectionState = S.Union([
53
- ConnectionDisconnected,
54
- ConnectionConnecting,
55
- ConnectionConnected,
56
- ConnectionError,
57
- ])
58
- type ConnectionState = typeof ConnectionState.Type
47
+ export const ConnectionState = defineTaggedUnion({
48
+ Disconnected: {},
49
+ Connecting: {},
50
+ Connected: {},
51
+ Error: { error: S.String },
52
+ })
53
+ export type ConnectionState = typeof ConnectionState.Type
59
54
 
60
55
  export const Model = S.Struct({
61
56
  connection: ConnectionState,
@@ -93,26 +88,26 @@ export const update = (model: Model, message: Message) =>
93
88
  Message.match<UpdateReturn>(message, {
94
89
  ClickedConnect: () => ({
95
90
  model: evo(model, {
96
- connection: () => ConnectionConnecting(),
91
+ connection: () => ConnectionState.Connecting(),
97
92
  }),
98
93
  }),
99
94
 
100
95
  Connected: () => ({
101
96
  model: evo(model, {
102
- connection: () => ConnectionConnected(),
97
+ connection: () => ConnectionState.Connected(),
103
98
  }),
104
99
  }),
105
100
 
106
101
  Disconnected: () => ({
107
102
  model: evo(model, {
108
- connection: () => ConnectionDisconnected(),
103
+ connection: () => ConnectionState.Disconnected(),
109
104
  messages: () => [],
110
105
  }),
111
106
  }),
112
107
 
113
108
  FailedConnect: ({ error }) => ({
114
109
  model: evo(model, {
115
- connection: () => ConnectionError({ error }),
110
+ connection: () => ConnectionState.Error({ error }),
116
111
  }),
117
112
  }),
118
113
 
@@ -131,7 +126,7 @@ export const update = (model: Model, message: Message) =>
131
126
 
132
127
  return M.value(model.connection).pipe(
133
128
  M.withReturnType<UpdateReturn>(),
134
- M.tag('ConnectionConnected', () => ({
129
+ M.tag('Connected', () => ({
135
130
  model: evo(model, {
136
131
  messageInput: () => '',
137
132
  }),
@@ -166,7 +161,7 @@ export const update = (model: Model, message: Message) =>
166
161
 
167
162
  export const init: Runtime.ApplicationInit<Model, Message> = () => ({
168
163
  model: {
169
- connection: ConnectionDisconnected(),
164
+ connection: ConnectionState.Disconnected(),
170
165
  messages: [],
171
166
  messageInput: '',
172
167
  },
@@ -224,8 +219,8 @@ export const managedResources = ManagedResource.make<Model, Message>()(
224
219
  resource: ChatSocket,
225
220
  modelToMaybeRequirements: model =>
226
221
  M.value(model.connection).pipe(
227
- M.tag('ConnectionConnecting', () => Option.some(null)),
228
- M.tag('ConnectionConnected', () => Option.some(null)),
222
+ M.tag('Connecting', () => Option.some(null)),
223
+ M.tag('Connected', () => Option.some(null)),
229
224
  M.orElse(() => Option.none()),
230
225
  ),
231
226
  acquire: () =>
@@ -321,7 +316,7 @@ export const subscriptions = Subscription.make<
321
316
  { isConnected: S.Boolean },
322
317
  {
323
318
  modelToDependencies: model => ({
324
- isConnected: model.connection._tag === 'ConnectionConnected',
319
+ isConnected: model.connection._tag === 'Connected',
325
320
  }),
326
321
  dependenciesToStream: ({ isConnected }) =>
327
322
  Stream.when(
@@ -385,11 +380,10 @@ export const view = (model: Model, h: HtmlBuilder<Message>): Document => ({
385
380
 
386
381
  M.value(model.connection).pipe(
387
382
  M.tagsExhaustive({
388
- ConnectionDisconnected: () => connectButtonView(h),
389
- ConnectionConnecting: () => connectingView(h),
390
- ConnectionConnected: () =>
391
- messageInputView(model.messageInput, h),
392
- ConnectionError: ({ error }) => errorView(error, h),
383
+ Disconnected: () => connectButtonView(h),
384
+ Connecting: () => connectingView(h),
385
+ Connected: () => messageInputView(model.messageInput, h),
386
+ Error: ({ error }) => errorView(error, h),
393
387
  }),
394
388
  ),
395
389
  ],
@@ -407,28 +401,26 @@ const connectionStatusView = (
407
401
  [
408
402
  M.value(connection).pipe(
409
403
  M.tagsExhaustive({
410
- ConnectionDisconnected: () =>
404
+ Disconnected: () =>
411
405
  h.div([h.Class('w-3 h-3 rounded-full bg-red-500')]),
412
- ConnectionConnecting: () =>
406
+ Connecting: () =>
413
407
  h.div([
414
408
  h.Class('w-3 h-3 rounded-full bg-yellow-500 animate-pulse'),
415
409
  ]),
416
- ConnectionConnected: () =>
410
+ Connected: () =>
417
411
  h.div([h.Class('w-3 h-3 rounded-full bg-green-500')]),
418
- ConnectionError: () =>
419
- h.div([h.Class('w-3 h-3 rounded-full bg-red-500')]),
412
+ Error: () => h.div([h.Class('w-3 h-3 rounded-full bg-red-500')]),
420
413
  }),
421
414
  ),
422
415
  M.value(connection).pipe(
423
416
  M.tagsExhaustive({
424
- ConnectionDisconnected: () =>
417
+ Disconnected: () =>
425
418
  h.span([h.Class('text-sm text-gray-600')], ['Disconnected']),
426
- ConnectionConnecting: () =>
419
+ Connecting: () =>
427
420
  h.span([h.Class('text-sm text-gray-600')], ['Connecting...']),
428
- ConnectionConnected: () =>
421
+ Connected: () =>
429
422
  h.span([h.Class('text-sm text-gray-600')], ['Connected']),
430
- ConnectionError: () =>
431
- h.span([h.Class('text-sm text-red-600')], ['Error']),
423
+ Error: () => h.span([h.Class('text-sm text-red-600')], ['Error']),
432
424
  }),
433
425
  ),
434
426
  ],
@@ -15,10 +15,7 @@ import {
15
15
  import { describe, test } from 'vitest'
16
16
 
17
17
  import {
18
- ConnectionConnected,
19
- ConnectionConnecting,
20
- ConnectionDisconnected,
21
- ConnectionError,
18
+ ConnectionState,
22
19
  Message,
23
20
  Model,
24
21
  SendMessage,
@@ -30,7 +27,7 @@ import {
30
27
  } from './main'
31
28
 
32
29
  const idleModel = Model.make({
33
- connection: ConnectionDisconnected(),
30
+ connection: ConnectionState.Disconnected(),
34
31
  messages: [],
35
32
  messageInput: '',
36
33
  })
@@ -53,7 +50,10 @@ describe('view', () => {
53
50
  test('connecting state renders the Connecting message', () => {
54
51
  scene(
55
52
  { update, view },
56
- given({ ...idleModel, connection: ConnectionConnecting() }),
53
+ given({
54
+ ...idleModel,
55
+ connection: ConnectionState.Connecting(),
56
+ }),
57
57
  expect(text('Connecting...')).toExist(),
58
58
  )
59
59
  })
@@ -61,7 +61,10 @@ describe('view', () => {
61
61
  test('connected state shows the message input and Send button', () => {
62
62
  scene(
63
63
  { update, view },
64
- given({ ...idleModel, connection: ConnectionConnected() }),
64
+ given({
65
+ ...idleModel,
66
+ connection: ConnectionState.Connected(),
67
+ }),
65
68
  expect(placeholder('Type a message...')).toExist(),
66
69
  expect(role('button', { name: 'Send' })).toBeDisabled(),
67
70
  type(placeholder('Type a message...'), 'hi'),
@@ -74,7 +77,9 @@ describe('view', () => {
74
77
  { update, view },
75
78
  given({
76
79
  ...idleModel,
77
- connection: ConnectionError({ error: 'Connection refused' }),
80
+ connection: ConnectionState.Error({
81
+ error: 'Connection refused',
82
+ }),
78
83
  }),
79
84
  expect(text('Connection Error')).toExist(),
80
85
  expect(text('Connection refused')).toExist(),
@@ -87,7 +92,7 @@ describe('view', () => {
87
92
  { update, view },
88
93
  given({
89
94
  ...idleModel,
90
- connection: ConnectionConnected(),
95
+ connection: ConnectionState.Connected(),
91
96
  messages: [
92
97
  { text: 'Hello there', zoned: zonedAt(0), isSent: true },
93
98
  { text: 'General Kenobi', zoned: zonedAt(0), isSent: false },
@@ -146,7 +151,10 @@ describe('view', () => {
146
151
  test('a message arriving on the socket Subscription lands in the conversation', () => {
147
152
  scene(
148
153
  { update, view },
149
- given({ ...idleModel, connection: ConnectionConnected() }),
154
+ given({
155
+ ...idleModel,
156
+ connection: ConnectionState.Connected(),
157
+ }),
150
158
  Subscription.emit(Message.ReceivedMessage({ text: 'hello from echo' })),
151
159
  Command.expectExact(
152
160
  TimestampReceivedMessage({ text: 'hello from echo' }),