create-foldkit-app 0.35.0 → 0.35.1

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 (24) hide show
  1. package/dist/templates/base/FOLDKIT.md +3 -1
  2. package/dist/templates/examples/generative-art/src/subscription.ts +1 -1
  3. package/dist/templates/examples/job-application/src/message.ts +0 -12
  4. package/dist/templates/examples/job-application/src/story.test.ts +0 -11
  5. package/dist/templates/examples/job-application/src/update.ts +0 -4
  6. package/dist/templates/examples/pixel-art/src/scene.test.ts +27 -7
  7. package/dist/templates/examples/pixel-art/src/story.test.ts +2 -3
  8. package/dist/templates/examples/pixel-art/src/subscription.ts +2 -2
  9. package/dist/templates/examples/routing/src/entry.ts +2 -1
  10. package/dist/templates/examples/routing/src/main.ts +56 -9
  11. package/dist/templates/examples/routing/src/story.test.ts +53 -1
  12. package/dist/templates/examples/slow-warnings/src/main.ts +4 -5
  13. package/dist/templates/examples/snake/src/main.ts +1 -1
  14. package/dist/templates/examples/ui-showcase/src/main.ts +4 -11
  15. package/dist/templates/examples/ui-showcase/src/scene.test.ts +13 -1
  16. package/dist/templates/examples/ui-showcase/src/story.test.ts +2 -3
  17. package/dist/templates/examples/ui-showcase/src/ui/init.ts +1 -0
  18. package/dist/templates/examples/ui-showcase/src/ui/message.ts +1 -0
  19. package/dist/templates/examples/ui-showcase/src/ui/model.ts +1 -0
  20. package/dist/templates/examples/ui-showcase/src/ui/subscriptions.ts +1 -1
  21. package/dist/templates/examples/ui-showcase/src/ui/update.ts +6 -0
  22. package/dist/templates/examples/ui-showcase/src/ui/view/disclosure.ts +110 -0
  23. package/dist/templates/release.json +13 -13
  24. package/package.json +1 -1
@@ -76,7 +76,7 @@ Pass optional Commands directly to APIs that accept them: `Command.mapMessages(h
76
76
 
77
77
  Use `Update.Return<Model, Message>` when an update cannot emit an OutMessage. It prevents a result containing an OutMessage from entering code that would keep only its Model and Commands. A result with no `outMessage` can still be used where `Update.ReturnWithOutMessage<Model, Message, OutMessage>` is expected. The missing field means that update emitted no OutMessage.
78
78
 
79
- Manual unpacking of a child result usually means the site should use `Update.foldChild` or `Update.foldChildStep`.
79
+ Use `Update.foldChildInit` for one child `init` or `boot` result. Use `Update.foldChildInits` when several child results enter one parent Model. Both lift child Commands and handle child OutMessages. Use `Update.foldChild` for a child update that receives input, or `Update.foldChildStep` for a child helper that receives only its Model. Keep route-gated initialization or Model-only child construction separate when there is no shared set of child results to fold.
80
80
 
81
81
  Use `Update.combine` when a later Step should receive the Model produced by an earlier Step. It takes two or more Steps. Do not wrap one Step in `Update.combine`; call that operation directly. Name an inline Step parameter `stepModel`; it contains the Model produced by the preceding Step.
82
82
 
@@ -84,6 +84,8 @@ When the OutMessage is already known while constructing a new result, include it
84
84
 
85
85
  Add `toParentOutMessage` only when at least one child OutMessage should continue to the current Submodel's parent. For partial forwarding, match every child variant and return `undefined` for the variants that stop here. Omit `toParentOutMessage` when every variant stops here. `foldOutMessage` still handles each variant locally, including variants that continue upward. Never write `toParentOutMessage: () => undefined`.
86
86
 
87
+ When a `foldChildInits` entry can derive or forward an OutMessage, add `resolveOutMessage` to construct one parent OutMessage from the named OutMessages after every local fold completes. Combine their information when both results matter; choosing one discards the other. The callback also receives the final Model. If that Model alone contains everything needed, use local folds and attach a parent OutMessage afterward with `Update.withOutMessage`.
88
+
87
89
  Use `evo()` from `foldkit/struct` for immutable model updates. Never spread or `Object.assign`.
88
90
 
89
91
  ### View
@@ -28,7 +28,7 @@ const frameSubscription = Subscription.make<Model, Message>()(_entry => ({
28
28
  }),
29
29
  }))
30
30
 
31
- export const subscriptions = Subscription.aggregate<Model, Message>()(
31
+ export const subscriptions = Subscription.aggregate(
32
32
  frameSubscription,
33
33
  flowStrengthSliderSubscriptions,
34
34
  noiseScaleSliderSubscriptions,
@@ -3,7 +3,6 @@ import { defineMessageUnion } from 'foldkit/message'
3
3
 
4
4
  import { Menu, Tabs } from '@foldkit/ui'
5
5
 
6
- import { Step } from './domain'
7
6
  import {
8
7
  Attachments,
9
8
  CoverLetter,
@@ -13,16 +12,6 @@ import {
13
12
  WorkHistory,
14
13
  } from './step'
15
14
 
16
- // STEP SUBMODELS
17
-
18
- // NAVIGATION
19
-
20
- // PREVIEW
21
-
22
- // SUBMISSION
23
-
24
- // UNION
25
-
26
15
  export const Message = defineMessageUnion({
27
16
  GotPersonalInfoMessage: { message: PersonalInfo.Message },
28
17
  GotWorkHistoryMessage: { message: WorkHistory.Message },
@@ -32,7 +21,6 @@ export const Message = defineMessageUnion({
32
21
  GotAttachmentsMessage: { message: Attachments.Message },
33
22
  GotStepMenuMessage: { message: Menu.Message },
34
23
  GotStepTabsMessage: { message: Tabs.Message },
35
- NavigatedToStep: { step: Step.Step },
36
24
  ClickedNext: {},
37
25
  ClickedPrevious: {},
38
26
  ToggledPreview: {},
@@ -79,17 +79,6 @@ describe('update', () => {
79
79
  )
80
80
  })
81
81
 
82
- test('NavigatedToStep jumps directly to a step', () => {
83
- story(
84
- update,
85
- givenInitial,
86
- message(Message.NavigatedToStep({ step: 'Skills' })),
87
- model(model => {
88
- expect(model.currentStep).toBe('Skills')
89
- }),
90
- )
91
- })
92
-
93
82
  test('GotStepTabsMessage selects the matching step', () => {
94
83
  story(
95
84
  update,
@@ -139,10 +139,6 @@ export const update = (model: Model, message: Message) =>
139
139
 
140
140
  GotStepTabsMessage: ({ message }) => foldStepTabs(model, message),
141
141
 
142
- NavigatedToStep: ({ step }) => ({
143
- model: evo(model, { currentStep: () => step }),
144
- }),
145
-
146
142
  ClickedNext: () => ({ model: evo(model, { currentStep: toNextStep }) }),
147
143
 
148
144
  ClickedPrevious: () => ({
@@ -1,5 +1,14 @@
1
1
  import { Option } from 'effect'
2
- import { Command, click, expect, given, role, scene, text } from 'foldkit/scene'
2
+ import {
3
+ Command,
4
+ Mount,
5
+ click,
6
+ expect,
7
+ given,
8
+ role,
9
+ scene,
10
+ text,
11
+ } from 'foldkit/scene'
3
12
  import { evo } from 'foldkit/struct'
4
13
  import { describe, test } from 'vitest'
5
14
 
@@ -17,6 +26,11 @@ const resolveFocusOption = Command.resolve(
17
26
  RadioGroup.Message.CompletedFocusOption(),
18
27
  )
19
28
 
29
+ const resolveDialogResources = Mount.resolve(
30
+ Dialog.AcquireResources,
31
+ Dialog.Message.SucceededAcquireResources(),
32
+ )
33
+
20
34
  const createTestModel = (): Model => ({
21
35
  grid: createEmptyGrid(4),
22
36
  undoStack: [],
@@ -70,6 +84,7 @@ describe('export workflow', () => {
70
84
  Message.FailedExportPng({ error: 'Canvas 2D context not available' }),
71
85
  ),
72
86
  Command.resolve(Dialog.ShowDialog, Dialog.Message.SucceededShowDialog()),
87
+ resolveDialogResources,
73
88
  expect(text('Export Failed')).toExist(),
74
89
  expect(text('Canvas 2D context not available')).toExist(),
75
90
  expect(role('button', { name: 'Dismiss' })).toExist(),
@@ -86,12 +101,14 @@ describe('export workflow', () => {
86
101
  Message.FailedExportPng({ error: 'Canvas 2D context not available' }),
87
102
  ),
88
103
  Command.resolve(Dialog.ShowDialog, Dialog.Message.SucceededShowDialog()),
104
+ resolveDialogResources,
89
105
  expect(text('Export Failed')).toExist(),
90
106
  click(role('button', { name: 'Dismiss' })),
91
107
  Command.resolve(
92
108
  Dialog.CloseDialog,
93
109
  Dialog.Message.CompletedCloseDialog(),
94
110
  ),
111
+ Mount.expectEnded(Dialog.AcquireResources),
95
112
  expect(text('Export Failed')).toBeAbsent(),
96
113
  )
97
114
  })
@@ -200,6 +217,7 @@ describe('grid size change', () => {
200
217
  click(role('radio', { name: '8' })),
201
218
  resolveFocusOption,
202
219
  Command.resolve(Dialog.ShowDialog, Dialog.Message.SucceededShowDialog()),
220
+ resolveDialogResources,
203
221
  expect(text('Change to 8\u00d78?')).toExist(),
204
222
  expect(
205
223
  text('This will clear your canvas and reset undo history.'),
@@ -213,22 +231,23 @@ describe('grid size change', () => {
213
231
  const modelWithPendingResize: Model = evo(createTestModel(), {
214
232
  maybePendingGridSize: () => Option.some(8),
215
233
  gridSizeConfirmDialog: () =>
216
- Dialog.init({
234
+ Dialog.boot({
217
235
  id: 'grid-size-confirm-dialog',
218
- isOpen: true,
219
- }),
236
+ }).model,
220
237
  undoStack: () => [createEmptyGrid(4)],
221
238
  })
222
239
 
223
240
  scene(
224
241
  { update, view },
225
242
  given(modelWithPendingResize),
243
+ resolveDialogResources,
226
244
  expect(text('Change to 8\u00d78?')).toExist(),
227
245
  click(role('button', { name: 'Clear and Resize' })),
228
246
  Command.resolve(
229
247
  Dialog.CloseDialog,
230
248
  Dialog.Message.CompletedCloseDialog(),
231
249
  ),
250
+ Mount.expectEnded(Dialog.AcquireResources),
232
251
  Command.resolve(SaveCanvas, Message.CompletedSaveCanvas()),
233
252
  expect(text('Change to 8\u00d78?')).toBeAbsent(),
234
253
  )
@@ -238,21 +257,22 @@ describe('grid size change', () => {
238
257
  const modelWithPendingResize: Model = evo(createTestModel(), {
239
258
  maybePendingGridSize: () => Option.some(8),
240
259
  gridSizeConfirmDialog: () =>
241
- Dialog.init({
260
+ Dialog.boot({
242
261
  id: 'grid-size-confirm-dialog',
243
- isOpen: true,
244
- }),
262
+ }).model,
245
263
  })
246
264
 
247
265
  scene(
248
266
  { update, view },
249
267
  given(modelWithPendingResize),
268
+ resolveDialogResources,
250
269
  expect(text('Change to 8\u00d78?')).toExist(),
251
270
  click(role('button', { name: 'Cancel' })),
252
271
  Command.resolve(
253
272
  Dialog.CloseDialog,
254
273
  Dialog.Message.CompletedCloseDialog(),
255
274
  ),
275
+ Mount.expectEnded(Dialog.AcquireResources),
256
276
  expect(text('Change to 8\u00d78?')).toBeAbsent(),
257
277
  )
258
278
  })
@@ -316,10 +316,9 @@ describe('grid size', () => {
316
316
  const modelWithPending: Model = evo(emptyModel, {
317
317
  maybePendingGridSize: () => Option.some(8),
318
318
  gridSizeConfirmDialog: () =>
319
- Dialog.init({
319
+ Dialog.boot({
320
320
  id: 'grid-size-confirm-dialog',
321
- isOpen: true,
322
- }),
321
+ }).model,
323
322
  undoStack: () => [createEmptyGrid(4)],
324
323
  })
325
324
 
@@ -40,7 +40,7 @@ const toToolMessage = (event: KeyboardEvent): Option.Option<Message> => {
40
40
 
41
41
  export const subscriptions = Subscription.make<Model, Message>()(entry => ({
42
42
  undoRedoKeys: Subscription.persistent(
43
- Subscription.fromEventFilterMapPreventDefault<KeyboardEvent, Message>({
43
+ Subscription.fromEventFilterMapPreventDefault({
44
44
  target: document,
45
45
  type: 'keydown',
46
46
  toMessage: toUndoRedoMessage,
@@ -48,7 +48,7 @@ export const subscriptions = Subscription.make<Model, Message>()(entry => ({
48
48
  ),
49
49
 
50
50
  toolKeys: Subscription.persistent(
51
- Subscription.fromEventFilterMap<KeyboardEvent, Message>({
51
+ Subscription.fromEventFilterMap({
52
52
  target: document,
53
53
  type: 'keydown',
54
54
  toMessage: toToolMessage,
@@ -1,12 +1,13 @@
1
1
  import { Runtime } from 'foldkit'
2
2
 
3
- import { Message, Model, init, update, view } from './main'
3
+ import { Message, Model, init, subscriptions, update, view } from './main'
4
4
 
5
5
  const application = Runtime.makeApplication({
6
6
  Model,
7
7
  init,
8
8
  update,
9
9
  view,
10
+ subscriptions,
10
11
  container: document.getElementById('root'),
11
12
  routing: {
12
13
  onUrlRequest: request => Message.ClickedLink({ request }),
@@ -1,5 +1,5 @@
1
1
  import { Array, Effect, Match, Option, Schema } from 'effect'
2
- import { Command, Runtime, Update } from 'foldkit'
2
+ import { Command, Runtime, Subscription, Update } from 'foldkit'
3
3
  import { Document, Html, HtmlBuilder } from 'foldkit/html'
4
4
  import { defineMessageUnion } from 'foldkit/message'
5
5
  import { UrlRequest, load, pushUrl } from 'foldkit/navigation'
@@ -37,11 +37,15 @@ export type Model = typeof Model.Type
37
37
 
38
38
  // MESSAGE
39
39
 
40
+ const NavigationShortcut = Schema.Literals(['GH', 'GP', 'GF', 'GN'])
41
+ type NavigationShortcut = typeof NavigationShortcut.Type
42
+
40
43
  export const Message = defineMessageUnion({
41
44
  CompletedNavigateInternal: {},
42
45
  CompletedLoadExternal: {},
43
46
  ClickedLink: { request: UrlRequest },
44
47
  ChangedUrl: { url: Url },
48
+ EnteredNavigationShortcut: { shortcut: NavigationShortcut },
45
49
  GotPeopleMessage: { message: People.Message },
46
50
  })
47
51
 
@@ -59,18 +63,15 @@ export const init: Runtime.RoutingApplicationInit<Model, Message> = (
59
63
  Match.orElse(() => AppRoute.People({ searchText: Option.none() })),
60
64
  )
61
65
 
62
- const peopleInit = People.init(initialPeopleRoute)
63
- return {
64
- model: { route, peoplePage: peopleInit.model },
65
- commands: Command.mapMessages(peopleInit.commands, childMessage =>
66
- Message.GotPeopleMessage({ message: childMessage }),
67
- ),
68
- }
66
+ return Update.foldChildInit(People.init(initialPeopleRoute), {
67
+ toParentModel: peoplePage => ({ route, peoplePage }),
68
+ toParentMessage: message => Message.GotPeopleMessage({ message }),
69
+ })
69
70
  }
70
71
 
71
72
  // COMMAND
72
73
 
73
- const NavigateInternal = Command.define('NavigateInternal', {
74
+ export const NavigateInternal = Command.define('NavigateInternal', {
74
75
  args: { url: Schema.String },
75
76
  messages: [Message.CompletedNavigateInternal],
76
77
  execute: ({ url }) =>
@@ -88,6 +89,15 @@ const LoadExternal = Command.define('LoadExternal', {
88
89
 
89
90
  type UpdateReturn = Update.Return<Model, Message>
90
91
 
92
+ const navigationUrlByShortcut: Readonly<
93
+ Record<NavigationShortcut, () => string>
94
+ > = {
95
+ GH: homeRouter,
96
+ GP: () => peopleRouter({ searchText: Option.none() }),
97
+ GF: filesIndexRouter,
98
+ GN: nestedRouter,
99
+ }
100
+
91
101
  const foldPeopleEntry = <Input>(
92
102
  update: (peoplePage: People.Model, input: Input) => People.UpdateReturn,
93
103
  ): Update.Fold<Model, Message, Input> =>
@@ -138,9 +148,46 @@ export const update = (model: Model, message: Message) =>
138
148
  return Update.combine(model, [setRoute(nextRoute), ...routeSteps])
139
149
  },
140
150
 
151
+ EnteredNavigationShortcut: ({ shortcut }) => {
152
+ const url = navigationUrlByShortcut[shortcut]()
153
+
154
+ return { model, commands: [NavigateInternal({ url })] }
155
+ },
156
+
141
157
  GotPeopleMessage: ({ message }) => foldPeople(model, message),
142
158
  })
143
159
 
160
+ // SUBSCRIPTION
161
+
162
+ export const subscriptions = Subscription.make<Model, Message>()(() => ({
163
+ shortcuts: Subscription.persistent(
164
+ Subscription.keyboardShortcuts<Message>({
165
+ bindings: [
166
+ {
167
+ shortcut: ['G', 'H'],
168
+ toMessage: () =>
169
+ Message.EnteredNavigationShortcut({ shortcut: 'GH' }),
170
+ },
171
+ {
172
+ shortcut: ['G', 'P'],
173
+ toMessage: () =>
174
+ Message.EnteredNavigationShortcut({ shortcut: 'GP' }),
175
+ },
176
+ {
177
+ shortcut: ['G', 'F'],
178
+ toMessage: () =>
179
+ Message.EnteredNavigationShortcut({ shortcut: 'GF' }),
180
+ },
181
+ {
182
+ shortcut: ['G', 'N'],
183
+ toMessage: () =>
184
+ Message.EnteredNavigationShortcut({ shortcut: 'GN' }),
185
+ },
186
+ ],
187
+ }),
188
+ ),
189
+ }))
190
+
144
191
  // VIEW
145
192
 
146
193
  const navigationView = (
@@ -3,8 +3,14 @@ import { Command, given, message, model, story } from 'foldkit/story'
3
3
  import { fromString } from 'foldkit/url'
4
4
  import { describe, expect, test } from 'vitest'
5
5
 
6
- import { AppRoute, Message, Model, update } from './main'
6
+ import { AppRoute, Message, Model, NavigateInternal, update } from './main'
7
7
  import { People } from './page'
8
+ import {
9
+ filesIndexRouter,
10
+ homeRouter,
11
+ nestedRouter,
12
+ peopleRouter,
13
+ } from './route'
8
14
 
9
15
  const peoplePageWith = (searchInput: string) =>
10
16
  People.Model.make({
@@ -47,6 +53,52 @@ const resolveFetch = (searchText: string) =>
47
53
  )
48
54
 
49
55
  describe('update', () => {
56
+ describe('EnteredNavigationShortcut', () => {
57
+ test('GH produces the Home navigation Command', () => {
58
+ story(
59
+ update,
60
+ given(home),
61
+ message(Message.EnteredNavigationShortcut({ shortcut: 'GH' })),
62
+ Command.expectHas(NavigateInternal({ url: homeRouter() })),
63
+ Command.resolve(NavigateInternal, Message.CompletedNavigateInternal()),
64
+ )
65
+ })
66
+
67
+ test('GP produces the People navigation Command', () => {
68
+ story(
69
+ update,
70
+ given(home),
71
+ message(Message.EnteredNavigationShortcut({ shortcut: 'GP' })),
72
+ Command.expectHas(
73
+ NavigateInternal({
74
+ url: peopleRouter({ searchText: Option.none() }),
75
+ }),
76
+ ),
77
+ Command.resolve(NavigateInternal, Message.CompletedNavigateInternal()),
78
+ )
79
+ })
80
+
81
+ test('GF produces the Files navigation Command', () => {
82
+ story(
83
+ update,
84
+ given(home),
85
+ message(Message.EnteredNavigationShortcut({ shortcut: 'GF' })),
86
+ Command.expectHas(NavigateInternal({ url: filesIndexRouter() })),
87
+ Command.resolve(NavigateInternal, Message.CompletedNavigateInternal()),
88
+ )
89
+ })
90
+
91
+ test('GN produces the Nested navigation Command', () => {
92
+ story(
93
+ update,
94
+ given(home),
95
+ message(Message.EnteredNavigationShortcut({ shortcut: 'GN' })),
96
+ Command.expectHas(NavigateInternal({ url: nestedRouter() })),
97
+ Command.resolve(NavigateInternal, Message.CompletedNavigateInternal()),
98
+ )
99
+ })
100
+ })
101
+
50
102
  describe('ChangedUrl', () => {
51
103
  test('navigating to /people parses to a People route', () => {
52
104
  story(
@@ -61,7 +61,9 @@ export const Message = defineMessageUnion({
61
61
 
62
62
  export type Message = typeof Message.Type
63
63
 
64
- const slowWarningTarget = new EventTarget()
64
+ const slowWarningTarget: Subscription.TypedEventTarget<{
65
+ [SLOW_WARNING_EVENT]: CustomEvent<SlowWarningReport>
66
+ }> = new EventTarget()
65
67
 
66
68
  const burnCpu = (durationMs: number): number => {
67
69
  const stopAt = performance.now() + durationMs
@@ -218,10 +220,7 @@ export const init: Runtime.ApplicationInit<Model, Message> = () => ({
218
220
 
219
221
  export const subscriptions = Subscription.make<Model, Message>()(entry => ({
220
222
  slowWarnings: Subscription.persistent(
221
- Subscription.fromEventFilterMap<
222
- CustomEvent,
223
- typeof Message.RecordedSlowWarning.Type
224
- >({
223
+ Subscription.fromEventFilterMap({
225
224
  target: slowWarningTarget,
226
225
  type: SLOW_WARNING_EVENT,
227
226
  toMessage: event =>
@@ -252,7 +252,7 @@ export const subscriptions = Subscription.make<Model, Message>()(entry => ({
252
252
  ),
253
253
 
254
254
  keyboard: Subscription.persistent(
255
- Subscription.fromEventFilterMapPreventDefault<KeyboardEvent, Message>({
255
+ Subscription.fromEventFilterMapPreventDefault({
256
256
  target: document,
257
257
  type: 'keydown',
258
258
  toMessage: keyboardEvent =>
@@ -197,17 +197,10 @@ export const init: Runtime.RoutingApplicationInit<Model, Message, Flags> = (
197
197
  flags: Flags,
198
198
  url: Url,
199
199
  ) => {
200
- const uiInit_ = uiInit(flags.today)
201
-
202
- return {
203
- model: {
204
- route: urlToAppRoute(url),
205
- uiModel: uiInit_.model,
206
- },
207
- commands: Command.mapMessages(uiInit_.commands, message =>
208
- Message.GotUiMessage({ message }),
209
- ),
210
- }
200
+ return Update.foldChildInit(uiInit(flags.today), {
201
+ toParentModel: uiModel => ({ route: urlToAppRoute(url), uiModel }),
202
+ toParentMessage: message => Message.GotUiMessage({ message }),
203
+ })
211
204
  }
212
205
 
213
206
  // UPDATE
@@ -1,5 +1,5 @@
1
1
  import { Calendar } from 'foldkit'
2
- import { expect, given, role, scene, text } from 'foldkit/scene'
2
+ import { click, expect, given, role, scene, text } from 'foldkit/scene'
3
3
  import { describe, test } from 'vitest'
4
4
 
5
5
  import { AppRoute, type Model, update, view } from './main'
@@ -85,6 +85,18 @@ describe('view', () => {
85
85
  )
86
86
  })
87
87
 
88
+ test('the Disclosure collapsed preview expands from its own control', () => {
89
+ scene(
90
+ { update, view },
91
+ given(modelForRoute(AppRoute.Disclosure())),
92
+ expect(role('heading', { name: 'Basic' })).toExist(),
93
+ expect(role('heading', { name: 'Animated' })).toExist(),
94
+ expect(role('heading', { name: 'Collapsed preview' })).toExist(),
95
+ click(role('button', { name: 'Read more' })),
96
+ expect(role('button', { name: 'Show less' })).toExist(),
97
+ )
98
+ })
99
+
88
100
  test('the NotFound route renders the 404 panel and a Go Home link', () => {
89
101
  scene(
90
102
  { update, view },
@@ -101,10 +101,9 @@ describe('update', () => {
101
101
  const modelWithOpenMenu: Model = evo(initialModel, {
102
102
  uiModel: evo({
103
103
  mobileMenuDialog: () =>
104
- Dialog.init({
104
+ Dialog.boot({
105
105
  id: 'mobile-menu',
106
- isOpen: true,
107
- }),
106
+ }).model,
108
107
  }),
109
108
  })
110
109
 
@@ -86,6 +86,7 @@ export const uiInit = (today: Calendar.CalendarDate): InitReturn => ({
86
86
  nestedDialogChildDemo: Dialog.init({ id: 'nested-dialog-child-demo' }),
87
87
  isDisclosureBasicDemoOpen: false,
88
88
  isDisclosureAnimatedDemoOpen: false,
89
+ isDisclosureCollapsedPreviewDemoOpen: false,
89
90
  dragAndDropDemo: DragAndDrop.init({ id: 'drag-and-drop-demo' }),
90
91
  dragAndDropDemoColumns: [
91
92
  {
@@ -54,6 +54,7 @@ export const Message = defineMessageUnion({
54
54
  ClickedOpenProjectSettings: {},
55
55
  ToggledDisclosureBasicDemo: { isOpen: Schema.Boolean },
56
56
  ToggledDisclosureAnimatedDemo: { isOpen: Schema.Boolean },
57
+ ToggledDisclosureCollapsedPreviewDemo: { isOpen: Schema.Boolean },
57
58
  GotCalendarBasicDemoMessage: { message: Calendar.Message },
58
59
  GotDatePickerBasicDemoMessage: { message: DatePicker.Message },
59
60
  GotDragAndDropDemoMessage: { message: DragAndDrop.Message },
@@ -102,6 +102,7 @@ export const UiModel = Schema.Struct({
102
102
  nestedDialogChildDemo: Dialog.Model,
103
103
  isDisclosureBasicDemoOpen: Schema.Boolean,
104
104
  isDisclosureAnimatedDemoOpen: Schema.Boolean,
105
+ isDisclosureCollapsedPreviewDemoOpen: Schema.Boolean,
105
106
  dragAndDropDemo: DragAndDrop.Model,
106
107
  dragAndDropDemoColumns: Schema.Array(DemoColumn),
107
108
  fileDropBasicDemo: FileDrop.Model,
@@ -46,7 +46,7 @@ const virtualListVariableDemoSubscriptions = Subscription.lift({
46
46
  UiMessage.GotVirtualListVariableDemoMessage({ message }),
47
47
  })
48
48
 
49
- export const subscriptions = Subscription.aggregate<UiModel, UiMessage>()(
49
+ export const subscriptions = Subscription.aggregate(
50
50
  dragAndDropSubscriptions,
51
51
  sliderRatingSubscriptions,
52
52
  sliderVolumeSubscriptions,
@@ -1009,6 +1009,12 @@ export const uiUpdate = (model: UiModel, message: UiMessage) =>
1009
1009
  }),
1010
1010
  }),
1011
1011
 
1012
+ ToggledDisclosureCollapsedPreviewDemo: ({ isOpen }) => ({
1013
+ model: evo(model, {
1014
+ isDisclosureCollapsedPreviewDemoOpen: () => isOpen,
1015
+ }),
1016
+ }),
1017
+
1012
1018
  GotCalendarBasicDemoMessage: ({ message }) =>
1013
1019
  foldCalendarBasicDemo(model, message),
1014
1020
 
@@ -10,6 +10,8 @@ import type { UiModel } from '../model'
10
10
 
11
11
  const DISCLOSURE_BASIC_DEMO_ID = 'disclosure-basic-demo'
12
12
  const DISCLOSURE_ANIMATED_DEMO_ID = 'disclosure-animated-demo'
13
+ const DISCLOSURE_COLLAPSED_PREVIEW_DEMO_ID = 'disclosure-collapsed-preview-demo'
14
+ const COLLAPSED_PREVIEW_HEIGHT = '6rem'
13
15
 
14
16
  const PANEL_TEXT =
15
17
  'Foldkit is an Elm-inspired UI framework powered by Effect. It brings the Model-View-Update architecture to TypeScript with Schema-typed state, explicit side effects via commands, and composable headless UI components.'
@@ -32,6 +34,31 @@ const panelText = (h: HtmlBuilder<UiMessage>): Html => {
32
34
  return h.p([h.Class('text-gray-800')], [PANEL_TEXT])
33
35
  }
34
36
 
37
+ const collapsedPreviewPanel = (h: HtmlBuilder<UiMessage>): Html =>
38
+ h.div(
39
+ [h.Class('space-y-3 text-base leading-6 text-gray-800')],
40
+ [
41
+ h.p(
42
+ [],
43
+ [
44
+ 'Foldkit keeps application state in one Model. Messages record facts, and update decides how each fact changes that Model.',
45
+ ],
46
+ ),
47
+ h.p(
48
+ [],
49
+ [
50
+ 'Commands describe one-time work for the Runtime, so network requests, focus changes, and storage writes stay outside state transitions.',
51
+ ],
52
+ ),
53
+ h.p(
54
+ [],
55
+ [
56
+ 'That separation leaves every transition visible in one place and gives tests the same inputs and outputs the application uses.',
57
+ ],
58
+ ),
59
+ ],
60
+ )
61
+
35
62
  const subheading = (text: string, h: HtmlBuilder<UiMessage>): Html => {
36
63
  return h.h3(
37
64
  [h.Class('text-lg font-semibold text-gray-900 mt-8 mb-4')],
@@ -136,6 +163,89 @@ export const view = Submodel.defineView<UiModel, UiMessage>(
136
163
  },
137
164
  h,
138
165
  ),
166
+
167
+ subheading('Collapsed preview', h),
168
+ h.p(
169
+ [h.Class('mb-1.5 text-sm font-medium text-gray-900')],
170
+ ['Featured article'],
171
+ ),
172
+ Disclosure.view(
173
+ {
174
+ id: DISCLOSURE_COLLAPSED_PREVIEW_DEMO_ID,
175
+ isOpen: model.isDisclosureCollapsedPreviewDemoOpen,
176
+ onToggle: isOpen =>
177
+ UiMessage.ToggledDisclosureCollapsedPreviewDemo({ isOpen }),
178
+ toView: ({ button, panel, animatePanel }) =>
179
+ h.article(
180
+ [
181
+ h.Class(
182
+ 'relative overflow-hidden rounded-lg border border-gray-300 bg-white',
183
+ ),
184
+ ],
185
+ [
186
+ h.h4(
187
+ [h.Class('px-4 py-3 text-base font-normal text-gray-900')],
188
+ ['Why the Elm Architecture scales'],
189
+ ),
190
+ h.div(
191
+ [h.Class('relative')],
192
+ [
193
+ animatePanel(
194
+ h.div(
195
+ [
196
+ ...panel,
197
+ h.Class(
198
+ clsx(
199
+ 'border-t border-gray-300 px-4 pt-3',
200
+ model.isDisclosureCollapsedPreviewDemoOpen
201
+ ? 'pb-20'
202
+ : 'pb-3',
203
+ ),
204
+ ),
205
+ ],
206
+ [collapsedPreviewPanel(h)],
207
+ ),
208
+ { peek: COLLAPSED_PREVIEW_HEIGHT },
209
+ ),
210
+ ...(model.isDisclosureCollapsedPreviewDemoOpen
211
+ ? []
212
+ : [
213
+ h.div([
214
+ h.AriaHidden(true),
215
+ h.Class(
216
+ 'pointer-events-none absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t from-white to-transparent',
217
+ ),
218
+ ]),
219
+ ]),
220
+ ],
221
+ ),
222
+ h.div(
223
+ [
224
+ h.Class(
225
+ 'absolute inset-x-0 bottom-2.5 flex justify-center px-4',
226
+ ),
227
+ ],
228
+ [
229
+ h.button(
230
+ [
231
+ ...button,
232
+ h.Class(
233
+ 'cursor-pointer whitespace-nowrap rounded-full border border-gray-300 bg-white px-5 py-2 text-sm font-medium text-gray-900 shadow-sm transition hover:bg-gray-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-600 select-none',
234
+ ),
235
+ ],
236
+ [
237
+ model.isDisclosureCollapsedPreviewDemoOpen
238
+ ? 'Show less'
239
+ : 'Read more',
240
+ ],
241
+ ),
242
+ ],
243
+ ),
244
+ ],
245
+ ),
246
+ },
247
+ h,
248
+ ),
139
249
  ],
140
250
  )
141
251
  },
@@ -1,22 +1,22 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "channel": "stable",
4
- "sourceCommit": "a124b3451a885f2e58b4477adcd04b2ef9e4795a",
4
+ "sourceCommit": "7a3180b90d4eff772fa22928f31009e7dda9ff8f",
5
5
  "packages": {
6
- "create-foldkit-app": "0.35.0",
7
- "@foldkit/devtools": "0.160.0",
8
- "@foldkit/devtools-mcp": "0.20.0",
9
- "foldkit": "0.160.0",
10
- "@foldkit/markdown": "0.10.0",
11
- "@foldkit/oxlint-plugin": "0.13.0",
12
- "@foldkit/ui": "0.160.0",
13
- "@foldkit/vite-plugin": "0.22.0"
6
+ "create-foldkit-app": "0.35.1",
7
+ "@foldkit/devtools": "0.161.0",
8
+ "@foldkit/devtools-mcp": "0.21.0",
9
+ "foldkit": "0.161.0",
10
+ "@foldkit/markdown": "0.11.0",
11
+ "@foldkit/oxlint-plugin": "0.14.0",
12
+ "@foldkit/ui": "0.161.0",
13
+ "@foldkit/vite-plugin": "0.23.0"
14
14
  },
15
15
  "dependencies": {
16
- "@foldkit/devtools": "0.160.0",
17
- "@foldkit/vite-plugin": "0.22.0",
18
- "@foldkit/devtools-mcp": "0.20.0",
19
- "@foldkit/oxlint-plugin": "0.13.0",
16
+ "@foldkit/devtools": "0.161.0",
17
+ "@foldkit/vite-plugin": "0.23.0",
18
+ "@foldkit/devtools-mcp": "0.21.0",
19
+ "@foldkit/oxlint-plugin": "0.14.0",
20
20
  "@types/node": "^26.4.0",
21
21
  "happy-dom": "^20.11.13",
22
22
  "oxfmt": "^0.66.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-foldkit-app",
3
- "version": "0.35.0",
3
+ "version": "0.35.1",
4
4
  "description": "Create Foldkit applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",