tutuca 0.11.2 → 0.12.0

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.
Files changed (115) hide show
  1. package/dist/tutuca-cli.js +130 -104
  2. package/dist/tutuca-dev.ext.js +121 -90
  3. package/dist/tutuca-dev.js +121 -90
  4. package/dist/tutuca-dev.min.js +3 -3
  5. package/dist/tutuca-extra.ext.js +116 -59
  6. package/dist/tutuca-extra.js +116 -59
  7. package/dist/tutuca-extra.min.js +2 -2
  8. package/dist/tutuca-storybook.js +3 -3
  9. package/dist/tutuca.ext.js +116 -59
  10. package/dist/tutuca.js +116 -59
  11. package/dist/tutuca.min.js +2 -2
  12. package/package.json +1 -1
  13. package/skill/margaui/SKILL.md +105 -0
  14. package/skill/margaui/components/accordion.md +127 -0
  15. package/skill/margaui/components/alert.md +174 -0
  16. package/skill/margaui/components/aura.md +97 -0
  17. package/skill/margaui/components/avatar.md +220 -0
  18. package/skill/margaui/components/badge.md +193 -0
  19. package/skill/margaui/components/breadcrumbs.md +103 -0
  20. package/skill/margaui/components/button.md +322 -0
  21. package/skill/margaui/components/calendar.md +67 -0
  22. package/skill/margaui/components/card.md +373 -0
  23. package/skill/margaui/components/carousel.md +387 -0
  24. package/skill/margaui/components/chat.md +171 -0
  25. package/skill/margaui/components/checkbox.md +101 -0
  26. package/skill/margaui/components/collapse.md +172 -0
  27. package/skill/margaui/components/countdown.md +165 -0
  28. package/skill/margaui/components/diff.md +53 -0
  29. package/skill/margaui/components/divider.md +107 -0
  30. package/skill/margaui/components/dock.md +173 -0
  31. package/skill/margaui/components/drawer.md +184 -0
  32. package/skill/margaui/components/dropdown.md +388 -0
  33. package/skill/margaui/components/fab.md +346 -0
  34. package/skill/margaui/components/fieldset.md +88 -0
  35. package/skill/margaui/components/file-input.md +84 -0
  36. package/skill/margaui/components/filter.md +52 -0
  37. package/skill/margaui/components/footer.md +583 -0
  38. package/skill/margaui/components/hero.md +135 -0
  39. package/skill/margaui/components/hover-3d.md +129 -0
  40. package/skill/margaui/components/hover-gallery.md +49 -0
  41. package/skill/margaui/components/indicator.md +265 -0
  42. package/skill/margaui/components/input.md +389 -0
  43. package/skill/margaui/components/join.md +100 -0
  44. package/skill/margaui/components/kbd.md +127 -0
  45. package/skill/margaui/components/label.md +102 -0
  46. package/skill/margaui/components/link.md +96 -0
  47. package/skill/margaui/components/list.md +182 -0
  48. package/skill/margaui/components/loading.md +105 -0
  49. package/skill/margaui/components/mask.md +168 -0
  50. package/skill/margaui/components/megamenu.md +131 -0
  51. package/skill/margaui/components/menu.md +887 -0
  52. package/skill/margaui/components/mockup-browser.md +39 -0
  53. package/skill/margaui/components/mockup-code.md +81 -0
  54. package/skill/margaui/components/mockup-phone.md +39 -0
  55. package/skill/margaui/components/mockup-window.md +33 -0
  56. package/skill/margaui/components/modal.md +196 -0
  57. package/skill/margaui/components/navbar.md +282 -0
  58. package/skill/margaui/components/otp.md +171 -0
  59. package/skill/margaui/components/pagination.md +122 -0
  60. package/skill/margaui/components/progress.md +135 -0
  61. package/skill/margaui/components/radial-progress.md +67 -0
  62. package/skill/margaui/components/radio.md +133 -0
  63. package/skill/margaui/components/range.md +134 -0
  64. package/skill/margaui/components/rating.md +170 -0
  65. package/skill/margaui/components/select.md +225 -0
  66. package/skill/margaui/components/skeleton.md +64 -0
  67. package/skill/margaui/components/stack.md +142 -0
  68. package/skill/margaui/components/stat.md +254 -0
  69. package/skill/margaui/components/status.md +73 -0
  70. package/skill/margaui/components/steps.md +138 -0
  71. package/skill/margaui/components/swap.md +152 -0
  72. package/skill/margaui/components/tab.md +248 -0
  73. package/skill/margaui/components/table.md +1018 -0
  74. package/skill/margaui/components/text-rotate.md +91 -0
  75. package/skill/margaui/components/textarea.md +85 -0
  76. package/skill/margaui/components/theme-controller.md +266 -0
  77. package/skill/margaui/components/timeline.md +1356 -0
  78. package/skill/margaui/components/toast.md +165 -0
  79. package/skill/margaui/components/toggle.md +135 -0
  80. package/skill/margaui/components/tooltip.md +181 -0
  81. package/skill/margaui/components/validator.md +163 -0
  82. package/skill/tutuca/SKILL.md +56 -0
  83. package/skill/tutuca/advanced.md +212 -0
  84. package/skill/tutuca/cli.md +239 -0
  85. package/skill/tutuca/component-design.md +168 -0
  86. package/skill/tutuca/core.md +918 -0
  87. package/skill/tutuca/iteration.md +207 -0
  88. package/skill/tutuca/macros.md +86 -0
  89. package/skill/tutuca/margaui.md +175 -0
  90. package/skill/tutuca/messages-and-intents.md +399 -0
  91. package/skill/tutuca/patterns/README.md +48 -0
  92. package/skill/tutuca/patterns/add-a-story.md +26 -0
  93. package/skill/tutuca/patterns/bind-text-and-attributes.md +30 -0
  94. package/skill/tutuca/patterns/conditional-attribute-value.md +29 -0
  95. package/skill/tutuca/patterns/coordinate-components.md +54 -0
  96. package/skill/tutuca/patterns/edit-through-a-dynamic-target.md +27 -0
  97. package/skill/tutuca/patterns/enrich-each-item.md +25 -0
  98. package/skill/tutuca/patterns/file-input.md +39 -0
  99. package/skill/tutuca/patterns/filter-a-list.md +25 -0
  100. package/skill/tutuca/patterns/filter-and-paginate.md +60 -0
  101. package/skill/tutuca/patterns/handle-events.md +44 -0
  102. package/skill/tutuca/patterns/iterate-a-list.md +18 -0
  103. package/skill/tutuca/patterns/paginate-a-list.md +29 -0
  104. package/skill/tutuca/patterns/render-a-child-component.md +21 -0
  105. package/skill/tutuca/patterns/reuse-markup-with-macros.md +36 -0
  106. package/skill/tutuca/patterns/share-state-across-the-tree.md +38 -0
  107. package/skill/tutuca/patterns/show-or-hide-content.md +23 -0
  108. package/skill/tutuca/patterns/switch-between-views.md +30 -0
  109. package/skill/tutuca/patterns/tabbed-interface.md +43 -0
  110. package/skill/tutuca/semantics.md +195 -0
  111. package/skill/tutuca/storybook.md +270 -0
  112. package/skill/tutuca/styles.md +48 -0
  113. package/skill/tutuca/testing.md +345 -0
  114. package/skill/tutuca-source/SKILL.md +33 -0
  115. package/skill/tutuca-source/tutuca.ext.js +4301 -0
@@ -0,0 +1,918 @@
1
+ # Tutuca — Core
2
+
3
+ Tutuca is an immutable-state web framework powered by Immer: components have
4
+ typed `fields`, native JavaScript collections, HTML-template `view`s with
5
+ `@`-prefixed directives, and `receive` / `intent` handlers for
6
+ orchestration. Read this file when authoring or reviewing
7
+ `component({...})` definitions, `view: html\`...\`` templates, macros, or
8
+ the `tutuca` CLI.
9
+
10
+ > Load the topic files only when the task touches them (the routing
11
+ > table in [SKILL.md](./SKILL.md) has the full descriptions):
12
+ > [iteration.md](./iteration.md) · [macros.md](./macros.md) ·
13
+ > [styles.md](./styles.md) · [messages-and-intents.md](./messages-and-intents.md) ·
14
+ > [component-design.md](./component-design.md) · [testing.md](./testing.md) ·
15
+ > [storybook.md](./storybook.md) · [cli.md](./cli.md) ·
16
+ > [semantics.md](./semantics.md) · [advanced.md](./advanced.md) ·
17
+ > [margaui.md](./margaui.md) · [patterns/README.md](./patterns/README.md).
18
+
19
+ ## Verifying changes
20
+
21
+ After editing a Tutuca module, run these checks before declaring the
22
+ edit done:
23
+
24
+ 1. **Lint the module** — catches undefined fields/handlers/macros/events
25
+ (all the `*_NOT_DEFINED` / `*_NOT_REFERENCED` codes):
26
+
27
+ tutuca lint <module-path>
28
+
29
+ Exits `2` on any error-level finding. Pass a component name to scope
30
+ it: `tutuca lint <module-path> Button`.
31
+
32
+ 2. **Test component behavior** — when the edit changes attributes,
33
+ instance methods, receive handlers, or static factories (anything
34
+ observable from JS, not just the rendered HTML), run the test
35
+ suite. The module opts in by exporting
36
+ `getTests({ describe, test, expect })`:
37
+
38
+ tutuca test <module-path>
39
+ tutuca test <module-path> Counter # one component
40
+ tutuca test <module-path> --grep "inc()" # one path
41
+
42
+ Exits `4` on any failure. Skip this step when the change is purely
43
+ templates/styling — `render` already covers that. Authoring patterns
44
+ (handler calling convention, designing handlers for testability,
45
+ worked `getTests` export) in [testing.md](./testing.md); CLI flags
46
+ and exit codes in [cli.md](./cli.md).
47
+
48
+ 3. **Render the example(s) that exercise the feature you changed** —
49
+ confirms the component actually mounts in a headless DOM with the new
50
+ behavior. Pick the example whose `title` matches the feature, or
51
+ filter by component:
52
+
53
+ tutuca render <module-path> --title "Disabled state"
54
+ tutuca render <module-path> Button
55
+
56
+ Exits `3` if any render crashes. If no example covers the feature
57
+ you're adding, add one to `getExamples()` first — that's how the
58
+ feature becomes verifiable. Add `--pretty` when you need to read the
59
+ emitted HTML to verify structure (attributes, nesting, text); omit it
60
+ when you only care that the render didn't crash.
61
+
62
+ 4. **Smoke-test the whole project** — when you've touched several
63
+ `*.dev.js` modules, or are about to launch the storybook, do a
64
+ project-wide dry run instead of opening a browser:
65
+
66
+ tutuca storybook --dry-run
67
+ tutuca storybook --dry-run --json # machine-readable for agents
68
+
69
+ It does everything the server would do up front — discovers every
70
+ co-located `*.dev.js`, imports and normalizes each (catching a missing
71
+ `getComponents()` or a malformed `getExamples()` shape), runs their
72
+ `getTests()`, and resolves the runtime import map — then prints what
73
+ it *would* show instead of serving. A broken module is reported in
74
+ place (an `error` line, or `modules[].error` in `--json`) while the
75
+ others still report, so one bad module never hides the rest. This is
76
+ the fast "is the whole catalog wired up correctly?" check; steps 1–3
77
+ stay the per-module loop.
78
+
79
+ Full reference: [cli.md](./cli.md).
80
+
81
+ The Tutuca CLI only catches Tutuca-specific issues. For generic JS
82
+ problems, pair it with a general linter/formatter — e.g. set up Biome
83
+ once with `npx -y @biomejs/biome init` and use its `lint`, `check`,
84
+ and `format` subcommands. Run `npx @biomejs/biome -h` for usage help.
85
+
86
+ ## Common pitfalls
87
+
88
+ - **`.field` reads a field, `$method` calls a no-arg method.** The two are
89
+ distinct prefixes: `.count` reads field `count`, `$inc` calls method
90
+ `inc`. Using the wrong one is a lint error that tells you to swap the
91
+ prefix.
92
+ - **Paths are not allowed in values.** `.foo` resolves a single field on
93
+ `this` — `@text=".foo.bar"`, `:value=".user.name"`, `@show=".item.isOpen"`
94
+ all fail. To reach into nested data: render the child as a component
95
+ (`<x render=".foo">` then `@text=".bar"` inside), add a method
96
+ (`fullName() { return this.user.name; }` and use `$fullName`), or use
97
+ `@enrich-with` for scope-level derivation. The one exception: a
98
+ **binding** may read exactly one **binding member** —
99
+ `@text="@value.title"` inside `@each` works (any `@`-binding, one level
100
+ only; a binding member read like `@value.a.b` is a lint error, and
101
+ render targets still reject it).
102
+ - **State writes happen on the handler's first `draft` argument.** `this` is
103
+ the frozen pre-transaction snapshot. Arrays, objects, Map, and Set use their
104
+ normal JavaScript mutation APIs on `draft`.
105
+ - **Multiple `@if.<attr>` on one element.** Every `@then`/`@else` after
106
+ the first must name the attr (`@then.title`, `@else.title`) — HTML
107
+ disallows duplicate attrs, so the second `@then=` is dropped silently.
108
+ - **Bare unquoted multi-word strings return `null`.** Either quote
109
+ (`'flex gap-3'`) or use a `$'…'` string template (`$'flex gap-3 {.color}'`).
110
+ - **`<x>` is stripped inside `<select>` / `<table>` / `<tr>`.** Use the
111
+ `@x` pseudo-x trick (see [advanced.md](./advanced.md)).
112
+ - **`receive.init` is a convention, not a lifecycle hook.** Nothing calls it
113
+ automatically — dispatch via `app.sendAtRoot("init")` or from
114
+ another handler.
115
+ - **`app.state.set(...)` takes a component instance**, not plain data.
116
+ Build with `Comp.make({...})`.
117
+ - **`html\`` templates must start with the opening tag.** A leading
118
+ newline / indent before the first element renders blank silently.
119
+ Use `view: html\`<el ...>` (or `html\`<el<newline> attr<newline>>...`),
120
+ never `view: html\`<newline> <el ...>`. Same applies to macro bodies.
121
+ - **Macro registry keys are lowercased.** `<x:Card>` becomes `<x:card>` — see [macros.md](./macros.md).
122
+
123
+ ## Bootstrap
124
+
125
+ ```js
126
+ import { component, html, tutuca } from "tutuca";
127
+
128
+ const Counter = component({
129
+ name: "Counter",
130
+ fields: { count: 0 },
131
+ receive: {
132
+ inc(draft) {
133
+ draft.count++;
134
+ },
135
+ },
136
+ view: html`<button @on.click="inc" @text=".count"></button>`,
137
+ });
138
+
139
+ const app = tutuca("#app");
140
+ app.registerComponents([Counter]);
141
+ app.state.set(Counter.make({}));
142
+ app.start();
143
+ ```
144
+
145
+ `app.onChange((info) => ...)` fires after every state change with
146
+ `{ val, old, info, timestamp }` (logging, persistence). `app.stop()`
147
+ removes all listeners and cancels cache eviction; pair with
148
+ `app.start()` to remount cleanly in tests or SPA navigation.
149
+
150
+ ## Mental model
151
+
152
+ Tutuca rests on three invariants: application state is one deeply frozen root;
153
+ the view is a pure function of it; and every dispatched handler receives an
154
+ Immer draft as its first argument while `this` remains the immutable current
155
+ snapshot. The transactor commits the produced root atomically.
156
+
157
+ **The value tree.** Components are generated Immer-draftable classes. Children
158
+ live in fields — an array of `Item`, a native `Map` of `User`, a scalar `count`.
159
+ "Updating a deep child" means producing a new root that shares
160
+ structure with the old one along the unchanged spine; the renderer
161
+ keys its cache on `===` identity, so unchanged subtrees skip work.
162
+ Every value carries a hidden tag back to its component class, so the
163
+ runtime never needs `instanceof` — it asks the value what it is.
164
+
165
+ Because children are ordinary frozen values held in fields, **handlers
166
+ and methods are ordinary JS with full read access to nested child
167
+ state** — `this.child.count`, `this.items[i].done`,
168
+ `this.byKey.get(k).label`. Reading *down* the tree is direct and needs
169
+ no channel: an ancestor that owns a list already holds every child's
170
+ state and can read it for an aggregate decision. The single-level
171
+ `.field` restriction (no `.foo.bar`) is a **view-template** rule, not a
172
+ JS one — it's why a derivation like `userName() { return this.user.name; }`
173
+ is written as a method (see *Methods as Predicates & Computed Values*).
174
+ Reading is free; **mutating** a child still flows through the model — mutate
175
+ the addressed draft or message the child with `ctx.send`. Don't reach in to
176
+ mutate the frozen snapshot,
177
+ and prefer letting a child own and render its own state — reach down to
178
+ read only when the ancestor genuinely needs it. See
179
+ [component-design.md](./component-design.md) and "When to send" in
180
+ [messages-and-intents.md](./messages-and-intents.md).
181
+
182
+ **Stack: frames vs scopes.** As the renderer walks the AST it pushes
183
+ `BindFrame`s. A *frame* is a barrier: name lookups (`@x`) stop at it,
184
+ so a child component view sees a clean namespace. A *scope* is
185
+ transparent: iteration `key` / `value` and `@enrich-with` binds layer
186
+ onto the surrounding frame and remain visible to handlers attached to
187
+ the same iteration. `it` (the target of `.field` reads and `$method`
188
+ calls) is set on both.
189
+
190
+ | pushed by | kind | shape |
191
+ | ----------------------------------- | ----- | ------------------------------------ |
192
+ | `<x render=".f">` / `<x render-it>` | frame | `it` = child, fresh binds |
193
+ | `<x render-each>` per iter | frame | `it` = item, binds `{ key }` |
194
+ | `<div @each>` per iter | scope | `it` = item, binds `{ key, value }` |
195
+ | `<div @enrich-with=…>` (no `@each`) | scope | `it` unchanged, binds = alter result |
196
+
197
+ For full mechanics see [iteration.md](./iteration.md).
198
+ This is why a handler attached to `<div @each>` runs against the
199
+ *parent* component (the scope is transparent — the surrounding frame
200
+ still owns dispatch), while one inside `<x render-it>` runs against
201
+ the *item* (render-it pushed a fresh frame for the child).
202
+
203
+ **Paths, not references.** The DOM is the only thing that survives
204
+ between render and click, so the renderer leaves breadcrumbs:
205
+ `data-cid` / `data-nid` / `data-eid` on rendered elements, and `§…§`
206
+ HTML comments adjacent to iteration entries. On a DOM event the
207
+ runtime walks from the target up to the root, reads those breadcrumbs,
208
+ and rebuilds a *positional* `Path` — an array of steps from the root
209
+ to the value the handler should run against. The same `Path` is reused
210
+ verbatim for `ctx.send` and `ctx.intent`: because it's positional rather
211
+ than a captured reference, an async answer survives intervening
212
+ transactions that rebuild the root.
213
+ "The right slot" is exact for named fields and for map entries by key
214
+ (seq-access keys like `.sheets[.selId]` are *pinned* to their
215
+ dispatch-time value by default); a bare list **index** still slides if the
216
+ list reordered. See [messages-and-intents.md](./messages-and-intents.md) for the
217
+ dispatch APIs and [semantics.md](./semantics.md) for the path/transaction
218
+ model and key pinning.
219
+
220
+ **Why `alter` is its own table.** Alter handlers are pure, evaluated
221
+ on every render, and produce binds (no state change). `receive` and
222
+ `intent` are transactional and produce new values. Same lookup mechanism, different contracts — keep them
223
+ separate.
224
+
225
+ ## Notation Reference
226
+
227
+ Views are name-based: there is no arithmetic expression syntax in
228
+ values, and no Vue- or Mustache-style `{{ … }}` placeholders. Every
229
+ value slot — conditions (`@show`, `@if`), iteration (`@each`,
230
+ `render-each`, `@when`), enrichment (`@enrich-with`, `@loop-with`), template
231
+ expansion (`{…}`, `:attr`, `@text`) — names a field, method, macro, or
232
+ handler defined on the component (or registered with the app). Logic
233
+ lives in `methods` / `alter` / `receive` / `intent` and is referenced by
234
+ name; the template itself only routes
235
+ data and events.
236
+
237
+ The one exception is **boolean predicates** in conditional slots
238
+ (`@show`, `@hide`, `@if.<attr>`): a closed set of operators applied to
239
+ a value, written predicate-first like a handler call —
240
+ `empty?`, `truthy?`, `falsy?`, `null?`, `equals?`. E.g.
241
+ `@hide="empty? .items"`, `@show="truthy? .query"`. A conditional slot
242
+ otherwise accepts the same value forms as `@text` — a plain field
243
+ (`@show=".isOpen"`), a no-arg method (`@show="$canSubmit"`), or a loop/scope
244
+ `@binding` (`@show="@isSelected"`, `@hide="@hasDesc"`) — read as a boolean.
245
+
246
+ `equals?` takes two args and is the idiomatic way to show/hide by name,
247
+ e.g. `@show="equals? .view 'detail'"`. Predicate args (and handler
248
+ args) accept string literals: `'detail'`, or `'two words'` for a
249
+ literal with spaces (escape an interior quote as `\'`).
250
+
251
+ | Prefix | Means | Example |
252
+ | -------- | ----------------------------------------- | --------------------- |
253
+ | `.x` | field on `this` (single-level — no `.foo.bar` paths) | `.count`, `.title` |
254
+ | `$x` | no-arg method call on `this` (a method reference) | `$inc`, `$canSubmit` |
255
+ | `@x` | local binding (loop / scope) | `@key`, `@value` |
256
+ | `^x` | macro parameter | `^label` |
257
+ | `*x` | dynamic binding — see [advanced.md](./advanced.md) | `*theme` |
258
+ | `Name` | component type (PascalCase) | `Item`, `JsonNull` |
259
+ | `name` | bare identifier — meaning depends on slot | `dec`, `value` |
260
+ | `'str'` | string literal | `'btn btn-success'` |
261
+ | `$'…'` | string template (`{expr}` interpolation) | `$'Hi {.name}'` |
262
+ | `.s[.k]` | sequence/map item access | `.byKey[.currentKey]` |
263
+ | `pred? .x` | boolean predicate in a conditional slot | `empty? .items`, `equals? .view 'detail'` |
264
+
265
+ `.x` and `$x` are not interchangeable: `.x` only reads a field, `$x`
266
+ only calls a method. The linter flags a mismatch and tells you which
267
+ prefix to use.
268
+
269
+ A bare `name` (no prefix) in `@on.<event>="<handler> <arg> <arg>..."`
270
+ resolves by slot:
271
+
272
+ - **First slot** — handler name looked up in `receive` / `alter` (use
273
+ `$name` for `methods`).
274
+ - **Subsequent slots** — built-in handler argument name (full list in
275
+ *Event Handling*); anything else triggers a lint warning.
276
+
277
+ ```html
278
+ <button @on.click="addItem JsonSelector">+</button>
279
+ <!-- ↑ handler ↑ Type -->
280
+ ```
281
+
282
+ `ctx` (an `EventContext`) is auto-appended as the trailing arg, so the
283
+ handler is called as `addItem(JsonSelector, ctx)`. Don't list `ctx` in
284
+ the template — it's always passed.
285
+
286
+ ## Quoting & String Literals
287
+
288
+ A string template is written `$'…'` — a single-quoted run with a leading
289
+ `$`, holding `{expr}` interpolations. `:attr=` and other text slots accept
290
+ `$'…'` templates; `@if`, `@each`, `<x render=>` do not.
291
+
292
+ | Form | Example | Where it works |
293
+ | ------------------- | ------------------------- | ------------------------------------------------ |
294
+ | `'string'` | `@then="'btn ok'"` | anywhere a value is allowed |
295
+ | `$'…'` template | `:class="$'btn {.kind}'"` | `:attr=`, `@text`, `@title`, macro dynamic attrs |
296
+ | Bare without quotes | `flex gap-3` | **never** — returns `null` |
297
+ | Bare identifier | `dec`, `value` | name slots only (handler/arg, not as a value) |
298
+
299
+ ```html
300
+ <!-- ✅ -->
301
+ <p :class="'flex gap-3'">x</p>
302
+ <p :class="$'flex {.color}'">x</p> <!-- $'…' string template -->
303
+ <p :class="$'static-classes {\'\'}'">x</p> <!-- folds to a const -->
304
+
305
+ <!-- ❌ -->
306
+ <p :class="flex gap-3">x</p> <!-- null: no quotes -->
307
+ <p :class="flex {.color}">x</p> <!-- null: unquoted {…} is not a template -->
308
+ <x render="'foo bar'"></x> <!-- @render rejects string templates -->
309
+ ```
310
+
311
+ ## Component Skeleton
312
+
313
+ The object passed to `component({...})` is the **component spec** — the
314
+ linter warns on unknown spec keys. The full shape:
315
+
316
+ ```js
317
+ component({
318
+ name: "MyComp",
319
+ fields: { // see "Field Types"
320
+ count: 0,
321
+ items: [],
322
+ nullable: null,
323
+ },
324
+ view: html`<p @text=".count"></p>`, // default view (named "main")
325
+ views: { // additional views
326
+ edit: html`<input :value=".count" @on.input="setCount e.valueAsInt" />`,
327
+ big: {
328
+ view: html`<h1 @text=".count"></h1>`,
329
+ style: css`h1 { font-size: 4rem; }`,
330
+ },
331
+ },
332
+ style: css`p { color: blue; }`, // scoped to main view
333
+ commonStyle: css`p { font-family: sans-serif; }`, // scoped to all views of this component
334
+ globalStyle: css`body { margin: 0; }`, // injected globally, no scoping
335
+ methods: {
336
+ doubled() { return this.count * 2; },
337
+ },
338
+ alter: { filterItem(_k, item) { return item.length > 0; } },
339
+ // ADDRESSED: this component's own @on.* names, what a parent sends it, and the
340
+ // answers to intents it raised — one bucket, and nothing tells them apart.
341
+ receive: {
342
+ inc(draft) { draft.count++; },
343
+ setCount(draft, value) { draft.count = value; },
344
+ onClick(draft) { draft.count++; },
345
+ init(_draft, ctx) { ctx.intent("loadData", [], { route: ["lex"] }); },
346
+ loadDataOk(draft, res) { draft.items = res; },
347
+ loadDataError(draft, err) { draft.error = String(err); },
348
+ },
349
+ // ROUTED: what this component answers for somebody who did not address it.
350
+ intent: { itemPicked(draft, item) { draft.selected = item; } },
351
+ statics: { fromData(d) { return this.make({ count: d.n ?? 0 }); } },
352
+ // provide: { ... }, lookup: { ... } // see advanced.md
353
+ });
354
+ ```
355
+
356
+ `Comp.make({...})` builds and deeply freezes an instance. Arrays, plain objects,
357
+ native `Map`, and native `Set` stay native. Wrap nested component data with
358
+ `Child.make({...})` when it needs component identity and its own view/handlers.
359
+
360
+ ## Field Types
361
+
362
+ `fields: { name: defaultValue }` — type inferred from the default.
363
+
364
+ | Default | Field type | Draft update example |
365
+ | --- | --- | --- |
366
+ | `"hi"` | text | `draft.x = value` |
367
+ | `42` | float | `draft.x++` |
368
+ | `{ type: "int", defaultValue: 0 }` | int | `draft.x = Math.trunc(value)` |
369
+ | `true` | bool | `draft.x = !draft.x` |
370
+ | `null` | any | `draft.x = value` |
371
+ | `[]` | list | `draft.x.push(value)`, `draft.x.splice(i, 1)` |
372
+ | `{}` | object | `draft.x.key = value` |
373
+ | `new Map()` | map | `draft.x.set(key, value)` |
374
+ | `new Set()` | set | `draft.x.add(value)`, `draft.x.delete(value)` |
375
+
376
+ Fields do not generate setters or collection mutators. Define only the named
377
+ handlers your view/API needs; the linter reports a field/method name collision.
378
+
379
+ Emptiness / truthiness / null checks are not generated as methods — use
380
+ the boolean predicates `empty?`, `truthy?`, `falsy?`, `null?`, `equals?`
381
+ in a conditional slot instead (e.g. `@hide="empty? .x"`,
382
+ `@show="equals? .view 'detail'"`).
383
+
384
+ Explicit field types via `classFromData`:
385
+
386
+ ```js
387
+ fields: {
388
+ count: { type: "int", defaultValue: 10 }, // text/int/float/bool/list/object/map/set/any
389
+ child: { component: "Item", args: { ... } }, // deferred reference by name
390
+ child2: Item.make({ name: "" }), // direct default if Item is in scope
391
+ }
392
+ ```
393
+
394
+ The `{ component, args }` form is for when the referenced component is **not
395
+ available** at field-definition time (forward reference, circular import).
396
+ `component` must be the component **name as a string** — passing the class
397
+ itself is a common mistake and is flagged by lint code
398
+ `COMP_FIELD_BAD_SHAPE`. When the component class **is** in scope, prefer
399
+ `ComponentName.make({...})` as the default value — no string indirection.
400
+
401
+ ## Methods as Predicates & Computed Values
402
+
403
+ A no-arg method called via `$name` is invoked and its return value is
404
+ used. Works anywhere a value is read — `@text`, `:attr`, `@show` /
405
+ `@hide`, `@if.<attr>`, and `{…}` interpolation. (`.name` is a field
406
+ read and never invokes; `$name` is the method call.)
407
+
408
+ ```js
409
+ methods: {
410
+ canSubmit() { return this.title.length > 0 && !this.isLoading; },
411
+ buttonClass() { return this.isActive ? "btn btn-primary" : "btn"; },
412
+ fullName() { return `${this.first} ${this.last}`; },
413
+ }
414
+ ```
415
+
416
+ ```html
417
+ <button @show="$canSubmit" :class="$buttonClass">Save</button>
418
+ <p :title="$'Hello, {$fullName}'" @text="$fullName"></p>
419
+ ```
420
+
421
+ The boolean predicates (`empty?`, `truthy?`, `falsy?`, `null?`,
422
+ `equals?`) cover single-field checks in conditional slots; reach for a
423
+ method when the condition spans multiple fields or needs derivation. The
424
+ method takes no args.
425
+
426
+ Tutuca expressions resolve a **single** name on `this` — there is no
427
+ path syntax. `@text=".user.name"` does not navigate; it fails. When the
428
+ value lives behind a field, your options are:
429
+
430
+ - **Render the child as a component** — `<x render=".user">` then
431
+ `@text=".name"` inside the child's view. Best when the nested thing is
432
+ already (or could be) a component.
433
+ - **Add a method** — `userName() { return this.user.name; }` then
434
+ `@text="$userName"`. Best for one-off derivations or formatting.
435
+ - **Use `@enrich-with`** — exposes computed values as `@`-bindings to a
436
+ subtree without putting them on the component. See *Scope Enrichment*
437
+ in [iteration.md](./iteration.md).
438
+
439
+ Exceptions: `@each` / `render-each` accept `.field` or `*dynamic` only
440
+ (not a `$method` — a method result has no addressable path for event
441
+ dispatch, so `$m` is rejected there at parse time), and `<x render>`
442
+ expects a component instance — for a derived list, store it in a field
443
+ or use `@when` with `alter`.
444
+
445
+ ## Statics
446
+
447
+ `statics: {...}` adds methods to the component **class**, not instances.
448
+ `component({...})` returns that class directly — the component IS the class,
449
+ with its metadata (fields, views, handlers, spec) attached behind
450
+ `tutuca`'s well-known `COMPONENT` symbol. Statics are available as
451
+ `Comp.<name>(...)` alongside the auto-generated `Comp.make(...)`. Inside a
452
+ static, `this` is the class itself.
453
+
454
+ Common use: a `fromData` factory that recursively builds instances from
455
+ plain JS data:
456
+
457
+ ```js
458
+ statics: {
459
+ fromData({ items = [] }) {
460
+ return this.make({ items: items.map((v) => Item.fromData(v)) });
461
+ },
462
+ }
463
+ // usage: TreeRoot.fromData([...])
464
+ ```
465
+
466
+ > **Scopes own the component.** A component is bound to a scope at
467
+ > `registerComponents` time — that scope owns its component, tag,
468
+ > and scope-bound `make`/statics — so a given component object is live in
469
+ > one scope at a time. Each app/registry is a separate scope; the same
470
+ > *object* registered into two of them rebinds (last wins). To run the
471
+ > same definition in two genuinely separate registries at once, build an
472
+ > independent copy with `component(Comp.spec)` (new id ⇒ separately
473
+ > compiled CSS + separate identity — the price of isolation, not a way to
474
+ > dedupe within one app). For reuse inside one scope, register the single
475
+ > object once and re-export it.
476
+
477
+ **Multi-scope caveat for statics.** A static like `fromData` that builds
478
+ a *different* child type by naming the imported const directly
479
+ (`Item.fromData(v)` above) hardcodes the child's *original* scope.
480
+ Fine in a single-scope app; across scopes, resolve the child through the
481
+ caller's scope instead — `this.scope.lookupComponent("Item")` — so it
482
+ deserializes into the right one. Recursion into the *same* type needs no
483
+ lookup: `this.fromData(v)` / `this.make` already target the caller's scope.
484
+
485
+ ## Text Rendering
486
+
487
+ ```html
488
+ <span @text=".str"></span> <!-- prepend text into span -->
489
+ <x text=".bool"></x> <!-- text-only, no DOM element -->
490
+ <x text="$getStrUpper"></x> <!-- $ calls a method -->
491
+ <x text="@value"></x> <!-- loop binding -->
492
+ ```
493
+
494
+ Use `@text` when you already have a host element to put the text in; use
495
+ `<x text=…>` for bare text with no wrapping element (e.g. text interleaved with
496
+ other inline content, or a loop binding). Both take the same value forms
497
+ (`.field`, `$method`, `@binding`).
498
+
499
+ ## Attribute Binding
500
+
501
+ ```html
502
+ <input :value=".str" @on.input="setStr e.value" />
503
+ <a :href=".url" :title="$'Hi {.name}'">link</a> <!-- string template -->
504
+ <button :class="$'btn {.color}'">x</button>
505
+ ```
506
+
507
+ Plain attrs are static. `:attr="..."` is a dynamic expression. Boolean
508
+ HTML attributes (`disabled`, `checked`, `hidden`, …) are auto-recognized;
509
+ pass a boolean field.
510
+
511
+ A static `class="…"` and a dynamic `:class`/`@if.class` **cannot coexist on the
512
+ same element** — setting one attribute two ways is a lint error
513
+ (`DUPLICATE_ATTR_DEFINITION`), and at runtime the dynamic value wins and the
514
+ static class is dropped. Fold any structural classes into the bound expression,
515
+ e.g. `:class="$'btn {.color}'"` (note `btn` is part of the template, not a
516
+ separate `class="btn"`). The same applies to other attributes — see the
517
+ duplicate-attribute note below.
518
+
519
+ The HTML parser lowercases attribute names before Tutuca sees them, so
520
+ `:mapId` arrives as `:mapid` and `<x:Card>` becomes `<x:card>`. Three
521
+ consequences:
522
+
523
+ - SVG attributes are case-sensitive. Tutuca special-cases `:viewbox` →
524
+ `viewBox` so SVG roots work; for other camelCased SVG attrs, wrap them
525
+ in components that emit raw markup.
526
+ - Custom-element property setters defined in camelCase **will not fire**.
527
+ `:mapId=".mapId"` runs `node.mapid = value`; if the
528
+ element defined `set mapId(...)`, the lookup misses and JS silently
529
+ creates an own data property `mapid` on the element instead of invoking
530
+ the setter — no error, no warning, the bound state stays null. Author
531
+ custom elements with kebab-case attributes plus lowercased property
532
+ setters (or aliases), and bind via `:kebab-name` from Tutuca templates.
533
+ - Macro registry keys are lowercased on insert for the same reason
534
+ (see [macros.md](./macros.md)).
535
+
536
+ Tutuca auto-namespaces by subtree: elements inside `<svg>` get the SVG
537
+ namespace and elements inside `<math>` get MathML, with spec-cased local
538
+ names preserved (`linearGradient`, `viewBox`). A `<foreignObject>` switches
539
+ its children back to the HTML namespace. Customised built-in elements work
540
+ via `is="..."` (e.g. `<button is="x-fancy">`); `is` is applied when the
541
+ element is created, so it must be a static attribute — setting it later
542
+ does not upgrade the element.
543
+
544
+ ### When nothing renders (or renders unstyled)
545
+
546
+ A few mistakes fail quietly — no error, just a blank or unstyled result, which
547
+ is the slowest kind to debug. **Run `tutuca lint <module>` first**: it catches
548
+ several of these. The usual suspects:
549
+
550
+ - **Unparseable attribute value** → the attribute is silently dropped. A bare
551
+ multi-word value isn't a string — quote it (`:label="'two words'"`) or make it
552
+ a template (`:label="$'{.a} {.b}'"`). Lint flags this as `BAD_VALUE`.
553
+ - **camelCase attribute on a custom element** → setter no-op (see the lowercasing
554
+ note above). Use kebab-case attributes. Not lintable — the HTML parser
555
+ lowercases the name before either Tutuca or the linter sees it.
556
+ - **Forgotten margaui `_palette`/decoy view** → classes assembled in methods or
557
+ interpolations render unstyled. See [margaui.md](./margaui.md). Not lintable.
558
+ - **A whitespace-only `html\`\``** → blank render. A *leading* newline before the
559
+ root element is fine (the parser trims it); a template with no element at all
560
+ is not.
561
+
562
+ ## Event Handling
563
+
564
+ ```html
565
+ <!-- every event names a receive handler (no prefix) -->
566
+ <button @on.click="inc">+</button>
567
+ <button @on.click="dec">-</button>
568
+
569
+ <!-- pass args by name -->
570
+ <input @on.input="setStr e.value" />
571
+ <input @on.input="setN e.valueAsInt" />
572
+ <button @on.click="pick @key e.altKey">pick</button>
573
+ <button @on.click="addItem JsonSelector">+</button> <!-- type as arg -->
574
+ <button @on.click="loadAnotherWay">load</button> <!-- ctx auto-appended -->
575
+ ```
576
+
577
+ Every `@on.<event>` handler receives an `EventContext` as its trailing
578
+ arg automatically — written args come first, `ctx` last. So
579
+ `loadAnotherWay` is called as `loadAnotherWay(draft, ctx)`, and
580
+ `pick @key e.altKey` is called as `pick(draft, key, isAlt, ctx)`. You can still
581
+ write `ctx` in the template (it resolves to a fresh `EventContext`), but it is
582
+ redundant.
583
+
584
+ Handler argument forms:
585
+
586
+ - **Event-member reads** — `e.value`, `e.key`, `e.altKey`, `e.target`, plus
587
+ dotted paths into the event: `e.target.dataset.slot`, `e.target.id`,
588
+ `e.detail.x`. Null-safe at every link; a missing member reads as null.
589
+ - **Computed conveniences** with no single-property form — `e.valueAsInt`,
590
+ `e.valueAsFloat`, `e.isCtrl`/`e.isCmd` (mac-aware), `e.isUpKey`, `e.isDownKey`,
591
+ `e.isSend`, `e.isCancel`, `e.isTabKey`, and on drags `e.dragInfo`/
592
+ `e.dragKey`/`e.dragValue`/`e.dragType`. Resolved only as a one-level path;
593
+ nothing composes through them (`e.target.isUpKey` is null).
594
+ - **Channel args** — `ctx` (the `EventContext`) and `dragInfo`.
595
+
596
+ The content of `e.value` depends on the event source:
597
+
598
+ | Source | What `e.value` resolves to |
599
+ |-----------------------------|--------------------------------------------------|
600
+ | `<input type="checkbox">` | `event.target.checked` (boolean) |
601
+ | `CustomEvent` | `event.detail` |
602
+ | anything else | `event.target.value` (string), or null if absent |
603
+
604
+ For numeric inputs, prefer `e.valueAsInt` / `valueAsFloat` to skip the
605
+ string parse.
606
+
607
+ Ask for the most granular arg the handler actually uses — `e.value` /
608
+ `e.valueAsInt` / `e.key`, not the raw event object — when the specific value is
609
+ all you need. A handler that takes `event` forces every test and
610
+ storybook story to fabricate a DOM-event-shaped object
611
+ (`{ target: { value: … } }`); one that takes `value` is called with a
612
+ plain literal. (Genuine exceptions exist — e.g. a file input needs
613
+ `event` to reach `event.target.files`.) See
614
+ [testing.md](./testing.md) *Designing handlers so tests stay simple*.
615
+
616
+ ### Event Modifiers
617
+
618
+ `@on.<event>+<mod>+<mod>=...`
619
+
620
+ Guards — run the handler only when they hold:
621
+
622
+ - All events: `+ctrl`, `+cmd`/`+meta`, `+alt`
623
+ - `keydown` only: `+send` (Enter), `+cancel` (Escape)
624
+
625
+ Effects — act on the DOM event, on all events:
626
+
627
+ - `+prevent` → `event.preventDefault()`
628
+ - `+stop` → `event.stopPropagation()`
629
+
630
+ ```html
631
+ <input @on.keydown+send="submit e.value" @on.keydown+cancel="reset" />
632
+ <button @on.click+ctrl="soloOnly">ctrl-click</button>
633
+ <form @on.submit+prevent="save">…</form>
634
+ <input @on.keydown+send+prevent="submit e.value" />
635
+ ```
636
+
637
+ Effects apply only when every guard on the same handler passed, whatever
638
+ order they are written in: `+send+prevent` prevents on Enter and nothing
639
+ else. They run from the app's root listener, which is where tutuca listens
640
+ — that is invisible to `+prevent` (the default action happens after the
641
+ event finishes propagating) but it makes `+stop` narrower than it looks:
642
+ it stops listeners *outside* the app root only. DOM ancestors between the
643
+ target and the root have already seen the event, and tutuca runs a single
644
+ handler per event anyway, so `+stop` never suppresses another `@on`.
645
+
646
+ ### Web Components & Custom Events
647
+
648
+ Custom elements just work, and any `CustomEvent` they fire is reachable
649
+ via `@on.<event-name>`. The event's `detail` surfaces as `value`:
650
+
651
+ ```js
652
+ import "https://cdn.jsdelivr.net/npm/emoji-picker-element/+esm";
653
+
654
+ receive: { onPick(draft, detail) { draft.current = detail.unicode; } }
655
+ view: html`<emoji-picker @on.emoji-click="onPick e.value"></emoji-picker>`,
656
+ ```
657
+
658
+ Handle these events declaratively with `@on.<event-name>` in the view —
659
+ don't grab the node from host/glue code and `addEventListener` on it. A
660
+ listener attached from outside the component runs outside the handler
661
+ model: no draft transaction, no transactor batching, and the mutation
662
+ is invisible to the component that owns the state (the same hazard as
663
+ reaching into `app.state` directly). For any event with a real element in
664
+ the tree, `@on.` is the only entry point you need. Genuinely external
665
+ inbound sources (WebSocket, `postMessage`, timers) have no element to bind
666
+ — route those through `app.sendAtRoot` instead (see
667
+ [messages-and-intents.md](./messages-and-intents.md)).
668
+
669
+ Pitfall: binding a camelCase JS property on a custom element silently
670
+ fails — see the lowercasing rules in *Attribute Binding* above.
671
+
672
+ ## Conditional Display
673
+
674
+ ```html
675
+ <div @show=".isLoading">Loading...</div>
676
+ <div @hide=".isLoading">content</div>
677
+
678
+ <!-- boolean predicates; equals? compares against a string literal -->
679
+ <div @show="equals? .view 'detail'">detail view</div>
680
+
681
+ <!-- @show / @hide also work as directives on `<x>` render ops:
682
+ wraps the produced node, no extra DOM element. Allowed on
683
+ text / render / render-it / render-each. First attr in
684
+ source order becomes the outermost wrapper. -->
685
+ <x text=".name" @show=".isOpen"></x>
686
+ <x render-it @hide=".isHidden"></x>
687
+ <x render-each=".items" @when="filter" @show=".isOpen"></x>
688
+
689
+ <!-- Single @if: shorthand @then/@else (attr inferred) -->
690
+ <button @if.class=".isActive" @then="'btn btn-success'" @else="'btn btn-ghost'">
691
+ ...
692
+ </button>
693
+
694
+ <!-- Multiple @if on same element: name the attr explicitly -->
695
+ <button
696
+ @if.class=".isActive"
697
+ @then="'on'"
698
+ @else="'off'"
699
+ @if.title=".isActive"
700
+ @then.title="'On'"
701
+ @else.title="'Off'"
702
+ >
703
+ ...
704
+ </button>
705
+ ```
706
+
707
+ > HTML disallows duplicate attrs, so with multiple `@if.<attr>` on one
708
+ > element every `@then`/`@else` after the first **must** include the attr
709
+ > name — otherwise the parser drops it before tutuca sees it.
710
+
711
+ ## List Iteration & Scope Enrichment
712
+
713
+ ```html
714
+ <li @each=".items"><span @text="@key"></span>: <x text="@value"></x></li>
715
+ <x render-each=".items"></x>
716
+ ```
717
+
718
+ Auto-bound names inside a loop are `@key` and `@value`. Iteration
719
+ (`@each` / `render-each`), filtering (`@when`), item and scope
720
+ enrichment (`@enrich-with`), pagination and the `@loop-with` return
721
+ shape, and the `@each` lifecycle: see [iteration.md](./iteration.md).
722
+
723
+ ## Rendering Components
724
+
725
+ ```html
726
+ <x render=".item"></x> <!-- default ("main") view -->
727
+ <x render=".item" as="edit"></x> <!-- specific view (literal) -->
728
+ <x render=".item" as=".mode"></x> <!-- view chosen by a field at runtime -->
729
+ <x render-it></x> <!-- only inside @each / render-each -->
730
+ <x render=".byIndex[.currentIndex]"></x> <!-- list item access -->
731
+ <x render=".byKey[.currentKey]"></x> <!-- map item access -->
732
+ <x render="*active"></x> <!-- dynamic binding — see advanced.md -->
733
+ <x render=".item" @show=".isOpen"></x> <!-- conditional wrap, see "Conditional Display" -->
734
+ ```
735
+
736
+ The top-level `view` is registered under `"main"` (the default); extras
737
+ go under `views: { name: html\`...\` }`. `as` selects which view of the
738
+ rendered component to use, falling back to `main` if absent. It accepts the
739
+ same dynamic values as `@push-view` (a literal name like `edit`, or `.field`,
740
+ `*dyn`, `@bind`, `$method`, `$'…{x}…'`), evaluated against the **host**
741
+ component at render time. `as` only applies to the **direct** component — for
742
+ whole-subtree control, use `@push-view` (next section). For `render-each` the
743
+ selector is evaluated once against the host, so every item gets the same view.
744
+
745
+ ## Multiple Views & View Stack
746
+
747
+ ```js
748
+ component({
749
+ view: html`<p @text=".title"></p>`, // "main"
750
+ views: { edit: html`<input :value=".title" @on.input="setTitle e.value" />` },
751
+ });
752
+ ```
753
+
754
+ ```html
755
+ <!-- @push-view pushes a name onto the rendering stack;
756
+ descendants resolve to first matching view, falling back to "main" -->
757
+ <div @push-view=".view"><x render-each=".items"></x></div>
758
+ ```
759
+
760
+ | Directive | Scope |
761
+ |--------------------|--------------------------------------------------------------------------|
762
+ | `as="edit"` / `as=".mode"` | One `<x render>` element only. Literal or dynamic (like `@push-view`), evaluated against the host. |
763
+ | `@push-view=".v"` | Every component rendered recursively under the host (children + descendants). Each picks the first stack entry it has a matching view for; falls back to `"main"`. Inner `@push-view`s nest, extending the outer ones. |
764
+
765
+ ## Styles
766
+
767
+ `style` is scoped to the main view, `commonStyle` to all views of the
768
+ component, `globalStyle` is injected unscoped (see the *Component
769
+ Skeleton* above). Scoping mechanics, styling the root element with bare
770
+ declarations, and the at-rules that must live in `globalStyle`: see
771
+ [styles.md](./styles.md). Tailwind / MargaUI utility classes:
772
+ [margaui.md](./margaui.md).
773
+
774
+ ## Triggers and Handlers
775
+
776
+ Tutuca has **two** dispatch channels, and one question separates them:
777
+ *does the sender know who handles this?*
778
+
779
+ | Triggered by | Handler block | Use for |
780
+ | ------------------------------------------------ | ------------------ | ------------------------------------------------ |
781
+ | DOM event (`click`, `input`, …) | `receive: { ... }` | the component handling its own events |
782
+ | `ctx.send(name)` — message to a target path | `receive: { ... }` | addressing one known component (or self) |
783
+ | an answer to an intent this component raised | `receive: { ... }` | `<name>Ok` / `<name>Error` / `<name>Unhandled` |
784
+ | `ctx.intent(name, args, opts)` — walks a route | `intent: { ... }` | a job the sender does not address |
785
+
786
+ The first three rows are one bucket and **nothing tells them apart** — a
787
+ component that answered its own click differently from the identical
788
+ `ctx.send` from its parent could be driven neither from a test nor from a
789
+ parent.
790
+
791
+ An intent carries a **route**: `["dyn"]` walks the ancestors, `["lex"]`
792
+ walks the handlers registered on the scope, and the default `["dyn","lex"]`
793
+ tries both. The verb does not decide which scope answers — the route does,
794
+ written at the call site.
795
+
796
+ Every dispatched `receive` or `intent` handler is called as
797
+ `handler(draft, ...args, ctx)`. `this` is the immutable current instance;
798
+ `draft` is its Immer draft; `ctx` is always trailing. Mutate `draft` and
799
+ return nothing (or return `draft`) to commit. Return any other value to swap
800
+ the addressed component for that value. Mutating the draft and returning a
801
+ replacement in the same handler is an error. An unchanged recipe preserves
802
+ the current identity. Routes,
803
+ `ctx.reply` / `ctx.fail` / `ctx.forward` / `ctx.stop`, the three
804
+ outcomes, `ctx.at`, the `$unknown` fallback, and intent-handler
805
+ registration are in [messages-and-intents.md](./messages-and-intents.md);
806
+ worked snippets in
807
+ [patterns/coordinate-components.md](./patterns/coordinate-components.md).
808
+
809
+ `alter` is a third handler block, but it isn't event-triggered — the
810
+ renderer invokes alter handlers with their existing read-only signature to
811
+ produce binds, not state changes
812
+ (see *Mental model*, and *Scope Enrichment* in
813
+ [iteration.md](./iteration.md)).
814
+
815
+ ## Macros
816
+
817
+ Pure template expansion — `macro({ params }, html\`...\`)` definitions
818
+ called as `<x:name>`, with `^param` references, slots, and named slots:
819
+ see [macros.md](./macros.md). Registry keys are lowercased —
820
+ `<x:Card>` resolves as `<x:card>`.
821
+
822
+ ## Raw HTML (escape hatch)
823
+
824
+ ```html
825
+ <div @dangerouslysetinnerhtml=".trustedHtml"></div>
826
+ ```
827
+
828
+ Bypasses all escaping; children of the element are ignored when active.
829
+
830
+ ## Immer Utilities
831
+
832
+ Tutuca uses Immer internally but does not add Immer's API to the root export.
833
+ Import recipe utilities explicitly when tests or host code need them:
834
+
835
+ ```js
836
+ import { produce, immerable } from "tutuca/immer";
837
+ ```
838
+
839
+ Generated component classes are already draftable. Add `[immerable] = true`
840
+ to custom classes stored in state. Native `Map` and `Set` support is enabled by
841
+ the `tutuca/immer` entry point.
842
+
843
+ ## Conventional Module Exports
844
+
845
+ Examples and the storybook glue follow this shape so files compose freely
846
+ and the `tutuca` CLI can introspect any module without per-app glue:
847
+
848
+ ```js
849
+ export function getComponents() { return [Comp, ...]; }
850
+ export function getMacros() { return { name: macro }; } // optional
851
+ export function getIntentHandlers() { return { name: async fn }; } // optional
852
+ export function getRoot() { return Root.make({...}); }
853
+ export function getExamples() {
854
+ // Return one section, or an array of sections.
855
+ return {
856
+ title: "...",
857
+ description: "...",
858
+ // value = Comp.make(...); intentHandlers (optional) mocks this example's requests
859
+ items: [{ title, description, value, view, intentHandlers }],
860
+ };
861
+ }
862
+ export function getTests({ describe, test, expect }) { /*...*/ } // optional — see cli.md
863
+ ```
864
+
865
+ An example item may carry an optional **`intentHandlers`** map — per-example
866
+ mocks (keyed by request name) that override the module's real
867
+ `getIntentHandlers()` for that one instance, so two examples of the same
868
+ component show different responses side by side. Return a fixture, `throw` for
869
+ the error path, or never resolve to hold a loading state. Full treatment, plus
870
+ the `on` lifecycle hooks, in [storybook.md](./storybook.md).
871
+
872
+ Best practice: have `getComponents()` return **every** component the module
873
+ defines — child and helper components included — and give each one at least
874
+ one item in `getExamples()`. A component left out of `getComponents()` is
875
+ invisible to `tutuca lint`/`render`/`test`, so it silently loses linting and
876
+ render coverage. If your components already live behind a differently named
877
+ export, alias it instead of teaching tools a new name:
878
+
879
+ ```js
880
+ export { allMyComponents as getComponents } from "./app.js";
881
+ ```
882
+
883
+ Put these exports in a co-located **`*.dev.js`** file (a dev-only module
884
+ holding stories + tests, never shipped) and `tutuca storybook` auto-discovers
885
+ and renders them with no setup — see [cli.md](./cli.md). The same shape is
886
+ consumed by the shipped `tutuca/storybook` library if you want to embed a
887
+ storybook in your own page — see [storybook.md](./storybook.md).
888
+
889
+ ## See also
890
+
891
+ - [iteration.md](./iteration.md) — `@each` / `render-each`, `@when`,
892
+ `@enrich-with`, `@loop-with` pagination, and the loop lifecycle.
893
+ - [macros.md](./macros.md) — `macro()` definitions, `<x:name>` calls,
894
+ slots, and registration.
895
+ - [styles.md](./styles.md) — `style` / `commonStyle` / `globalStyle`
896
+ scoping mechanics and pitfalls.
897
+ - [component-design.md](./component-design.md) — design judgment for shaping a
898
+ feature into components: responsibilities, where state lives, which channel to
899
+ reach for, and a curated do's & don'ts list.
900
+ - [messages-and-intents.md](./messages-and-intents.md) — addressed
901
+ `send`-`receive` vs routed `intent`, the `ctx.at` `PathBuilder`, `$unknown`, and
902
+ request-handler registration.
903
+ - [advanced.md](./advanced.md) — dynamic bindings (`*x`), pseudo-`@x` for
904
+ `<select>` / `<table>` / `<tr>`, drag & drop, custom seq types.
905
+ - [margaui.md](./margaui.md) — setting up MargaUI styling: install
906
+ (CDN / npm / vendoring), theme CSS, and `compileClassesToStyleText`.
907
+ - [semantics.md](./semantics.md) — runtime semantics: path steps, the
908
+ transaction lifecycle, dyn-var teleporting, and async key pinning
909
+ (`livePath`).
910
+ - [testing.md](./testing.md) — `getTests` shape and the handler calling
911
+ convention for tests.
912
+ - [storybook.md](./storybook.md) — authoring `*.dev.js` story modules and
913
+ running / embedding the storybook.
914
+ - [cli.md](./cli.md) — commands, flags, exit codes, and the full linter rule
915
+ list.
916
+ - [patterns/README.md](./patterns/README.md) — task-oriented recipes ("how do I
917
+ iterate / filter / paginate / show-hide / build tabs / share state / …"),
918
+ each linking back here and to a runnable example.