create-foldkit-app 0.7.0 → 0.7.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 +3 -3
- package/templates/base/AGENTS.md +31 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-foldkit-app",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Create Foldkit applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"@effect/platform-node": "4.0.0-beta.59",
|
|
16
16
|
"chalk": "^5.6.2",
|
|
17
17
|
"effect": "4.0.0-beta.59",
|
|
18
|
-
"rimraf": "^6.1.
|
|
19
|
-
"typescript": "^6.0.
|
|
18
|
+
"rimraf": "^6.1.3",
|
|
19
|
+
"typescript": "^6.0.3"
|
|
20
20
|
},
|
|
21
21
|
"keywords": [
|
|
22
22
|
"create-foldkit-app",
|
package/templates/base/AGENTS.md
CHANGED
|
@@ -49,13 +49,23 @@ Don't add type annotations to `evo` callbacks when the type can be inferred.
|
|
|
49
49
|
|
|
50
50
|
### View
|
|
51
51
|
|
|
52
|
-
|
|
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:
|
|
53
53
|
|
|
54
54
|
```ts
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
)
|
|
57
65
|
```
|
|
58
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
|
+
|
|
59
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.
|
|
60
70
|
|
|
61
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.
|
|
@@ -89,31 +99,38 @@ Command definitions live where they're produced — colocated with the update fu
|
|
|
89
99
|
|
|
90
100
|
### Mount
|
|
91
101
|
|
|
92
|
-
For per-element DOM work
|
|
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.
|
|
93
103
|
|
|
94
104
|
```ts
|
|
95
|
-
const
|
|
105
|
+
const CompletedPortalToBody = m('CompletedPortalToBody')
|
|
96
106
|
|
|
97
|
-
const
|
|
107
|
+
const PortalToBody = Mount.define('PortalToBody', CompletedPortalToBody)
|
|
98
108
|
|
|
99
|
-
const
|
|
109
|
+
const portalToBody = PortalToBody(element =>
|
|
100
110
|
Effect.sync(() => {
|
|
101
|
-
|
|
102
|
-
element.focus()
|
|
103
|
-
}
|
|
111
|
+
document.body.appendChild(element)
|
|
104
112
|
return {
|
|
105
|
-
message:
|
|
106
|
-
cleanup:
|
|
113
|
+
message: CompletedPortalToBody(),
|
|
114
|
+
cleanup: () => element.remove(),
|
|
107
115
|
}
|
|
108
116
|
}),
|
|
109
117
|
)
|
|
110
118
|
|
|
111
119
|
// In view:
|
|
112
|
-
|
|
120
|
+
div([Class('fixed inset-0 bg-black/50'), OnMount(portalToBody)])
|
|
113
121
|
```
|
|
114
122
|
|
|
115
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.
|
|
116
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.
|
|
133
|
+
|
|
117
134
|
### File Organization
|
|
118
135
|
|
|
119
136
|
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`).
|
|
@@ -187,7 +204,7 @@ Use `typeof ClickedSubmit` in type positions to reference a schema value's type.
|
|
|
187
204
|
- Avoid `let`. Use `const` and prefer immutable patterns.
|
|
188
205
|
- Always use braces for control flow. `if (foo) { return true }` not `if (foo) return true`.
|
|
189
206
|
- Use `is*` for boolean naming e.g. `isPlaying`, `isValid`.
|
|
190
|
-
- Don't add inline or block comments to explain code
|
|
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.
|
|
191
208
|
- Use capitalized string literals for Schema literal types: `S.Literals(['Horizontal', 'Vertical'])` not `S.Literals(['horizontal', 'vertical'])`.
|
|
192
209
|
- Capitalize namespace imports: `import * as Command from './command'` not `import * as command from './command'`.
|
|
193
210
|
- Extract magic numbers to named constants. No raw numeric literals in logic.
|