create-foldkit-app 0.10.1 → 0.10.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-foldkit-app",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "Create Foldkit applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,21 +1,33 @@
1
1
  # Agent Development Notes
2
2
 
3
- This is a Foldkit app a framework built on Effect-TS.
3
+ This is a Foldkit app, a framework built on Effect-TS, architected like Elm.
4
4
 
5
- If `./repos/foldkit` exists, it contains the full Foldkit repository, vendored in as a git subtree. The `examples/` directory and `packages/typing-game/client/src/` in particular are the best reference for what idiomatic Foldkit apps look like. Treat the subtree as read-only reference: never import from `repos/foldkit/` in your project source. Imports must come from the `foldkit` npm package.
5
+ ## Getting the canonical references
6
+
7
+ The live Foldkit code is the canonical reference for everything: which APIs exist, what idiomatic apps look like, what the current conventions are. Written docs (including this one) can drift; the code can't.
8
+
9
+ If `./repos/foldkit` exists, it contains the full Foldkit repository, vendored in as a git subtree. Read from it directly:
10
+
11
+ - `repos/foldkit/examples/`: runnable example apps spanning every complexity tier. Usually your first stop when looking for a precedent for how to write something.
12
+ - `repos/foldkit/packages/foldkit/src/`: framework source. Ground truth for API signatures.
13
+ - `repos/foldkit/packages/typing-game/client/src/` and `repos/foldkit/packages/website/src/`: production apps built with Foldkit. Highest-fidelity reference for application architecture, Submodels, and OutMessage.
14
+
15
+ Treat the subtree as read-only reference: never import from `repos/foldkit/` in your project source. Imports must come from the `foldkit` npm package.
6
16
 
7
17
  If `./repos/foldkit` does not exist and the line below says `subtree_prompted: false`, offer to run `git subtree add --prefix=repos/foldkit https://github.com/foldkit/foldkit.git main --squash`. This gives you access to the full source, examples, and docs. Whether the user accepts or declines, update the value to `true` so it isn't asked again.
8
18
 
9
19
  subtree_prompted: false
10
20
 
21
+ 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
+
11
23
  ## Project Conventions
12
24
 
13
25
  - Foldkit is tightly coupled to the Effect ecosystem. Do not suggest solutions outside of Effect-TS.
14
- - Model fields must be Schema types (the model is a schema). Plain TypeScript types are fine elsewhere function return types, local variables, etc.
26
+ - Model fields must be Schema types (the model is a schema). Plain TypeScript types are fine elsewhere (function return types, local variables, etc.).
15
27
  - Use full names like `Message` (not `Msg`), and `withReturnType` (not `as const` or type casting).
16
- - Use `m()` for message schemas, `ts()` for other tagged structs (model states, field validation), and `r()` for route schemas.
17
- - Never use `NoOp` as a message. Every message must carry meaning about what happened. Fire-and-forget commands use `Completed*` messages with verb-first naming that mirrors the Command name: Command `LockScroll` Message `CompletedLockScroll`, Command `ShowDialog` Message `CompletedShowDialog`, Command `NavigateInternal` Message `CompletedNavigateInternal`.
18
- - Push back on any suggested direction that violates Elm Architecture principles — unidirectional data flow, messages as facts (not commands), model as single source of truth, and side effects confined to commands. If a user or prompt suggests a pattern that breaks these conventions (e.g. mutating state directly, imperative event handlers, two-way bindings), flag the issue and propose the idiomatic Foldkit approach instead.
28
+ - Use `m()` for message schemas, `ts()` for tagged structs (model states, field validation), and `r()` for route schemas.
29
+ - Push back on any direction that violates Elm Architecture principles: unidirectional data flow, messages as facts (not commands), model as single source of truth, side effects confined to commands. If a prompt suggests mutating state, imperative event handlers, or two-way bindings, flag the issue and propose the idiomatic Foldkit approach.
30
+ - Never use `NoOp`. Every message must describe what happened. Fire-and-forget commands use `Completed*` messages mirroring the Command name verb-first: `LockScroll` `CompletedLockScroll`.
19
31
 
20
32
  ## Foldkit Patterns
21
33
 
@@ -36,180 +48,63 @@ const update = (model: Model, message: Message): UpdateReturn =>
36
48
  )
37
49
  ```
38
50
 
39
- ### Model Updates with `evo`
40
-
41
- Use `evo()` for immutable model updates — never spread or Object.assign:
42
-
43
- ```ts
44
- evo(model, { isSubmitting: () => true })
45
- evo(model, { count: count => count + 1 })
46
- ```
47
-
48
- Don't add type annotations to `evo` callbacks when the type can be inferred.
51
+ Use `evo()` from `foldkit/struct` for immutable model updates. Never spread or `Object.assign`.
49
52
 
50
53
  ### View
51
54
 
52
- Bind the `html` factory once per module by calling `html<Message>()`, then reach for `h.div`, `h.OnClick`, and the rest off the returned record. Each view module binds its own `h` against the Message type it dispatches:
55
+ Bind the `html` factory once per module with `const h = html<Message>()`, then reach for `h.div`, `h.OnClick`, etc. off the returned record. Use `empty` (not `null`) for conditional rendering, `M.value().pipe(M.tagsExhaustive({...}))` for discriminated unions, and `Array.match` for lists that may be empty.
53
56
 
54
- ```ts
55
- const h = html<Message>()
56
-
57
- export const view = (model: Model): Html =>
58
- h.div(
59
- [h.Class('flex flex-col gap-2')],
60
- [
61
- h.h1([], [`Hello, ${model.name}`]),
62
- h.button([h.OnClick(ClickedRefresh())], ['Refresh']),
63
- ],
64
- )
65
- ```
66
-
67
- For child views that should be agnostic to their parent, take `ParentMessage` as a function generic and bind `html<ParentMessage>()` inside. The view stays decoupled from any particular parent and composes through the `toParentMessage` callback the parent supplies.
68
-
69
- Use `empty` (not `null`) for conditional rendering. Use `M.value().pipe(M.tagsExhaustive({...}))` for rendering discriminated unions and `Array.match` for rendering lists that may be empty.
70
-
71
- Use `keyed` wrappers whenever the view branches into structurally different layouts based on route or model state. Without keying, the virtual DOM will try to diff one layout into another (e.g. a full-width landing page into a sidebar docs layout), which causes stale DOM, mismatched event handlers, and subtle rendering bugs. Key the outermost container of each layout branch with a stable string (e.g. `keyed('div')('landing', ...)` vs `keyed('div')('docs', ...)`). Within a single layout, key the content area on the route tag (e.g. `keyed('div')(model.route._tag, ...)`) so page transitions replace rather than patch.
57
+ Use `keyed` wrappers whenever the view branches into structurally different layouts based on route or model state. Without keying, the virtual DOM tries to diff one layout into another, causing stale DOM and event handler mismatches.
72
58
 
73
59
  ### Commands
74
60
 
75
- Define Command identities with `Command.define`, passing the result Message schemas after the name result types are required. Always assign definitions to PascalCase constants never use `Command.define` inline in a pipe chain:
76
-
77
- ```ts
78
- const FetchWeather = Command.define(
79
- 'FetchWeather',
80
- SucceededFetchWeather,
81
- FailedFetchWeather,
82
- )
83
-
84
- const fetchWeather = (city: string) =>
85
- Effect.gen(function* () {
86
- // ...
87
- return SucceededFetchWeather({ data })
88
- }).pipe(
89
- Effect.catch(error =>
90
- Effect.succeed(FailedFetchWeather({ error: String(error) })),
91
- ),
92
- FetchWeather,
93
- )
94
- ```
95
-
96
- Commands catch all errors and return Messages — side effects never crash the app. Let TypeScript infer Command return types from the Effect — the result Message schemas passed to `Command.define` constrain the Effect's return type at the type level.
61
+ Define a Command with `Command.define`, which is curried: the first call binds the name (and optionally args + result Message schemas), and the second call binds the Effect. Assign definitions to PascalCase constants. Never inline in pipe chains. Commands catch all errors via `Effect.catch(() => Effect.succeed(FailedX(...)))` so side effects never crash the app. Definitions live colocated with the update function that returns them.
97
62
 
98
- Command definitions live where they're produced colocated with the update function that returns them. Don't centralize all definitions in one file.
63
+ 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.
99
64
 
100
- ### Mount
101
-
102
- For per-element DOM work that needs the live `Element` handle (anchor positioning, portaling an overlay, attaching observers, handing the element to a third-party library), define a Mount with `Mount.define` and attach it to a view element with `OnMount`. The runtime runs the Effect when the element mounts, dispatches its result Message back through `update`, and runs the paired cleanup on unmount.
103
-
104
- ```ts
105
- const CompletedPortalToBody = m('CompletedPortalToBody')
106
-
107
- const PortalToBody = Mount.define('PortalToBody', CompletedPortalToBody)
108
-
109
- const portalToBody = PortalToBody(element =>
110
- Effect.sync(() => {
111
- document.body.appendChild(element)
112
- return {
113
- message: CompletedPortalToBody(),
114
- cleanup: () => element.remove(),
115
- }
116
- }),
117
- )
118
-
119
- // In view:
120
- div([Class('fixed inset-0 bg-black/50'), OnMount(portalToBody)])
121
- ```
122
-
123
- Cleanup is data, paired with setup as a single value. For setup with no cleanup, pass `Function.constVoid`. The `Completed*` Message marks the lifecycle without forcing a meaningful response in update.
124
-
125
- Two rules for Mount, both must hold:
126
-
127
- 1. **The Effect uses the element parameter.** Mount provides the live element handle, and that handle is what makes Mount distinct from Command. If your Effect doesn't read or write the element, pick a different primitive.
128
- 2. **The work is DOM measurement or DOM manipulation on that element.** Read its geometry, mutate its CSS, attach an observer to it, portal it, hand it to a third-party library. Anything else (network, storage, focus-on-transition, scroll lock for the page) belongs in a Command returned from `update`.
129
-
130
- Mount Effects re-run during DevTools time-travel renders. The two rules above keep Mount work inherently replay-safe (read-only measurement, idempotent DOM mutation, paired observer attach + cleanup).
131
-
132
- Don't reach for Mount just because the work happens to coincide with an element appearing. Check what causes the work. If a Message just dispatched (like `Opened`), the cause is the Message, not the element. Use a Command returned from `update`'s handler instead. Example: focusing a search input when its dialog opens. The cause is `Opened`, not the input's existence; return a `FocusInput` Command from the `Opened` handler.
65
+ For DOM operations (focus, scroll, modals, scroll lock), Foldkit ships a `Dom` module. For time, randomness, UUIDs, and delays, use Effect's built-ins directly (`Clock`, `Random`, `Effect.uuid`, `Effect.sleep`). Don't reach for raw `document.querySelector`, `setTimeout`, `Date.now()`, or `Math.random()`.
133
66
 
134
67
  ### File Organization
135
68
 
136
- A Foldkit app lives in two files. `src/main.ts` holds the pure definitions: Model, Messages, init, update, view. `src/entry.ts` imports them and boots the runtime with `Runtime.makeProgram` and `Runtime.run`. `index.html` references `entry.ts`. The split keeps `main.ts` importable from tests without booting a runtime as a side effect. Never call `Runtime.run` from `main.ts`.
69
+ A Foldkit app lives in two files. `src/main.ts` holds the pure definitions (Model, Messages, init, update, view). `src/entry.ts` imports them and boots the runtime with `Runtime.makeProgram` and `Runtime.run`. `index.html` references `entry.ts`. The split keeps `main.ts` importable from tests without booting a runtime as a side effect. Never call `Runtime.run` from `main.ts`.
137
70
 
138
- Use uppercase section headers (`// MODEL`, `// MESSAGE`, `// INIT`, `// UPDATE`, `// COMMAND`, `// VIEW`) to make files easier to skim. These are for wayfinding — they make it clear where things live and where new code should go. Use domain-specific headers too when it helps (e.g. `// PHYSICS`, `// ROUTING`).
139
-
140
- Even after extracting some sections to their own files (e.g. `message.ts`), the remaining file may still benefit from headers. Extract to separate files when it helps with organization.
71
+ Use uppercase section headers (`// MODEL`, `// MESSAGE`, `// INIT`, `// UPDATE`, `// COMMAND`, `// VIEW`) for wayfinding.
141
72
 
142
73
  ### Testing
143
74
 
144
- Test update functions with `foldkit/test`. Since update is pure `(Model, Message) [Model, Commands]` tests run without a runtime, DOM, or side effects.
145
-
146
- Use `Story.story` to chain steps into a readable narrative: set initial Model → send Message → assert → resolve Command → assert again. Use `Scene.scene` for feature-level testing through the view — clicking buttons, typing into inputs, pressing keys — with accessible locators (`Scene.role`, `Scene.label`, `Scene.placeholder`). Every `Command.define` must include result Message schemas so Commands can be resolved in tests.
75
+ Test update functions with `foldkit/test`. Since update is pure, tests run without a runtime, DOM, or side effects. Use `Story.story` for update-level tests (send Messages, assert on Model and Commands) and `Scene.scene` for feature-level testing through the view with accessible locators. If the `repos/foldkit` subtree is available, study the `.story.test.ts` and `.scene.test.ts` files in `repos/foldkit/examples/`.
147
76
 
148
- If the `repos/foldkit` subtree is available, study the `.test.ts` files in `repos/foldkit/examples/` for patterns — they cover simple Command resolution, multi-step interactions, and Submodel OutMessage assertions.
77
+ ## Code Style
149
78
 
150
- ## Debugging with Foldkit DevTools
79
+ - Encode state in discriminated unions, not booleans or nullable fields. `Idle | Loading | Error | Ok`, not `isLoading: boolean`. Make impossible states unrepresentable.
80
+ - Use `Option` instead of `null` or `undefined`. Prefix Option-typed values with `maybe*`. Match with `Option.match`; don't unwrap with `Option.map(...)` + `Option.getOrElse(...)` when you can just match.
81
+ - Use Effect modules over native methods in `pipe` chains (`Array.map`, `String.startsWith`, `Array.findFirst`). Native methods are fine when calling directly on a named variable.
82
+ - Never cast Schema values with `as Type`. Use the callable constructor: `SucceededLogin({ sessionId })`, not `{ _tag: 'SucceededLogin', sessionId } as Message`.
83
+ - Always `Array.isEmptyArray` / `Array.isNonEmptyArray` (not `.length === 0`). Use `Array.match` when handling both empty and non-empty cases.
84
+ - Never use `for` loops or `let` for iteration. Reach for `Array.map`, `Array.filterMap`, `Array.makeBy`, `Array.reduce`.
85
+ - Never use `T[]`. Always `Array<T>` or `ReadonlyArray<T>`.
86
+ - Always use `Effect.Match`, never `switch`.
87
+ - Always use braces for control flow: `if (foo) { return true }`.
88
+ - Don't add inline comments to explain code. Use better names instead. Reserve `// NOTE:` for behavior that would mislead a careful reader.
151
89
 
152
- This project ships with `@foldkit/devtools-mcp` pre-wired. When the dev server is running and the app is open in a browser tab, `foldkit_*` MCP tools are available for inspecting Model, Message history, and time-travel. Reach for them before adding `console.log` whenever the question is about state or Message flow.
90
+ ## Message Layout
153
91
 
154
- If the `foldkit_*` tools aren't visible, see `@foldkit/devtools-mcp` on npm for setup.
92
+ Group all `m()` declarations together with no blank lines between them, then put `S.Union([...])` and `type Message = typeof Message.Type` on adjacent lines:
155
93
 
156
- ## Code Quality Standards
157
-
158
- - Every name should eliminate ambiguity. Prefix Option-typed values with `maybe` (e.g. `maybeSession`). Name functions by their precise effect (e.g. `enqueueMessage` not `addMessage`). A reader should never need to check a type signature to understand what a name refers to.
159
- - Each function should operate at a single abstraction level. Orchestrators delegate to focused helpers — they don't mix coordination with implementation. If a function reads like it's doing two things, extract one.
160
- - Encode state in discriminated unions, not booleans or nullable fields. Use `Idle | Loading | Error | Ok` instead of `isLoading: boolean`. Make impossible states unrepresentable.
161
- - Name messages as verb-first, past-tense events describing what happened (`SubmittedUsernameForm`, `CreatedRoom`, `PressedKey`), not imperative commands. The verb prefix acts as a category marker: `Clicked*` for button presses, `Updated*` for input changes, `Succeeded*`/`Failed*` for command results that can meaningfully fail (e.g. `SucceededFetchWeather`, `FailedFetchWeather`), `Completed*` for fire-and-forget command acknowledgments where the result is uninteresting and the update function is a no-op (e.g. `CompletedLockScroll`, `CompletedShowDialog`, `CompletedNavigateInternal`), `Got*` exclusively for receiving child module results via the OutMessage pattern (e.g. `GotProductsMessage`). Never use `NoOp` — every message must describe what happened. The update function decides what to do — messages are facts.
162
- - Use `Option` instead of `null` or `undefined`. Match explicitly with `Option.match` or chain with `Option.map`/`Option.flatMap`. No `if (x != null)` checks. Prefer `Option.match` over `Option.map` + `Option.getOrElse` — if you're unwrapping at the end, just match.
163
- - Prefer curried, data-last functions that compose in `pipe` chains.
164
- - Every line should serve a purpose. No dead code, no empty catch blocks, no placeholder types, no defensive code for impossible cases.
165
-
166
- ## Code Style Conventions
94
+ ```ts
95
+ const ClickedSubmit = m('ClickedSubmit')
96
+ const UpdatedEmail = m('UpdatedEmail', { value: S.String })
167
97
 
168
- ### Effect-TS Patterns
98
+ const Message = S.Union([ClickedSubmit, UpdatedEmail])
99
+ type Message = typeof Message.Type
100
+ ```
169
101
 
170
- - Prefer `pipe()` for multi-step data flow. Never use `pipe` with a single operation call the function directly instead: `Option.match(value, {...})` not `pipe(value, Option.match({...}))`.
171
- - Use `Effect.gen()` for imperative-style async operations.
172
- - Always use Effect.Match instead of switch.
173
- - Prefer Effect module functions over native methods when available — e.g. `Array.map`, `Array.filter`, `Option.map`, `String.startsWith` from Effect instead of their native equivalents. This includes Effect's `String` module: use `String.includes`, `String.indexOf` (returns `Option<number>`), `String.slice`, `String.startsWith`, `String.replaceAll`, `String.length`, `String.isNonEmpty`, `String.trim` etc. in `pipe` chains. Exception: native `.map`, `.filter`, `.indexOf()`, `.slice()`, etc. are fine when calling directly on a named variable — use Effect's curried, data-last forms in `pipe` chains where they compose naturally.
174
- - Never use `for` loops or `let` for iteration. Use `Array.makeBy` for index-based construction, `Array.range` + `Array.findFirst`/`Array.findLast` for searches, and `Array.filterMap`/`Array.flatMap` for transforms.
175
- - Never cast Schema values with `as Type`. Use callable constructors: `LoginSucceeded({ sessionId })` not `{ _tag: 'LoginSucceeded', sessionId } as Message`.
176
- - Use `Option` for model fields that may be absent — not empty strings or zero values. `loginError: S.Option(S.String)` not `loginError: S.String` with `''` as the "none" state. Use `Option.match` in views to conditionally render.
177
- - Use `Array.take` instead of `.slice(0, n)`.
178
- - Always use `Array.isEmptyArray(foo)` instead of `foo.length === 0`. Use `Array.isNonEmptyArray(foo)` for non-empty checks. When handling both cases, prefer `Array.match`.
102
+ Messages are verb-first past-tense. Common prefixes: `Clicked*`, `Updated*` (input changes and external state updates), `Submitted*`, `Pressed*`, `Selected*`, `Succeeded*` / `Failed*` (paired async results), `Completed*` (fire-and-forget), `Got*` (child OutMessage in the Submodel pattern).
179
103
 
180
- ### Message Layout
104
+ ## Debugging
181
105
 
182
- Message definitions follow a strict layout:
106
+ This project ships with `@foldkit/devtools-mcp` pre-wired. When the dev server is running and the app is open in a browser, `foldkit_*` MCP tools let you inspect Model, Message history, and time-travel. Reach for them before adding `console.log` whenever the question is about state or Message flow.
183
107
 
184
- ```ts
185
- const ClickedSubmit = m('ClickedSubmit')
186
- const ChangedEmail = m('ChangedEmail', { value: S.String })
187
- const CompletedNavigateInternal = m('CompletedNavigateInternal')
188
-
189
- const Message = S.Union([
190
- ClickedSubmit,
191
- ChangedEmail,
192
- CompletedNavigateInternal,
193
- ])
194
- type Message = typeof Message.Type
195
- ```
108
+ ## Going Deeper
196
109
 
197
- 1. **Values** all `m()` declarations, no blank lines between them
198
- 2. **Union + type** — `S.Union([...])` followed by `type Message = typeof Message.Type` on adjacent lines (no blank line between them)
199
-
200
- Use `typeof ClickedSubmit` in type positions to reference a schema value's type.
201
-
202
- ### General Preferences
203
-
204
- - Never abbreviate names. Use full, descriptive names everywhere — variables, types, functions, parameters, including callback parameters. e.g. `signature` not `sig`, `Message` not `Msg`, `(tickCount) => tickCount + 1` not `(t) => t + 1`.
205
- - Don't suffix Command variables with `Command`. Name them by what they do: `focusButton` not `focusButtonCommand`, `scrollToItem` not `scrollToItemCommand`. The type already communicates that it's a Command. Command definitions are PascalCase (`FocusButton`, `ScrollToItem`); Command instances and factory functions are camelCase (`focusButton`, `scrollToItem`).
206
- - Avoid `let`. Use `const` and prefer immutable patterns.
207
- - Always use braces for control flow. `if (foo) { return true }` not `if (foo) return true`.
208
- - Use `is*` for boolean naming e.g. `isPlaying`, `isValid`.
209
- - Don't add inline or block comments to explain code. If code needs explanation, refactor for clarity or use better names. Exceptions: section headers (see File Organization above), TSDoc (`/** ... */`) on public exports, and `// NOTE:` comments. Reserve `// NOTE:` for behavior that would mislead a careful reader into breaking things: a timing dependency that's silent if violated, a workaround for an upstream bug, a browser quirk that costs real debugging time to rediscover. The bar is high. When in doubt, delete it.
210
- - Use capitalized string literals for Schema literal types: `S.Literals(['Horizontal', 'Vertical'])` not `S.Literals(['horizontal', 'vertical'])`.
211
- - Capitalize namespace imports: `import * as Command from './command'` not `import * as command from './command'`.
212
- - Extract magic numbers to named constants. No raw numeric literals in logic.
213
- - Never use `T[]` syntax. Always use `Array<T>` or `ReadonlyArray<T>`.
214
- - For inline object types in `ReadonlyArray`, put `Readonly<{...}>` on the element type rather than `ReadonlyArray<{ readonly a: ...; readonly b: ... }>`. e.g. `ReadonlyArray<Readonly<{ model: Foo; toParentMessage: (m: Bar) => Baz }>>` not `ReadonlyArray<{ readonly model: Foo; readonly toParentMessage: ... }>`.
215
- - Extract repeated inline style values (colors, shadows) to constants. Use Tailwind `@theme` for colors that map to utility classes (e.g. `--color-valentine: #ff2d55` → `text-valentine`). Use a `theme.ts` for values Tailwind can't express as utilities (textShadow, boxShadow).
110
+ For Submodels and OutMessage, Subscriptions, Mount / ManagedResource / CustomElement, field validation, routing, accessibility, and the full convention set, read the live Foldkit code in `repos/foldkit/`. The `examples/` directory and the production apps (`packages/typing-game/`, `packages/website/`) are the highest-fidelity references for any specific pattern. The `foldkit-skills` plugin's `generate-program` and `audit-program` skills carry written snapshot guides if you want a structured walkthrough.