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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-foldkit-app",
3
- "version": "0.7.0",
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.2",
19
- "typescript": "^6.0.2"
18
+ "rimraf": "^6.1.3",
19
+ "typescript": "^6.0.3"
20
20
  },
21
21
  "keywords": [
22
22
  "create-foldkit-app",
@@ -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
- Call `html<Message>()` once in a dedicated `html.ts` file and import the destructured helpers everywhere else:
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
- // html.ts
56
- export const { div, button, span, Class, OnClick } = html<Message>()
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 focusing an input, handing the live `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.
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 CompletedFocusInput = m('CompletedFocusInput')
105
+ const CompletedPortalToBody = m('CompletedPortalToBody')
96
106
 
97
- const FocusInput = Mount.define('FocusInput', CompletedFocusInput)
107
+ const PortalToBody = Mount.define('PortalToBody', CompletedPortalToBody)
98
108
 
99
- const focusInput = FocusInput(element =>
109
+ const portalToBody = PortalToBody(element =>
100
110
  Effect.sync(() => {
101
- if (element instanceof HTMLInputElement) {
102
- element.focus()
103
- }
111
+ document.body.appendChild(element)
104
112
  return {
105
- message: CompletedFocusInput(),
106
- cleanup: Function.constVoid,
113
+ message: CompletedPortalToBody(),
114
+ cleanup: () => element.remove(),
107
115
  }
108
116
  }),
109
117
  )
110
118
 
111
119
  // In view:
112
- input([Type('search'), OnMount(focusInput)])
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 if code needs explanation, refactor for clarity or use better names. Exceptions: section headers (see File Organization above) and TSDoc (`/** ... */`) on public exports.
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.