create-foldkit-app 0.10.0 → 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/dist/commands/create.js
CHANGED
|
@@ -81,12 +81,15 @@ const displaySuccessMessage = (name, packageManager) => Effect.gen(function* ()
|
|
|
81
81
|
yield* Console.log('');
|
|
82
82
|
yield* Console.log(chalk.bold('AI-Assisted Development'));
|
|
83
83
|
yield* Console.log('');
|
|
84
|
-
yield* Console.log('
|
|
85
|
-
' reference the source, examples, and documentation
|
|
84
|
+
yield* Console.log(' Vendor Foldkit in as a git subtree so your AI assistant can\n' +
|
|
85
|
+
' reference the source, examples, and documentation. Commit\n' +
|
|
86
|
+
' the scaffold first so subtree has a base commit to merge into:');
|
|
86
87
|
yield* Console.log('');
|
|
87
88
|
yield* Console.log(` > ${chalk.cyan('cd')} ${name}`);
|
|
88
89
|
yield* Console.log(` > ${chalk.cyan('git init')}`);
|
|
89
|
-
yield* Console.log(` > ${chalk.cyan('git
|
|
90
|
+
yield* Console.log(` > ${chalk.cyan('git add .')}`);
|
|
91
|
+
yield* Console.log(` > ${chalk.cyan('git commit -m "chore: initial commit"')}`);
|
|
92
|
+
yield* Console.log(` > ${chalk.cyan('git subtree add --prefix=repos/foldkit https://github.com/foldkit/foldkit.git main --squash')}`);
|
|
90
93
|
yield* Console.log('');
|
|
91
94
|
yield* Console.log(` Details: ${chalk.cyan('foldkit.dev/ai/overview')}`);
|
|
92
95
|
yield* Console.log('');
|
package/package.json
CHANGED
package/templates/base/AGENTS.md
CHANGED
|
@@ -1,21 +1,33 @@
|
|
|
1
1
|
# Agent Development Notes
|
|
2
2
|
|
|
3
|
-
This is a Foldkit app
|
|
3
|
+
This is a Foldkit app, a framework built on Effect-TS, architected like Elm.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Getting the canonical references
|
|
6
6
|
|
|
7
|
-
|
|
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
8
|
|
|
9
|
-
|
|
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.
|
|
16
|
+
|
|
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.
|
|
18
|
+
|
|
19
|
+
subtree_prompted: false
|
|
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.
|
|
10
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
|
|
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
|
|
17
|
-
-
|
|
18
|
-
-
|
|
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,178 +48,63 @@ const update = (model: Model, message: Message): UpdateReturn =>
|
|
|
36
48
|
)
|
|
37
49
|
```
|
|
38
50
|
|
|
39
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
+
Use uppercase section headers (`// MODEL`, `// MESSAGE`, `// INIT`, `// UPDATE`, `// COMMAND`, `// VIEW`) for wayfinding.
|
|
139
72
|
|
|
140
73
|
### Testing
|
|
141
74
|
|
|
142
|
-
Test update functions with `foldkit/test`. Since update is pure
|
|
143
|
-
|
|
144
|
-
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/`.
|
|
145
76
|
|
|
146
|
-
|
|
77
|
+
## Code Style
|
|
147
78
|
|
|
148
|
-
|
|
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.
|
|
149
89
|
|
|
150
|
-
|
|
90
|
+
## Message Layout
|
|
151
91
|
|
|
152
|
-
|
|
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:
|
|
153
93
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
- 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.
|
|
158
|
-
- Encode state in discriminated unions, not booleans or nullable fields. Use `Idle | Loading | Error | Ok` instead of `isLoading: boolean`. Make impossible states unrepresentable.
|
|
159
|
-
- 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.
|
|
160
|
-
- 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.
|
|
161
|
-
- Prefer curried, data-last functions that compose in `pipe` chains.
|
|
162
|
-
- Every line should serve a purpose. No dead code, no empty catch blocks, no placeholder types, no defensive code for impossible cases.
|
|
163
|
-
|
|
164
|
-
## Code Style Conventions
|
|
94
|
+
```ts
|
|
95
|
+
const ClickedSubmit = m('ClickedSubmit')
|
|
96
|
+
const UpdatedEmail = m('UpdatedEmail', { value: S.String })
|
|
165
97
|
|
|
166
|
-
|
|
98
|
+
const Message = S.Union([ClickedSubmit, UpdatedEmail])
|
|
99
|
+
type Message = typeof Message.Type
|
|
100
|
+
```
|
|
167
101
|
|
|
168
|
-
|
|
169
|
-
- Use `Effect.gen()` for imperative-style async operations.
|
|
170
|
-
- Always use Effect.Match instead of switch.
|
|
171
|
-
- 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.
|
|
172
|
-
- 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.
|
|
173
|
-
- Never cast Schema values with `as Type`. Use callable constructors: `LoginSucceeded({ sessionId })` not `{ _tag: 'LoginSucceeded', sessionId } as Message`.
|
|
174
|
-
- 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.
|
|
175
|
-
- Use `Array.take` instead of `.slice(0, n)`.
|
|
176
|
-
- 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).
|
|
177
103
|
|
|
178
|
-
|
|
104
|
+
## Debugging
|
|
179
105
|
|
|
180
|
-
|
|
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.
|
|
181
107
|
|
|
182
|
-
|
|
183
|
-
const ClickedSubmit = m('ClickedSubmit')
|
|
184
|
-
const ChangedEmail = m('ChangedEmail', { value: S.String })
|
|
185
|
-
const CompletedNavigateInternal = m('CompletedNavigateInternal')
|
|
186
|
-
|
|
187
|
-
const Message = S.Union([
|
|
188
|
-
ClickedSubmit,
|
|
189
|
-
ChangedEmail,
|
|
190
|
-
CompletedNavigateInternal,
|
|
191
|
-
])
|
|
192
|
-
type Message = typeof Message.Type
|
|
193
|
-
```
|
|
108
|
+
## Going Deeper
|
|
194
109
|
|
|
195
|
-
|
|
196
|
-
2. **Union + type** — `S.Union([...])` followed by `type Message = typeof Message.Type` on adjacent lines (no blank line between them)
|
|
197
|
-
|
|
198
|
-
Use `typeof ClickedSubmit` in type positions to reference a schema value's type.
|
|
199
|
-
|
|
200
|
-
### General Preferences
|
|
201
|
-
|
|
202
|
-
- 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`.
|
|
203
|
-
- 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`).
|
|
204
|
-
- Avoid `let`. Use `const` and prefer immutable patterns.
|
|
205
|
-
- Always use braces for control flow. `if (foo) { return true }` not `if (foo) return true`.
|
|
206
|
-
- Use `is*` for boolean naming e.g. `isPlaying`, `isValid`.
|
|
207
|
-
- 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.
|
|
208
|
-
- Use capitalized string literals for Schema literal types: `S.Literals(['Horizontal', 'Vertical'])` not `S.Literals(['horizontal', 'vertical'])`.
|
|
209
|
-
- Capitalize namespace imports: `import * as Command from './command'` not `import * as command from './command'`.
|
|
210
|
-
- Extract magic numbers to named constants. No raw numeric literals in logic.
|
|
211
|
-
- Never use `T[]` syntax. Always use `Array<T>` or `ReadonlyArray<T>`.
|
|
212
|
-
- 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: ... }>`.
|
|
213
|
-
- 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.
|