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.
- package/dist/templates/base/FOLDKIT.md +3 -1
- package/dist/templates/examples/auth/src/main.ts +3 -3
- package/dist/templates/examples/auth/src/page/loggedIn/model.ts +2 -2
- package/dist/templates/examples/auth/src/page/loggedOut/model.ts +2 -2
- package/dist/templates/examples/auth/src/route.ts +19 -29
- package/dist/templates/examples/auth/src/scene.test.ts +2 -2
- package/dist/templates/examples/auth/src/update.ts +3 -4
- package/dist/templates/examples/form/src/main.ts +12 -21
- package/dist/templates/examples/interrupting-commands/src/scene.test.ts +2 -2
- package/dist/templates/examples/interrupting-commands/src/story.test.ts +7 -7
- package/dist/templates/examples/job-application/src/main.ts +2 -2
- package/dist/templates/examples/job-application/src/model.ts +7 -12
- package/dist/templates/examples/job-application/src/scene.test.ts +5 -11
- package/dist/templates/examples/job-application/src/story.test.ts +4 -4
- package/dist/templates/examples/job-application/src/update.ts +8 -4
- package/dist/templates/examples/job-application/src/view/review.ts +92 -94
- package/dist/templates/examples/managed-resource-layer/src/main.ts +23 -27
- package/dist/templates/examples/managed-resource-layer/src/scene.test.ts +3 -4
- package/dist/templates/examples/managed-resource-layer/src/story.test.ts +11 -20
- package/dist/templates/examples/map/src/main.fixtures.ts +2 -2
- package/dist/templates/examples/map/src/main.ts +23 -21
- package/dist/templates/examples/map/src/scene.test.ts +4 -2
- package/dist/templates/examples/map/src/story.test.ts +9 -10
- package/dist/templates/examples/query-sync/src/main.ts +30 -29
- package/dist/templates/examples/query-sync/src/scene.test.ts +8 -15
- package/dist/templates/examples/query-sync/src/story.test.ts +21 -8
- package/dist/templates/examples/route-transitions/src/main.ts +15 -25
- package/dist/templates/examples/route-transitions/src/route.ts +21 -26
- package/dist/templates/examples/route-transitions/src/story.test.ts +20 -26
- package/dist/templates/examples/routing/src/fileTree.ts +3 -3
- package/dist/templates/examples/routing/src/main.ts +2 -12
- package/dist/templates/examples/routing/src/page/people.ts +18 -16
- package/dist/templates/examples/routing/src/route.ts +18 -32
- package/dist/templates/examples/routing/src/scene.test.ts +12 -20
- package/dist/templates/examples/routing/src/story.test.ts +9 -6
- package/dist/templates/examples/shopping-cart/src/route.ts +11 -19
- package/dist/templates/examples/shopping-cart/src/scene.test.ts +10 -8
- package/dist/templates/examples/shopping-cart/src/story.test.ts +2 -2
- package/dist/templates/examples/ssg/src/route.ts +9 -8
- package/dist/templates/examples/state-machine/src/main.ts +68 -63
- package/dist/templates/examples/state-machine/src/story.test.ts +8 -7
- package/dist/templates/examples/state-machine/src/view.ts +7 -13
- package/dist/templates/examples/todo/src/main.ts +11 -17
- package/dist/templates/examples/todo/src/scene.test.ts +2 -2
- package/dist/templates/examples/todo/src/story.test.ts +11 -12
- package/dist/templates/examples/ui-showcase/src/main.ts +64 -85
- package/dist/templates/examples/ui-showcase/src/scene.test.ts +14 -30
- package/dist/templates/examples/ui-showcase/src/story.test.ts +2 -2
- package/dist/templates/examples/ui-showcase/src/ui/message.ts +0 -70
- package/dist/templates/examples/view-transitions/src/main.ts +1 -1
- package/dist/templates/examples/view-transitions/src/route.ts +9 -12
- package/dist/templates/examples/view-transitions/src/scene.test.ts +5 -14
- package/dist/templates/examples/websocket-chat/src/main.ts +29 -37
- package/dist/templates/examples/websocket-chat/src/scene.test.ts +18 -10
- package/dist/templates/examples/websocket-chat/src/story.test.ts +19 -15
- package/dist/templates/release.json +13 -13
- package/dist/templates/rendering/ssg/src/route.ts +9 -8
- package/dist/templates/rendering/ssg/src/scene.test.ts +2 -2
- 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 {
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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(
|
|
69
|
+
Route.mapTo(AppRoute.DatePicker),
|
|
97
70
|
)
|
|
98
|
-
const dialogRouter = pipe(literal('dialog'), Route.mapTo(
|
|
71
|
+
const dialogRouter = pipe(literal('dialog'), Route.mapTo(AppRoute.Dialog))
|
|
99
72
|
const disclosureRouter = pipe(
|
|
100
73
|
literal('disclosure'),
|
|
101
|
-
Route.mapTo(
|
|
74
|
+
Route.mapTo(AppRoute.Disclosure),
|
|
102
75
|
)
|
|
103
76
|
const dragAndDropRouter = pipe(
|
|
104
77
|
literal('drag-and-drop'),
|
|
105
|
-
Route.mapTo(
|
|
78
|
+
Route.mapTo(AppRoute.DragAndDrop),
|
|
106
79
|
)
|
|
107
|
-
const fieldsetRouter = pipe(literal('fieldset'), Route.mapTo(
|
|
108
|
-
const fileDropRouter = pipe(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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(
|
|
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(
|
|
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 {
|
|
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:
|
|
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 {
|
|
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,
|
|
3
|
+
import { defineRouteUnion, int, literal, slash } from 'foldkit/route'
|
|
4
4
|
|
|
5
|
-
export const
|
|
6
|
-
|
|
7
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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:
|
|
10
|
+
Model.make({ route: AppRoute.Artwork({ artworkId }), filterText: '' })
|
|
20
11
|
const notFound = Model.make({
|
|
21
|
-
route:
|
|
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 {
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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: () =>
|
|
91
|
+
connection: () => ConnectionState.Connecting(),
|
|
97
92
|
}),
|
|
98
93
|
}),
|
|
99
94
|
|
|
100
95
|
Connected: () => ({
|
|
101
96
|
model: evo(model, {
|
|
102
|
-
connection: () =>
|
|
97
|
+
connection: () => ConnectionState.Connected(),
|
|
103
98
|
}),
|
|
104
99
|
}),
|
|
105
100
|
|
|
106
101
|
Disconnected: () => ({
|
|
107
102
|
model: evo(model, {
|
|
108
|
-
connection: () =>
|
|
103
|
+
connection: () => ConnectionState.Disconnected(),
|
|
109
104
|
messages: () => [],
|
|
110
105
|
}),
|
|
111
106
|
}),
|
|
112
107
|
|
|
113
108
|
FailedConnect: ({ error }) => ({
|
|
114
109
|
model: evo(model, {
|
|
115
|
-
connection: () =>
|
|
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('
|
|
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:
|
|
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('
|
|
228
|
-
M.tag('
|
|
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 === '
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
404
|
+
Disconnected: () =>
|
|
411
405
|
h.div([h.Class('w-3 h-3 rounded-full bg-red-500')]),
|
|
412
|
-
|
|
406
|
+
Connecting: () =>
|
|
413
407
|
h.div([
|
|
414
408
|
h.Class('w-3 h-3 rounded-full bg-yellow-500 animate-pulse'),
|
|
415
409
|
]),
|
|
416
|
-
|
|
410
|
+
Connected: () =>
|
|
417
411
|
h.div([h.Class('w-3 h-3 rounded-full bg-green-500')]),
|
|
418
|
-
|
|
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
|
-
|
|
417
|
+
Disconnected: () =>
|
|
425
418
|
h.span([h.Class('text-sm text-gray-600')], ['Disconnected']),
|
|
426
|
-
|
|
419
|
+
Connecting: () =>
|
|
427
420
|
h.span([h.Class('text-sm text-gray-600')], ['Connecting...']),
|
|
428
|
-
|
|
421
|
+
Connected: () =>
|
|
429
422
|
h.span([h.Class('text-sm text-gray-600')], ['Connected']),
|
|
430
|
-
|
|
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
|
-
|
|
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:
|
|
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({
|
|
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({
|
|
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:
|
|
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:
|
|
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({
|
|
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' }),
|