create-foldkit-app 0.32.0 → 0.32.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.
- package/dist/commands/create.js +9 -7
- package/dist/templates/base/FOLDKIT.md +22 -6
- package/dist/templates/examples/api-cache/src/data.ts +14 -14
- package/dist/templates/examples/api-cache/src/main.ts +42 -39
- package/dist/templates/examples/auth/src/command.ts +3 -3
- package/dist/templates/examples/auth/src/domain/session.ts +8 -6
- package/dist/templates/examples/auth/src/main.ts +11 -11
- package/dist/templates/examples/auth/src/message.ts +3 -3
- package/dist/templates/examples/auth/src/model.ts +2 -2
- package/dist/templates/examples/auth/src/page/loggedIn/index.ts +1 -1
- package/dist/templates/examples/auth/src/page/loggedIn/view.ts +3 -3
- package/dist/templates/examples/auth/src/page/loggedOut/index.ts +1 -1
- package/dist/templates/examples/auth/src/page/loggedOut/page/login.ts +9 -17
- package/dist/templates/examples/auth/src/page/loggedOut/update.ts +4 -6
- package/dist/templates/examples/auth/src/page/loggedOut/view.ts +3 -3
- package/dist/templates/examples/auth/src/route.ts +2 -2
- package/dist/templates/examples/auth/src/update.ts +37 -40
- package/dist/templates/examples/auth/src/view.ts +6 -6
- package/dist/templates/examples/canvas-art/src/main.ts +22 -30
- package/dist/templates/examples/charting/src/command.ts +3 -3
- package/dist/templates/examples/charting/src/domain.ts +53 -48
- package/dist/templates/examples/charting/src/echarts.ts +6 -6
- package/dist/templates/examples/charting/src/githubApi.ts +33 -34
- package/dist/templates/examples/charting/src/http.ts +7 -7
- package/dist/templates/examples/charting/src/message.ts +6 -6
- package/dist/templates/examples/charting/src/model.ts +6 -6
- package/dist/templates/examples/charting/src/npmApi.ts +19 -15
- package/dist/templates/examples/charting/src/subscription.ts +5 -5
- package/dist/templates/examples/charting/src/telemetry.test.ts +18 -12
- package/dist/templates/examples/charting/src/telemetry.ts +11 -18
- package/dist/templates/examples/charting/src/update.ts +17 -23
- package/dist/templates/examples/charting/src/view/chart.ts +2 -2
- package/dist/templates/examples/counter/src/main.ts +2 -2
- package/dist/templates/examples/counters/src/counter.ts +2 -2
- package/dist/templates/examples/counters/src/main.ts +8 -8
- package/dist/templates/examples/embedding/src/main.ts +10 -7
- package/dist/templates/examples/form/src/main.ts +18 -14
- package/dist/templates/examples/generative-art/src/command.ts +7 -2
- package/dist/templates/examples/generative-art/src/message.ts +14 -14
- package/dist/templates/examples/generative-art/src/model.ts +21 -21
- package/dist/templates/examples/generative-art/src/update.ts +15 -17
- package/dist/templates/examples/interrupting-commands/src/main.ts +30 -27
- package/dist/templates/examples/job-application/src/domain/proficiencyLevel.ts +2 -2
- package/dist/templates/examples/job-application/src/domain/pronounOption.ts +2 -2
- package/dist/templates/examples/job-application/src/domain/step.ts +11 -11
- package/dist/templates/examples/job-application/src/main.ts +5 -5
- package/dist/templates/examples/job-application/src/message.ts +2 -2
- package/dist/templates/examples/job-application/src/model.ts +5 -5
- package/dist/templates/examples/job-application/src/step/attachments.ts +32 -34
- package/dist/templates/examples/job-application/src/step/coverLetter.ts +4 -4
- package/dist/templates/examples/job-application/src/step/education/education.ts +11 -14
- package/dist/templates/examples/job-application/src/step/education/entry.ts +21 -22
- package/dist/templates/examples/job-application/src/step/personalInfo.ts +44 -54
- package/dist/templates/examples/job-application/src/step/skills/entry.ts +12 -14
- package/dist/templates/examples/job-application/src/step/skills/skills.ts +11 -14
- package/dist/templates/examples/job-application/src/step/workHistory/entry.ts +43 -45
- package/dist/templates/examples/job-application/src/step/workHistory/workHistory.ts +11 -14
- package/dist/templates/examples/job-application/src/update.ts +17 -17
- package/dist/templates/examples/job-application/src/view/attachments.ts +5 -5
- package/dist/templates/examples/job-application/src/view/datePicker.ts +3 -3
- package/dist/templates/examples/job-application/src/view/field.ts +5 -5
- package/dist/templates/examples/job-application/src/view/index.ts +28 -22
- package/dist/templates/examples/kanban/src/command.ts +4 -4
- package/dist/templates/examples/kanban/src/domain/card.ts +6 -6
- package/dist/templates/examples/kanban/src/domain/column.ts +9 -9
- package/dist/templates/examples/kanban/src/main.ts +4 -4
- package/dist/templates/examples/kanban/src/message.ts +6 -6
- package/dist/templates/examples/kanban/src/model.ts +11 -9
- package/dist/templates/examples/kanban/src/update.ts +5 -5
- package/dist/templates/examples/managed-resource-layer/src/main.ts +20 -20
- package/dist/templates/examples/map/src/locations.ts +7 -7
- package/dist/templates/examples/map/src/main.ts +83 -44
- package/dist/templates/examples/map/src/mount.test.ts +127 -0
- package/dist/templates/examples/pixel-art/src/command.ts +12 -5
- package/dist/templates/examples/pixel-art/src/main.ts +4 -4
- package/dist/templates/examples/pixel-art/src/message.ts +7 -7
- package/dist/templates/examples/pixel-art/src/model.ts +34 -29
- package/dist/templates/examples/pixel-art/src/subscription.ts +2 -2
- package/dist/templates/examples/pixel-art/src/update.ts +79 -86
- package/dist/templates/examples/query-sync/src/main.ts +106 -102
- package/dist/templates/examples/route-transitions/src/data.ts +6 -6
- package/dist/templates/examples/route-transitions/src/main.ts +23 -29
- package/dist/templates/examples/route-transitions/src/route.ts +3 -3
- package/dist/templates/examples/routing/src/fileTree.ts +10 -8
- package/dist/templates/examples/routing/src/main.ts +23 -21
- package/dist/templates/examples/routing/src/page/people.ts +14 -22
- package/dist/templates/examples/routing/src/route.ts +7 -7
- package/dist/templates/examples/shopping-cart/src/main.ts +30 -31
- package/dist/templates/examples/shopping-cart/src/page/products.ts +10 -10
- package/dist/templates/examples/shopping-cart/src/route.ts +6 -4
- package/dist/templates/examples/slow-warnings/src/main.ts +35 -43
- package/dist/templates/examples/snake/src/domain/snake.ts +2 -2
- package/dist/templates/examples/snake/src/main.ts +39 -47
- package/dist/templates/examples/ssg/src/main.ts +5 -5
- package/dist/templates/examples/ssg/src/route.ts +2 -2
- package/dist/templates/examples/ssr/src/cookie.ts +3 -3
- package/dist/templates/examples/ssr/src/main.ts +10 -10
- package/dist/templates/examples/state-machine/src/main.ts +55 -53
- package/dist/templates/examples/state-machine/src/view.ts +7 -7
- package/dist/templates/examples/stopwatch/src/main.ts +10 -10
- package/dist/templates/examples/todo/src/main.ts +41 -34
- package/dist/templates/examples/ui-showcase/src/main.ts +54 -42
- package/dist/templates/examples/ui-showcase/src/scene.test.ts +1 -1
- package/dist/templates/examples/ui-showcase/src/ui/init.ts +1 -2
- package/dist/templates/examples/ui-showcase/src/ui/message.ts +17 -18
- package/dist/templates/examples/ui-showcase/src/ui/model.ts +47 -44
- package/dist/templates/examples/ui-showcase/src/ui/subscriptions.ts +1 -1
- package/dist/templates/examples/ui-showcase/src/ui/toast.ts +4 -4
- package/dist/templates/examples/ui-showcase/src/ui/update.ts +287 -298
- package/dist/templates/examples/ui-showcase/src/ui/view/animation.ts +2 -2
- package/dist/templates/examples/ui-showcase/src/ui/view/button.ts +1 -1
- package/dist/templates/examples/ui-showcase/src/ui/view/calendar.ts +4 -4
- package/dist/templates/examples/ui-showcase/src/ui/view/checkbox.ts +1 -1
- package/dist/templates/examples/ui-showcase/src/ui/view/combobox.ts +1 -1
- package/dist/templates/examples/ui-showcase/src/ui/view/datePicker.ts +4 -4
- package/dist/templates/examples/ui-showcase/src/ui/view/dialog.ts +1 -1
- package/dist/templates/examples/ui-showcase/src/ui/view/disclosure.ts +1 -1
- package/dist/templates/examples/ui-showcase/src/ui/view/dragAndDrop.ts +1 -1
- package/dist/templates/examples/ui-showcase/src/ui/view/fieldset.ts +1 -1
- package/dist/templates/examples/ui-showcase/src/ui/view/fileDrop.ts +6 -6
- package/dist/templates/examples/ui-showcase/src/ui/view/hoverIntent.ts +9 -20
- package/dist/templates/examples/ui-showcase/src/ui/view/input.ts +1 -1
- package/dist/templates/examples/ui-showcase/src/ui/view/listbox.ts +1 -1
- package/dist/templates/examples/ui-showcase/src/ui/view/menu.ts +15 -15
- package/dist/templates/examples/ui-showcase/src/ui/view/popover.ts +1 -1
- package/dist/templates/examples/ui-showcase/src/ui/view/radioGroup.ts +1 -1
- package/dist/templates/examples/ui-showcase/src/ui/view/select.ts +1 -1
- package/dist/templates/examples/ui-showcase/src/ui/view/slider.ts +1 -1
- package/dist/templates/examples/ui-showcase/src/ui/view/switch.ts +1 -1
- package/dist/templates/examples/ui-showcase/src/ui/view/tabs.ts +7 -7
- package/dist/templates/examples/ui-showcase/src/ui/view/textarea.ts +1 -1
- package/dist/templates/examples/ui-showcase/src/ui/view/toast.ts +8 -8
- package/dist/templates/examples/ui-showcase/src/ui/view/tooltip.ts +1 -1
- package/dist/templates/examples/ui-showcase/src/ui/view/virtualList.ts +7 -7
- package/dist/templates/examples/view-transitions/src/artwork.ts +7 -7
- package/dist/templates/examples/view-transitions/src/main.ts +6 -6
- package/dist/templates/examples/view-transitions/src/route.ts +3 -3
- package/dist/templates/examples/weather/src/main.ts +42 -42
- package/dist/templates/examples/weather/src/story.test.ts +5 -5
- package/dist/templates/examples/web-components/src/main.ts +16 -16
- package/dist/templates/examples/websocket-chat/src/main.ts +30 -30
- package/dist/templates/release.json +13 -13
- package/dist/templates/rendering/ssg/src/main.ts +5 -5
- package/dist/templates/rendering/ssg/src/route.ts +2 -2
- package/dist/templates/rendering/ssr/server/main.ts +15 -15
- package/dist/templates/rendering/ssr/src/cookie.ts +3 -3
- package/dist/templates/rendering/ssr/src/main.ts +10 -10
- package/dist/utils/packages.js +17 -0
- package/package.json +1 -1
package/dist/commands/create.js
CHANGED
|
@@ -5,7 +5,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
5
5
|
import { examples } from '../examples.js';
|
|
6
6
|
import { Scaffold, renderings } from '../rendering.js';
|
|
7
7
|
import { createProject } from '../utils/files.js';
|
|
8
|
-
import { devCommand, installDependencies, } from '../utils/packages.js';
|
|
8
|
+
import { devCommand, installDependencies, readFoldkitSubtreeRef, } from '../utils/packages.js';
|
|
9
9
|
import { validateProjectName } from '../validateName.js';
|
|
10
10
|
const isWindows = process.platform === 'win32';
|
|
11
11
|
const promptForName = Prompt.text({
|
|
@@ -90,7 +90,7 @@ const installProjectDependencies = (projectPath, packageManager, scaffold, maybe
|
|
|
90
90
|
yield* Console.log(chalk.green('✅ Dependencies installed'));
|
|
91
91
|
yield* Console.log('');
|
|
92
92
|
});
|
|
93
|
-
const displaySuccessMessage = (name, packageManager) => Effect.gen(function* () {
|
|
93
|
+
const displaySuccessMessage = (name, packageManager, subtreeRef) => Effect.gen(function* () {
|
|
94
94
|
yield* Console.log(chalk.bold('All systems nominal.'));
|
|
95
95
|
yield* Console.log('');
|
|
96
96
|
yield* Console.log(` > ${chalk.cyan('cd')} ${name}`);
|
|
@@ -98,15 +98,16 @@ const displaySuccessMessage = (name, packageManager) => Effect.gen(function* ()
|
|
|
98
98
|
yield* Console.log('');
|
|
99
99
|
yield* Console.log(chalk.bold('AI-Assisted Development'));
|
|
100
100
|
yield* Console.log('');
|
|
101
|
-
yield* Console.log(' Vendor Foldkit in as a git subtree
|
|
102
|
-
'
|
|
103
|
-
'
|
|
101
|
+
yield* Console.log(' Vendor Foldkit in as a git subtree, pinned to the release this\n' +
|
|
102
|
+
' project installs, so your AI assistant can reference the matching\n' +
|
|
103
|
+
' source, examples, and documentation. Commit the scaffold first so\n' +
|
|
104
|
+
' subtree has a base commit to merge into:');
|
|
104
105
|
yield* Console.log('');
|
|
105
106
|
yield* Console.log(` > ${chalk.cyan('cd')} ${name}`);
|
|
106
107
|
yield* Console.log(` > ${chalk.cyan('git init')}`);
|
|
107
108
|
yield* Console.log(` > ${chalk.cyan('git add .')}`);
|
|
108
109
|
yield* Console.log(` > ${chalk.cyan('git commit -m "chore: initial commit"')}`);
|
|
109
|
-
yield* Console.log(` > ${chalk.cyan(
|
|
110
|
+
yield* Console.log(` > ${chalk.cyan(`git subtree add --prefix=repos/foldkit https://github.com/foldkit/foldkit.git ${subtreeRef} --squash`)}`);
|
|
110
111
|
yield* Console.log('');
|
|
111
112
|
yield* Console.log(` Details: ${chalk.cyan('foldkit.dev/ai/overview')}`);
|
|
112
113
|
yield* Console.log('');
|
|
@@ -138,8 +139,9 @@ export const create = (input) => Effect.gen(function* () {
|
|
|
138
139
|
}
|
|
139
140
|
const projectPath = path.resolve(name);
|
|
140
141
|
yield* validateProject(name, projectPath, packageManager);
|
|
142
|
+
const subtreeRef = yield* readFoldkitSubtreeRef;
|
|
141
143
|
yield* setupProject(name, projectPath, scaffold, packageManager);
|
|
142
144
|
yield* installProjectDependencies(projectPath, packageManager, scaffold, input.maybeDependencyManifestsDirectory);
|
|
143
|
-
yield* displaySuccessMessage(name, packageManager);
|
|
145
|
+
yield* displaySuccessMessage(name, packageManager, subtreeRef);
|
|
144
146
|
return name;
|
|
145
147
|
});
|
|
@@ -16,15 +16,29 @@ If `./repos/foldkit` exists, it contains the full Foldkit repository, vendored i
|
|
|
16
16
|
|
|
17
17
|
Treat the subtree as read-only reference: never import from `repos/foldkit/` in your project source. Imports must come from the `foldkit` npm package.
|
|
18
18
|
|
|
19
|
-
If `./repos/foldkit` does not exist and `AGENTS.md` says `subtree_prompted: false`, offer to
|
|
19
|
+
If `./repos/foldkit` does not exist and `AGENTS.md` says `subtree_prompted: false`, offer to vendor the repository. It gives you access to the full source, examples, and docs. Pin it to the release git tag matching the installed `foldkit` package; vendoring `main` instead can hand you examples and APIs from a release the project has not installed:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
git subtree add --prefix=repos/foldkit https://github.com/foldkit/foldkit.git "foldkit@$(node -p "require('./node_modules/foldkit/package.json').version")" --squash
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
A canary install has no release tag to pin to. Its version names its source commit (`0.156.0-canary.<commit>`); use the full hash of that commit as the ref instead. GitHub expands the short hash at `https://github.com/foldkit/foldkit/commit/<commit>`.
|
|
26
|
+
|
|
27
|
+
Whether the user accepts or declines, set that line in `AGENTS.md` to `true` so it isn't asked again.
|
|
20
28
|
|
|
21
29
|
If `foldkit-skills` is installed as a Claude Code plugin, the `generate-program` and `audit-program` skills carry snapshot architecture and conventions guides synced from the live code.
|
|
22
30
|
|
|
23
|
-
##
|
|
31
|
+
## After a Foldkit upgrade
|
|
24
32
|
|
|
25
|
-
Foldkit
|
|
33
|
+
Two things go stale when the project upgrades its Foldkit packages: the vendored subtree and this file. Bring both forward, subtree first, since the fresh copy of this file comes from it.
|
|
26
34
|
|
|
27
|
-
|
|
35
|
+
Re-pin the subtree to the newly installed release (for a canary install, pin to the commit its version names, as above):
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
git subtree pull --prefix=repos/foldkit https://github.com/foldkit/foldkit.git "foldkit@$(node -p "require('./node_modules/foldkit/package.json').version")" --squash
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Then replace this file whole. Foldkit's conventions change with its APIs, and a stale copy sends agents after APIs the installed packages no longer export. Copy `repos/foldkit/packages/create-foldkit-app/templates/base/FOLDKIT.md` over this file. Without the subtree, take the template from GitHub at the tag matching the installed version: `https://github.com/foldkit/foldkit/blob/foldkit@<version>/packages/create-foldkit-app/templates/base/FOLDKIT.md`. There is nothing here to merge or preserve.
|
|
28
42
|
|
|
29
43
|
## Project Conventions
|
|
30
44
|
|
|
@@ -84,9 +98,11 @@ Omit the children argument when an element has none: `h.div([h.Class('divider')]
|
|
|
84
98
|
|
|
85
99
|
Define a Command with `Command.define(name, { args, messages, execute })`; omit `args` when the Command takes none. Assign definitions to PascalCase constants. Never inline in pipe chains. Name the effect `execute` performs, not the later Model transition caused when update handles its result: a timer that only waits before update starts a dismissal is `WaitBeforeDismissal`, not `DismissAfter`. Commands catch all errors via `Effect.catch(() => Effect.succeed(Message.FailedX(...)))` so side effects never crash the app. Definitions live colocated with the update function that returns them.
|
|
86
100
|
|
|
101
|
+
Command args contain values already present in the Model or Message. Calling `Date.now()`, `crypto.randomUUID()`, or another source of time or randomness while preparing a Command happens before the Command executes, whether the call appears directly in the args object or its result is assigned to a local variable first. Obtain those values in `execute` and return them in the result Message.
|
|
102
|
+
|
|
87
103
|
For the with-args shape, see `repos/foldkit/examples/weather/src/main.ts` or `repos/foldkit/examples/kanban/src/command.ts`. For an argless DOM-side-effect Command, the argless form in `kanban/src/command.ts` (`FocusAddCardInput`) is the canonical reference.
|
|
88
104
|
|
|
89
|
-
For DOM operations (focus, scroll, modals, scroll lock), Foldkit ships a `Dom` module. For time, randomness, UUIDs, and delays, use Effect's
|
|
105
|
+
For DOM operations (focus, scroll, modals, scroll lock), Foldkit ships a `Dom` module. For time, randomness, UUIDs, and delays, use Effect's APIs directly (`Clock`, `Random`, `Crypto.Crypto`, `Effect.sleep`). Provide the platform Crypto layer when using `Crypto.Crypto`. Don't reach for raw `document.querySelector`, `setTimeout`, `Date.now()`, or `Math.random()`.
|
|
90
106
|
|
|
91
107
|
### File Organization
|
|
92
108
|
|
|
@@ -126,7 +142,7 @@ Declare the whole Message union with `defineMessageUnion()`, then put `type Mess
|
|
|
126
142
|
```ts
|
|
127
143
|
const Message = defineMessageUnion({
|
|
128
144
|
ClickedSubmit: {},
|
|
129
|
-
UpdatedEmail: { value:
|
|
145
|
+
UpdatedEmail: { value: Schema.String },
|
|
130
146
|
})
|
|
131
147
|
type Message = typeof Message.Type
|
|
132
148
|
```
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import { Array, Duration, Effect, Option, Random, Schema
|
|
1
|
+
import { Array, Duration, Effect, Option, Random, Schema } from 'effect'
|
|
2
2
|
|
|
3
|
-
export const Post =
|
|
4
|
-
id:
|
|
5
|
-
title:
|
|
6
|
-
excerpt:
|
|
3
|
+
export const Post = Schema.Struct({
|
|
4
|
+
id: Schema.String,
|
|
5
|
+
title: Schema.String,
|
|
6
|
+
excerpt: Schema.String,
|
|
7
7
|
})
|
|
8
8
|
export type Post = typeof Post.Type
|
|
9
9
|
|
|
10
|
-
export const PostDetail =
|
|
11
|
-
id:
|
|
12
|
-
title:
|
|
13
|
-
author:
|
|
14
|
-
body:
|
|
10
|
+
export const PostDetail = Schema.Struct({
|
|
11
|
+
id: Schema.String,
|
|
12
|
+
title: Schema.String,
|
|
13
|
+
author: Schema.String,
|
|
14
|
+
body: Schema.String,
|
|
15
15
|
})
|
|
16
16
|
export type PostDetail = typeof PostDetail.Type
|
|
17
17
|
|
|
18
|
-
export const Stats =
|
|
19
|
-
activeUsers:
|
|
20
|
-
requestsPerSecond:
|
|
21
|
-
cacheHitRatePercent:
|
|
18
|
+
export const Stats = Schema.Struct({
|
|
19
|
+
activeUsers: Schema.Number,
|
|
20
|
+
requestsPerSecond: Schema.Number,
|
|
21
|
+
cacheHitRatePercent: Schema.Number,
|
|
22
22
|
})
|
|
23
23
|
export type Stats = typeof Stats.Type
|
|
24
24
|
|
|
@@ -4,9 +4,9 @@ import {
|
|
|
4
4
|
Duration,
|
|
5
5
|
Effect,
|
|
6
6
|
HashMap,
|
|
7
|
-
Match
|
|
7
|
+
Match,
|
|
8
8
|
Option,
|
|
9
|
-
Schema
|
|
9
|
+
Schema,
|
|
10
10
|
Stream,
|
|
11
11
|
pipe,
|
|
12
12
|
} from 'effect'
|
|
@@ -32,36 +32,39 @@ export const TABS_ID = 'api-cache-tabs'
|
|
|
32
32
|
|
|
33
33
|
// MODEL
|
|
34
34
|
|
|
35
|
-
const FetchedPosts =
|
|
35
|
+
const FetchedPosts = Schema.Struct({
|
|
36
|
+
posts: Schema.Array(Post),
|
|
37
|
+
fetchedAt: Schema.Number,
|
|
38
|
+
})
|
|
36
39
|
|
|
37
|
-
const FetchedPostDetail =
|
|
40
|
+
const FetchedPostDetail = Schema.Struct({
|
|
38
41
|
detail: PostDetail,
|
|
39
|
-
fetchedAt:
|
|
42
|
+
fetchedAt: Schema.Number,
|
|
40
43
|
})
|
|
41
44
|
|
|
42
|
-
const FetchedStats =
|
|
45
|
+
const FetchedStats = Schema.Struct({ stats: Stats, fetchedAt: Schema.Number })
|
|
43
46
|
|
|
44
|
-
export const PostsData = AsyncData.Schema(FetchedPosts,
|
|
45
|
-
export const PostDetailData = AsyncData.Schema(FetchedPostDetail,
|
|
46
|
-
export const StatsData = AsyncData.Schema(FetchedStats,
|
|
47
|
+
export const PostsData = AsyncData.Schema(FetchedPosts, Schema.String)
|
|
48
|
+
export const PostDetailData = AsyncData.Schema(FetchedPostDetail, Schema.String)
|
|
49
|
+
export const StatsData = AsyncData.Schema(FetchedStats, Schema.String)
|
|
47
50
|
|
|
48
51
|
type PostsData = typeof PostsData.schema.Type
|
|
49
52
|
type PostDetailData = typeof PostDetailData.schema.Type
|
|
50
53
|
type StatsData = typeof StatsData.schema.Type
|
|
51
54
|
|
|
52
|
-
const Tab =
|
|
55
|
+
const Tab = Schema.Literals(['Posts', 'Stats'])
|
|
53
56
|
type Tab = typeof Tab.Type
|
|
54
57
|
|
|
55
58
|
const tabValues: ReadonlyArray<Tab> = Tab.literals
|
|
56
59
|
|
|
57
60
|
export const AppTabs = Tabs.create<Tab>()
|
|
58
61
|
|
|
59
|
-
export const Model =
|
|
62
|
+
export const Model = Schema.Struct({
|
|
60
63
|
tabs: Tabs.Model,
|
|
61
64
|
activeTab: Tab,
|
|
62
65
|
posts: PostsData.schema,
|
|
63
|
-
postDetailById:
|
|
64
|
-
maybeSelectedPostId:
|
|
66
|
+
postDetailById: Schema.HashMap(Schema.String, PostDetailData.schema),
|
|
67
|
+
maybeSelectedPostId: Schema.Option(Schema.String),
|
|
65
68
|
stats: StatsData.schema,
|
|
66
69
|
})
|
|
67
70
|
export type Model = typeof Model.Type
|
|
@@ -70,20 +73,20 @@ export type Model = typeof Model.Type
|
|
|
70
73
|
|
|
71
74
|
export const Message = defineMessageUnion({
|
|
72
75
|
GotTabsMessage: { message: Tabs.Message },
|
|
73
|
-
ClickedPost: { postId:
|
|
76
|
+
ClickedPost: { postId: Schema.String },
|
|
74
77
|
ClickedBackToPosts: {},
|
|
75
78
|
ClickedInvalidatePosts: {},
|
|
76
79
|
ClickedRetryPosts: {},
|
|
77
|
-
ClickedRetryPostDetail: { postId:
|
|
80
|
+
ClickedRetryPostDetail: { postId: Schema.String },
|
|
78
81
|
ClickedRefreshStats: {},
|
|
79
82
|
ClickedRetryStats: {},
|
|
80
83
|
TickedRevalidateStats: {},
|
|
81
|
-
SettledFetchPosts: { result:
|
|
84
|
+
SettledFetchPosts: { result: Schema.Result(FetchedPosts, Schema.String) },
|
|
82
85
|
SettledFetchPostDetail: {
|
|
83
|
-
postId:
|
|
84
|
-
result:
|
|
86
|
+
postId: Schema.String,
|
|
87
|
+
result: Schema.Result(FetchedPostDetail, Schema.String),
|
|
85
88
|
},
|
|
86
|
-
SettledFetchStats: { result:
|
|
89
|
+
SettledFetchStats: { result: Schema.Result(FetchedStats, Schema.String) },
|
|
87
90
|
})
|
|
88
91
|
|
|
89
92
|
export type Message = typeof Message.Type
|
|
@@ -122,33 +125,33 @@ const setPostDetail = (postId: string, postDetail: PostDetailData) =>
|
|
|
122
125
|
const activateTab = (model: Model, tab: Tab): UpdateReturn => {
|
|
123
126
|
const modelWithActiveTab = evo(model, { activeTab: () => tab })
|
|
124
127
|
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
return Match.value(tab).pipe(
|
|
129
|
+
Match.withReturnType<UpdateReturn>(),
|
|
130
|
+
Match.when('Posts', () =>
|
|
128
131
|
applyPostsTransition(
|
|
129
132
|
modelWithActiveTab,
|
|
130
133
|
AsyncData.loadIfMissing(modelWithActiveTab.posts),
|
|
131
134
|
),
|
|
132
135
|
),
|
|
133
|
-
|
|
136
|
+
Match.when('Stats', () =>
|
|
134
137
|
applyStatsTransition(
|
|
135
138
|
modelWithActiveTab,
|
|
136
139
|
AsyncData.loadIfMissing(modelWithActiveTab.stats),
|
|
137
140
|
),
|
|
138
141
|
),
|
|
139
|
-
|
|
142
|
+
Match.exhaustive,
|
|
140
143
|
)
|
|
141
144
|
}
|
|
142
145
|
|
|
143
|
-
const foldTabsOutMessage =
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
)
|
|
146
|
+
const foldTabsOutMessage = Tabs.OutMessage.match<
|
|
147
|
+
Update.Step<Model, Message>,
|
|
148
|
+
Tabs.OutMessage<Tab>
|
|
149
|
+
>({
|
|
150
|
+
Selected:
|
|
151
|
+
({ value }) =>
|
|
152
|
+
model =>
|
|
153
|
+
activateTab(model, value),
|
|
154
|
+
})
|
|
152
155
|
|
|
153
156
|
const foldTabs = Update.foldChild({
|
|
154
157
|
update: AppTabs.update,
|
|
@@ -249,7 +252,7 @@ export const FetchPosts = Command.define('FetchPosts', {
|
|
|
249
252
|
})
|
|
250
253
|
|
|
251
254
|
export const FetchPostDetail = Command.define('FetchPostDetail', {
|
|
252
|
-
args: { postId:
|
|
255
|
+
args: { postId: Schema.String },
|
|
253
256
|
messages: [Message.SettledFetchPostDetail],
|
|
254
257
|
execute: ({ postId }) =>
|
|
255
258
|
pipe(
|
|
@@ -280,7 +283,7 @@ export const FetchStats = Command.define('FetchStats', {
|
|
|
280
283
|
|
|
281
284
|
export const subscriptions = Subscription.make<Model, Message>()(entry => ({
|
|
282
285
|
revalidateStats: entry(
|
|
283
|
-
{ isObservingStats:
|
|
286
|
+
{ isObservingStats: Schema.Boolean },
|
|
284
287
|
{
|
|
285
288
|
modelToDependencies: model => ({
|
|
286
289
|
isObservingStats:
|
|
@@ -350,10 +353,10 @@ export const view = (model: Model, h: HtmlBuilder<Message>): Document => ({
|
|
|
350
353
|
tabInfo.value,
|
|
351
354
|
[...tabInfo.panel, h.Class('flex flex-col gap-4')],
|
|
352
355
|
[
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
356
|
+
Match.value(tabInfo.value).pipe(
|
|
357
|
+
Match.when('Posts', () => postsTabView(model, h)),
|
|
358
|
+
Match.when('Stats', () => statsTabView(model, h)),
|
|
359
|
+
Match.exhaustive,
|
|
357
360
|
),
|
|
358
361
|
],
|
|
359
362
|
),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Console, Effect, Schema
|
|
1
|
+
import { Console, Effect, Schema } from 'effect'
|
|
2
2
|
import { KeyValueStore } from 'effect/unstable/persistence'
|
|
3
3
|
import { Command } from 'foldkit'
|
|
4
4
|
|
|
@@ -16,7 +16,7 @@ export const SaveSession = Command.define('SaveSession', {
|
|
|
16
16
|
const store = yield* KeyValueStore.KeyValueStore
|
|
17
17
|
yield* store.set(
|
|
18
18
|
SESSION_STORAGE_KEY,
|
|
19
|
-
|
|
19
|
+
Schema.encodeSync(SessionJsonString)(session),
|
|
20
20
|
)
|
|
21
21
|
return Message.SucceededSaveSession()
|
|
22
22
|
}).pipe(
|
|
@@ -42,7 +42,7 @@ export const ClearSession = Command.define('ClearSession', {
|
|
|
42
42
|
})
|
|
43
43
|
|
|
44
44
|
export const LogError = Command.define('LogError', {
|
|
45
|
-
args: { entries:
|
|
45
|
+
args: { entries: Schema.Array(Schema.Unknown) },
|
|
46
46
|
messages: [Message.CompletedLogError],
|
|
47
47
|
execute: ({ entries }) =>
|
|
48
48
|
Console.error(...entries).pipe(Effect.as(Message.CompletedLogError())),
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { Schema
|
|
1
|
+
import { Schema } from 'effect'
|
|
2
2
|
|
|
3
|
-
export const Session =
|
|
4
|
-
userId:
|
|
5
|
-
email:
|
|
6
|
-
name:
|
|
3
|
+
export const Session = Schema.Struct({
|
|
4
|
+
userId: Schema.String,
|
|
5
|
+
email: Schema.String,
|
|
6
|
+
name: Schema.String,
|
|
7
7
|
})
|
|
8
8
|
|
|
9
9
|
export type Session = typeof Session.Type
|
|
10
10
|
|
|
11
|
-
export const SessionJsonString =
|
|
11
|
+
export const SessionJsonString = Schema.fromJsonString(
|
|
12
|
+
Schema.toCodecJson(Session),
|
|
13
|
+
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, Match
|
|
1
|
+
import { Effect, Match, Option, Schema } from 'effect'
|
|
2
2
|
import { KeyValueStore } from 'effect/unstable/persistence'
|
|
3
3
|
import { Runtime, type Update } from 'foldkit'
|
|
4
4
|
import { Url } from 'foldkit/url'
|
|
@@ -14,8 +14,8 @@ import { RedirectToDashboard, RedirectToLogin } from './update'
|
|
|
14
14
|
|
|
15
15
|
// FLAGS
|
|
16
16
|
|
|
17
|
-
export const Flags =
|
|
18
|
-
maybeSession:
|
|
17
|
+
export const Flags = Schema.Struct({
|
|
18
|
+
maybeSession: Schema.Option(Session),
|
|
19
19
|
})
|
|
20
20
|
|
|
21
21
|
export const flags: Effect.Effect<Flags> = Effect.gen(function* () {
|
|
@@ -24,7 +24,7 @@ export const flags: Effect.Effect<Flags> = Effect.gen(function* () {
|
|
|
24
24
|
Option.fromNullishOr(yield* store.get(SESSION_STORAGE_KEY)),
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
-
const decodeSession =
|
|
27
|
+
const decodeSession = Schema.decodeEffect(SessionJsonString)
|
|
28
28
|
const session = yield* decodeSession(sessionJson)
|
|
29
29
|
|
|
30
30
|
return Flags.make({ maybeSession: Option.some(session) })
|
|
@@ -40,7 +40,7 @@ export type Flags = typeof Flags.Type
|
|
|
40
40
|
// INIT
|
|
41
41
|
|
|
42
42
|
type InitReturn = Update.Return<Model, Message>
|
|
43
|
-
const withInitReturn =
|
|
43
|
+
const withInitReturn = Match.withReturnType<InitReturn>()
|
|
44
44
|
|
|
45
45
|
export const init: Runtime.RoutingApplicationInit<Model, Message, Flags> = (
|
|
46
46
|
flags: Flags,
|
|
@@ -50,24 +50,24 @@ export const init: Runtime.RoutingApplicationInit<Model, Message, Flags> = (
|
|
|
50
50
|
|
|
51
51
|
return Option.match(flags.maybeSession, {
|
|
52
52
|
onNone: () =>
|
|
53
|
-
|
|
53
|
+
Match.value(route).pipe(
|
|
54
54
|
withInitReturn,
|
|
55
|
-
|
|
55
|
+
Match.tag('Home', 'Login', 'NotFound', route => ({
|
|
56
56
|
model: LoggedOut.init(route),
|
|
57
57
|
})),
|
|
58
|
-
|
|
58
|
+
Match.orElse(() => ({
|
|
59
59
|
model: LoggedOut.init(AppRoute.Login()),
|
|
60
60
|
commands: [RedirectToLogin()],
|
|
61
61
|
})),
|
|
62
62
|
),
|
|
63
63
|
|
|
64
64
|
onSome: session =>
|
|
65
|
-
|
|
65
|
+
Match.value(route).pipe(
|
|
66
66
|
withInitReturn,
|
|
67
|
-
|
|
67
|
+
Match.tag('Dashboard', 'Settings', 'NotFound', route => ({
|
|
68
68
|
model: LoggedIn.init(route, session),
|
|
69
69
|
})),
|
|
70
|
-
|
|
70
|
+
Match.orElse(() => ({
|
|
71
71
|
model: LoggedIn.init(AppRoute.Dashboard(), session),
|
|
72
72
|
commands: [RedirectToDashboard()],
|
|
73
73
|
})),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Schema
|
|
1
|
+
import { Schema } from 'effect'
|
|
2
2
|
import { defineMessageUnion } from 'foldkit/message'
|
|
3
3
|
import { UrlRequest } from 'foldkit/navigation'
|
|
4
4
|
import { Url } from 'foldkit/url'
|
|
@@ -12,9 +12,9 @@ export const Message = defineMessageUnion({
|
|
|
12
12
|
ClickedLink: { request: UrlRequest },
|
|
13
13
|
ChangedUrl: { url: Url },
|
|
14
14
|
SucceededSaveSession: {},
|
|
15
|
-
FailedSaveSession: { error:
|
|
15
|
+
FailedSaveSession: { error: Schema.String },
|
|
16
16
|
SucceededClearSession: {},
|
|
17
|
-
FailedClearSession: { error:
|
|
17
|
+
FailedClearSession: { error: Schema.String },
|
|
18
18
|
GotLoggedOutMessage: { message: LoggedOut.Message },
|
|
19
19
|
GotLoggedInMessage: { message: LoggedIn.Message },
|
|
20
20
|
})
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Schema
|
|
1
|
+
import { Schema } from 'effect'
|
|
2
2
|
|
|
3
3
|
import { LoggedIn, LoggedOut } from './page'
|
|
4
4
|
|
|
5
|
-
export const Model =
|
|
5
|
+
export const Model = Schema.Union([LoggedOut.Model, LoggedIn.Model])
|
|
6
6
|
|
|
7
7
|
export type Model = typeof Model.Type
|
|
8
8
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { clsx } from 'clsx'
|
|
2
|
-
import { Match
|
|
2
|
+
import { Match } from 'effect'
|
|
3
3
|
import { Submodel } from 'foldkit'
|
|
4
4
|
import { Html, HtmlBuilder } from 'foldkit/html'
|
|
5
5
|
|
|
@@ -72,8 +72,8 @@ export const view = Submodel.defineView<Model, Message>((model, h) =>
|
|
|
72
72
|
h.main(
|
|
73
73
|
[h.Class('py-8')],
|
|
74
74
|
[
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
Match.value(model.route).pipe(
|
|
76
|
+
Match.tagsExhaustive({
|
|
77
77
|
Dashboard: () => Dashboard.view(model.session, h),
|
|
78
78
|
Settings: () => Settings.view(model.session, h),
|
|
79
79
|
NotFound: ({ path }) =>
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import { clsx } from 'clsx'
|
|
2
|
-
import {
|
|
3
|
-
Array,
|
|
4
|
-
Duration,
|
|
5
|
-
Effect,
|
|
6
|
-
Option,
|
|
7
|
-
Schema as S,
|
|
8
|
-
String,
|
|
9
|
-
pipe,
|
|
10
|
-
} from 'effect'
|
|
2
|
+
import { Array, Duration, Effect, Option, Schema, String, pipe } from 'effect'
|
|
11
3
|
import { Command, FieldValidation, Submodel, type Update } from 'foldkit'
|
|
12
4
|
import {
|
|
13
5
|
Field,
|
|
@@ -29,10 +21,10 @@ import { homeRouter } from '../../../route'
|
|
|
29
21
|
|
|
30
22
|
// MODEL
|
|
31
23
|
|
|
32
|
-
export const Model =
|
|
33
|
-
email: Field(
|
|
34
|
-
password: Field(
|
|
35
|
-
isSubmitting:
|
|
24
|
+
export const Model = Schema.Struct({
|
|
25
|
+
email: Field(Schema.String),
|
|
26
|
+
password: Field(Schema.String),
|
|
27
|
+
isSubmitting: Schema.Boolean,
|
|
36
28
|
})
|
|
37
29
|
|
|
38
30
|
export type Model = typeof Model.Type
|
|
@@ -46,11 +38,11 @@ export const initModel = (): Model => ({
|
|
|
46
38
|
// MESSAGE
|
|
47
39
|
|
|
48
40
|
export const Message = defineMessageUnion({
|
|
49
|
-
ChangedEmail: { value:
|
|
50
|
-
ChangedPassword: { value:
|
|
41
|
+
ChangedEmail: { value: Schema.String },
|
|
42
|
+
ChangedPassword: { value: Schema.String },
|
|
51
43
|
SubmittedForm: {},
|
|
52
44
|
SucceededSimulateAuthRequest: { session: Session },
|
|
53
|
-
FailedSimulateAuthRequest: { error:
|
|
45
|
+
FailedSimulateAuthRequest: { error: Schema.String },
|
|
54
46
|
})
|
|
55
47
|
|
|
56
48
|
export type Message = typeof Message.Type
|
|
@@ -86,7 +78,7 @@ const isFormValid = (model: Model): boolean =>
|
|
|
86
78
|
// UPDATE
|
|
87
79
|
|
|
88
80
|
export const SimulateAuthRequest = Command.define('SimulateAuthRequest', {
|
|
89
|
-
args: { email:
|
|
81
|
+
args: { email: Schema.String, password: Schema.String },
|
|
90
82
|
messages: [
|
|
91
83
|
Message.SucceededSimulateAuthRequest,
|
|
92
84
|
Message.FailedSimulateAuthRequest,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Option } from 'effect'
|
|
2
2
|
import { Update } from 'foldkit'
|
|
3
3
|
import { evo } from 'foldkit/struct'
|
|
4
4
|
|
|
@@ -12,11 +12,9 @@ const foldLogin = Update.foldChild({
|
|
|
12
12
|
write: (model, nextLoginModel) =>
|
|
13
13
|
evo(model, { loginModel: () => nextLoginModel }),
|
|
14
14
|
toParentMessage: message => Message.GotLoginMessage({ message }),
|
|
15
|
-
toParentOutMessage:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}),
|
|
19
|
-
),
|
|
15
|
+
toParentOutMessage: Login.OutMessage.match<OutMessage>({
|
|
16
|
+
SucceededLogin: ({ session }) => OutMessage.SucceededLogin({ session }),
|
|
17
|
+
}),
|
|
20
18
|
})
|
|
21
19
|
|
|
22
20
|
export const update = (model: Model, message: Message) =>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Match
|
|
1
|
+
import { Match } from 'effect'
|
|
2
2
|
import { Submodel } from 'foldkit'
|
|
3
3
|
|
|
4
4
|
import { notFoundView } from '../../notFoundView'
|
|
@@ -12,8 +12,8 @@ export const view = Submodel.defineView<Model, Message>((model, h) =>
|
|
|
12
12
|
h.div(
|
|
13
13
|
[h.Class('py-8')],
|
|
14
14
|
[
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
Match.value(model.route).pipe(
|
|
16
|
+
Match.tagsExhaustive({
|
|
17
17
|
Home: () => Home.view(h),
|
|
18
18
|
Login: () =>
|
|
19
19
|
h.submodel({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Schema
|
|
1
|
+
import { Schema, pipe } from 'effect'
|
|
2
2
|
import { Route } from 'foldkit'
|
|
3
3
|
import { defineRouteUnion, literal } from 'foldkit/route'
|
|
4
4
|
|
|
@@ -7,7 +7,7 @@ export const AppRoute = defineRouteUnion({
|
|
|
7
7
|
Login: {},
|
|
8
8
|
Dashboard: {},
|
|
9
9
|
Settings: {},
|
|
10
|
-
NotFound: { path:
|
|
10
|
+
NotFound: { path: Schema.String },
|
|
11
11
|
})
|
|
12
12
|
|
|
13
13
|
export type AppRoute = typeof AppRoute.Type
|