tutuca 0.11.1 → 0.11.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.
Files changed (107) hide show
  1. package/dist/tutuca-dev.ext.js +6 -1
  2. package/dist/tutuca-dev.js +6 -1
  3. package/dist/tutuca-dev.min.js +1 -1
  4. package/package.json +1 -1
  5. package/skill/margaui/SKILL.md +0 -105
  6. package/skill/margaui/components/accordion.md +0 -127
  7. package/skill/margaui/components/alert.md +0 -174
  8. package/skill/margaui/components/aura.md +0 -97
  9. package/skill/margaui/components/avatar.md +0 -220
  10. package/skill/margaui/components/badge.md +0 -193
  11. package/skill/margaui/components/breadcrumbs.md +0 -103
  12. package/skill/margaui/components/button.md +0 -322
  13. package/skill/margaui/components/calendar.md +0 -67
  14. package/skill/margaui/components/card.md +0 -373
  15. package/skill/margaui/components/carousel.md +0 -387
  16. package/skill/margaui/components/chat.md +0 -171
  17. package/skill/margaui/components/checkbox.md +0 -101
  18. package/skill/margaui/components/collapse.md +0 -172
  19. package/skill/margaui/components/countdown.md +0 -165
  20. package/skill/margaui/components/diff.md +0 -53
  21. package/skill/margaui/components/divider.md +0 -107
  22. package/skill/margaui/components/dock.md +0 -173
  23. package/skill/margaui/components/drawer.md +0 -184
  24. package/skill/margaui/components/dropdown.md +0 -388
  25. package/skill/margaui/components/fab.md +0 -346
  26. package/skill/margaui/components/fieldset.md +0 -88
  27. package/skill/margaui/components/file-input.md +0 -84
  28. package/skill/margaui/components/filter.md +0 -52
  29. package/skill/margaui/components/footer.md +0 -583
  30. package/skill/margaui/components/hero.md +0 -135
  31. package/skill/margaui/components/hover-3d.md +0 -129
  32. package/skill/margaui/components/hover-gallery.md +0 -49
  33. package/skill/margaui/components/indicator.md +0 -265
  34. package/skill/margaui/components/input.md +0 -389
  35. package/skill/margaui/components/join.md +0 -100
  36. package/skill/margaui/components/kbd.md +0 -127
  37. package/skill/margaui/components/label.md +0 -102
  38. package/skill/margaui/components/link.md +0 -96
  39. package/skill/margaui/components/list.md +0 -182
  40. package/skill/margaui/components/loading.md +0 -105
  41. package/skill/margaui/components/mask.md +0 -168
  42. package/skill/margaui/components/megamenu.md +0 -131
  43. package/skill/margaui/components/menu.md +0 -887
  44. package/skill/margaui/components/mockup-browser.md +0 -39
  45. package/skill/margaui/components/mockup-code.md +0 -81
  46. package/skill/margaui/components/mockup-phone.md +0 -39
  47. package/skill/margaui/components/mockup-window.md +0 -33
  48. package/skill/margaui/components/modal.md +0 -196
  49. package/skill/margaui/components/navbar.md +0 -282
  50. package/skill/margaui/components/otp.md +0 -171
  51. package/skill/margaui/components/pagination.md +0 -122
  52. package/skill/margaui/components/progress.md +0 -135
  53. package/skill/margaui/components/radial-progress.md +0 -67
  54. package/skill/margaui/components/radio.md +0 -133
  55. package/skill/margaui/components/range.md +0 -134
  56. package/skill/margaui/components/rating.md +0 -170
  57. package/skill/margaui/components/select.md +0 -225
  58. package/skill/margaui/components/skeleton.md +0 -64
  59. package/skill/margaui/components/stack.md +0 -142
  60. package/skill/margaui/components/stat.md +0 -254
  61. package/skill/margaui/components/status.md +0 -73
  62. package/skill/margaui/components/steps.md +0 -138
  63. package/skill/margaui/components/swap.md +0 -152
  64. package/skill/margaui/components/tab.md +0 -248
  65. package/skill/margaui/components/table.md +0 -1018
  66. package/skill/margaui/components/text-rotate.md +0 -91
  67. package/skill/margaui/components/textarea.md +0 -85
  68. package/skill/margaui/components/theme-controller.md +0 -266
  69. package/skill/margaui/components/timeline.md +0 -1356
  70. package/skill/margaui/components/toast.md +0 -165
  71. package/skill/margaui/components/toggle.md +0 -135
  72. package/skill/margaui/components/tooltip.md +0 -181
  73. package/skill/margaui/components/validator.md +0 -163
  74. package/skill/tutuca/SKILL.md +0 -56
  75. package/skill/tutuca/advanced.md +0 -212
  76. package/skill/tutuca/cli.md +0 -239
  77. package/skill/tutuca/component-design.md +0 -168
  78. package/skill/tutuca/core.md +0 -910
  79. package/skill/tutuca/iteration.md +0 -207
  80. package/skill/tutuca/macros.md +0 -86
  81. package/skill/tutuca/margaui.md +0 -175
  82. package/skill/tutuca/messages-and-intents.md +0 -399
  83. package/skill/tutuca/patterns/README.md +0 -48
  84. package/skill/tutuca/patterns/add-a-story.md +0 -26
  85. package/skill/tutuca/patterns/bind-text-and-attributes.md +0 -30
  86. package/skill/tutuca/patterns/conditional-attribute-value.md +0 -29
  87. package/skill/tutuca/patterns/coordinate-components.md +0 -54
  88. package/skill/tutuca/patterns/edit-through-a-dynamic-target.md +0 -27
  89. package/skill/tutuca/patterns/enrich-each-item.md +0 -25
  90. package/skill/tutuca/patterns/file-input.md +0 -39
  91. package/skill/tutuca/patterns/filter-a-list.md +0 -25
  92. package/skill/tutuca/patterns/filter-and-paginate.md +0 -60
  93. package/skill/tutuca/patterns/handle-events.md +0 -38
  94. package/skill/tutuca/patterns/iterate-a-list.md +0 -18
  95. package/skill/tutuca/patterns/paginate-a-list.md +0 -29
  96. package/skill/tutuca/patterns/render-a-child-component.md +0 -21
  97. package/skill/tutuca/patterns/reuse-markup-with-macros.md +0 -36
  98. package/skill/tutuca/patterns/share-state-across-the-tree.md +0 -38
  99. package/skill/tutuca/patterns/show-or-hide-content.md +0 -23
  100. package/skill/tutuca/patterns/switch-between-views.md +0 -30
  101. package/skill/tutuca/patterns/tabbed-interface.md +0 -43
  102. package/skill/tutuca/semantics.md +0 -195
  103. package/skill/tutuca/storybook.md +0 -270
  104. package/skill/tutuca/styles.md +0 -48
  105. package/skill/tutuca/testing.md +0 -346
  106. package/skill/tutuca-source/SKILL.md +0 -33
  107. package/skill/tutuca-source/tutuca.ext.js +0 -4244
@@ -1,910 +0,0 @@
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 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 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 value" />
571
- <input @on.input="setN valueAsInt" />
572
- <button @on.click="pick @key isAlt">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 `pick @key isAlt`
580
- is called as `pick(draft, key, isAlt, ctx)`. You can still write `ctx` in the
581
- template (it resolves to a fresh `EventContext`), but it is redundant.
582
-
583
- Built-in handler argument names: `value`, `valueAsInt`, `valueAsFloat`,
584
- `target`, `event`, `isAlt`, `isShift`, `isCtrl`/`isCmd`, `key`, `keyCode`,
585
- `isUpKey`, `isDownKey`, `isSend`, `isCancel`, `isTabKey`, `ctx`,
586
- `dragInfo`.
587
-
588
- The content of `value` depends on the event source:
589
-
590
- | Source | What `value` resolves to |
591
- |-----------------------------|--------------------------------------------------|
592
- | `<input type="checkbox">` | `event.target.checked` (boolean) |
593
- | `CustomEvent` | `event.detail` |
594
- | anything else | `event.target.value` (string), or null if absent |
595
-
596
- For numeric inputs, prefer `valueAsInt` / `valueAsFloat` to skip the
597
- string parse.
598
-
599
- Ask for the most granular arg the handler actually uses — `value` /
600
- `valueAsInt` / `key`, not the raw `event` — when the specific value is
601
- all you need. A handler that takes `event` forces every test and
602
- storybook story to fabricate a DOM-event-shaped object
603
- (`{ target: { value: … } }`); one that takes `value` is called with a
604
- plain literal. (Genuine exceptions exist — e.g. a file input needs
605
- `event` to reach `event.target.files`.) See
606
- [testing.md](./testing.md) *Designing handlers so tests stay simple*.
607
-
608
- ### Event Modifiers
609
-
610
- `@on.<event>+<mod>+<mod>=...`
611
-
612
- Guards — run the handler only when they hold:
613
-
614
- - All events: `+ctrl`, `+cmd`/`+meta`, `+alt`
615
- - `keydown` only: `+send` (Enter), `+cancel` (Escape)
616
-
617
- Effects — act on the DOM event, on all events:
618
-
619
- - `+prevent` → `event.preventDefault()`
620
- - `+stop` → `event.stopPropagation()`
621
-
622
- ```html
623
- <input @on.keydown+send="submit value" @on.keydown+cancel="reset" />
624
- <button @on.click+ctrl="soloOnly">ctrl-click</button>
625
- <form @on.submit+prevent="save">…</form>
626
- <input @on.keydown+send+prevent="submit value" />
627
- ```
628
-
629
- Effects apply only when every guard on the same handler passed, whatever
630
- order they are written in: `+send+prevent` prevents on Enter and nothing
631
- else. They run from the app's root listener, which is where tutuca listens
632
- — that is invisible to `+prevent` (the default action happens after the
633
- event finishes propagating) but it makes `+stop` narrower than it looks:
634
- it stops listeners *outside* the app root only. DOM ancestors between the
635
- target and the root have already seen the event, and tutuca runs a single
636
- handler per event anyway, so `+stop` never suppresses another `@on`.
637
-
638
- ### Web Components & Custom Events
639
-
640
- Custom elements just work, and any `CustomEvent` they fire is reachable
641
- via `@on.<event-name>`. The event's `detail` surfaces as `value`:
642
-
643
- ```js
644
- import "https://cdn.jsdelivr.net/npm/emoji-picker-element/+esm";
645
-
646
- receive: { onPick(draft, detail) { draft.current = detail.unicode; } }
647
- view: html`<emoji-picker @on.emoji-click="onPick value"></emoji-picker>`,
648
- ```
649
-
650
- Handle these events declaratively with `@on.<event-name>` in the view —
651
- don't grab the node from host/glue code and `addEventListener` on it. A
652
- listener attached from outside the component runs outside the handler
653
- model: no draft transaction, no transactor batching, and the mutation
654
- is invisible to the component that owns the state (the same hazard as
655
- reaching into `app.state` directly). For any event with a real element in
656
- the tree, `@on.` is the only entry point you need. Genuinely external
657
- inbound sources (WebSocket, `postMessage`, timers) have no element to bind
658
- — route those through `app.sendAtRoot` instead (see
659
- [messages-and-intents.md](./messages-and-intents.md)).
660
-
661
- Pitfall: binding a camelCase JS property on a custom element silently
662
- fails — see the lowercasing rules in *Attribute Binding* above.
663
-
664
- ## Conditional Display
665
-
666
- ```html
667
- <div @show=".isLoading">Loading...</div>
668
- <div @hide=".isLoading">content</div>
669
-
670
- <!-- boolean predicates; equals? compares against a string literal -->
671
- <div @show="equals? .view 'detail'">detail view</div>
672
-
673
- <!-- @show / @hide also work as directives on `<x>` render ops:
674
- wraps the produced node, no extra DOM element. Allowed on
675
- text / render / render-it / render-each. First attr in
676
- source order becomes the outermost wrapper. -->
677
- <x text=".name" @show=".isOpen"></x>
678
- <x render-it @hide=".isHidden"></x>
679
- <x render-each=".items" @when="filter" @show=".isOpen"></x>
680
-
681
- <!-- Single @if: shorthand @then/@else (attr inferred) -->
682
- <button @if.class=".isActive" @then="'btn btn-success'" @else="'btn btn-ghost'">
683
- ...
684
- </button>
685
-
686
- <!-- Multiple @if on same element: name the attr explicitly -->
687
- <button
688
- @if.class=".isActive"
689
- @then="'on'"
690
- @else="'off'"
691
- @if.title=".isActive"
692
- @then.title="'On'"
693
- @else.title="'Off'"
694
- >
695
- ...
696
- </button>
697
- ```
698
-
699
- > HTML disallows duplicate attrs, so with multiple `@if.<attr>` on one
700
- > element every `@then`/`@else` after the first **must** include the attr
701
- > name — otherwise the parser drops it before tutuca sees it.
702
-
703
- ## List Iteration & Scope Enrichment
704
-
705
- ```html
706
- <li @each=".items"><span @text="@key"></span>: <x text="@value"></x></li>
707
- <x render-each=".items"></x>
708
- ```
709
-
710
- Auto-bound names inside a loop are `@key` and `@value`. Iteration
711
- (`@each` / `render-each`), filtering (`@when`), item and scope
712
- enrichment (`@enrich-with`), pagination and the `@loop-with` return
713
- shape, and the `@each` lifecycle: see [iteration.md](./iteration.md).
714
-
715
- ## Rendering Components
716
-
717
- ```html
718
- <x render=".item"></x> <!-- default ("main") view -->
719
- <x render=".item" as="edit"></x> <!-- specific view (literal) -->
720
- <x render=".item" as=".mode"></x> <!-- view chosen by a field at runtime -->
721
- <x render-it></x> <!-- only inside @each / render-each -->
722
- <x render=".byIndex[.currentIndex]"></x> <!-- list item access -->
723
- <x render=".byKey[.currentKey]"></x> <!-- map item access -->
724
- <x render="*active"></x> <!-- dynamic binding — see advanced.md -->
725
- <x render=".item" @show=".isOpen"></x> <!-- conditional wrap, see "Conditional Display" -->
726
- ```
727
-
728
- The top-level `view` is registered under `"main"` (the default); extras
729
- go under `views: { name: html\`...\` }`. `as` selects which view of the
730
- rendered component to use, falling back to `main` if absent. It accepts the
731
- same dynamic values as `@push-view` (a literal name like `edit`, or `.field`,
732
- `*dyn`, `@bind`, `$method`, `$'…{x}…'`), evaluated against the **host**
733
- component at render time. `as` only applies to the **direct** component — for
734
- whole-subtree control, use `@push-view` (next section). For `render-each` the
735
- selector is evaluated once against the host, so every item gets the same view.
736
-
737
- ## Multiple Views & View Stack
738
-
739
- ```js
740
- component({
741
- view: html`<p @text=".title"></p>`, // "main"
742
- views: { edit: html`<input :value=".title" @on.input="setTitle value" />` },
743
- });
744
- ```
745
-
746
- ```html
747
- <!-- @push-view pushes a name onto the rendering stack;
748
- descendants resolve to first matching view, falling back to "main" -->
749
- <div @push-view=".view"><x render-each=".items"></x></div>
750
- ```
751
-
752
- | Directive | Scope |
753
- |--------------------|--------------------------------------------------------------------------|
754
- | `as="edit"` / `as=".mode"` | One `<x render>` element only. Literal or dynamic (like `@push-view`), evaluated against the host. |
755
- | `@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. |
756
-
757
- ## Styles
758
-
759
- `style` is scoped to the main view, `commonStyle` to all views of the
760
- component, `globalStyle` is injected unscoped (see the *Component
761
- Skeleton* above). Scoping mechanics, styling the root element with bare
762
- declarations, and the at-rules that must live in `globalStyle`: see
763
- [styles.md](./styles.md). Tailwind / MargaUI utility classes:
764
- [margaui.md](./margaui.md).
765
-
766
- ## Triggers and Handlers
767
-
768
- Tutuca has **two** dispatch channels, and one question separates them:
769
- *does the sender know who handles this?*
770
-
771
- | Triggered by | Handler block | Use for |
772
- | ------------------------------------------------ | ------------------ | ------------------------------------------------ |
773
- | DOM event (`click`, `input`, …) | `receive: { ... }` | the component handling its own events |
774
- | `ctx.send(name)` — message to a target path | `receive: { ... }` | addressing one known component (or self) |
775
- | an answer to an intent this component raised | `receive: { ... }` | `<name>Ok` / `<name>Error` / `<name>Unhandled` |
776
- | `ctx.intent(name, args, opts)` — walks a route | `intent: { ... }` | a job the sender does not address |
777
-
778
- The first three rows are one bucket and **nothing tells them apart** — a
779
- component that answered its own click differently from the identical
780
- `ctx.send` from its parent could be driven neither from a test nor from a
781
- parent.
782
-
783
- An intent carries a **route**: `["dyn"]` walks the ancestors, `["lex"]`
784
- walks the handlers registered on the scope, and the default `["dyn","lex"]`
785
- tries both. The verb does not decide which scope answers — the route does,
786
- written at the call site.
787
-
788
- Every dispatched `receive` or `intent` handler is called as
789
- `handler(draft, ...args, ctx)`. `this` is the immutable current instance;
790
- `draft` is its Immer draft; `ctx` is always trailing. Mutate `draft` and
791
- return nothing (or return `draft`) to commit. Return any other value to swap
792
- the addressed component for that value. Mutating the draft and returning a
793
- replacement in the same handler is an error. An unchanged recipe preserves
794
- the current identity. Routes,
795
- `ctx.reply` / `ctx.fail` / `ctx.forward` / `ctx.stop`, the three
796
- outcomes, `ctx.at`, the `$unknown` fallback, and intent-handler
797
- registration are in [messages-and-intents.md](./messages-and-intents.md);
798
- worked snippets in
799
- [patterns/coordinate-components.md](./patterns/coordinate-components.md).
800
-
801
- `alter` is a third handler block, but it isn't event-triggered — the
802
- renderer invokes alter handlers with their existing read-only signature to
803
- produce binds, not state changes
804
- (see *Mental model*, and *Scope Enrichment* in
805
- [iteration.md](./iteration.md)).
806
-
807
- ## Macros
808
-
809
- Pure template expansion — `macro({ params }, html\`...\`)` definitions
810
- called as `<x:name>`, with `^param` references, slots, and named slots:
811
- see [macros.md](./macros.md). Registry keys are lowercased —
812
- `<x:Card>` resolves as `<x:card>`.
813
-
814
- ## Raw HTML (escape hatch)
815
-
816
- ```html
817
- <div @dangerouslysetinnerhtml=".trustedHtml"></div>
818
- ```
819
-
820
- Bypasses all escaping; children of the element are ignored when active.
821
-
822
- ## Immer Utilities
823
-
824
- Tutuca uses Immer internally but does not add Immer's API to the root export.
825
- Import recipe utilities explicitly when tests or host code need them:
826
-
827
- ```js
828
- import { produce, immerable } from "tutuca/immer";
829
- ```
830
-
831
- Generated component classes are already draftable. Add `[immerable] = true`
832
- to custom classes stored in state. Native `Map` and `Set` support is enabled by
833
- the `tutuca/immer` entry point.
834
-
835
- ## Conventional Module Exports
836
-
837
- Examples and the storybook glue follow this shape so files compose freely
838
- and the `tutuca` CLI can introspect any module without per-app glue:
839
-
840
- ```js
841
- export function getComponents() { return [Comp, ...]; }
842
- export function getMacros() { return { name: macro }; } // optional
843
- export function getIntentHandlers() { return { name: async fn }; } // optional
844
- export function getRoot() { return Root.make({...}); }
845
- export function getExamples() {
846
- // Return one section, or an array of sections.
847
- return {
848
- title: "...",
849
- description: "...",
850
- // value = Comp.make(...); intentHandlers (optional) mocks this example's requests
851
- items: [{ title, description, value, view, intentHandlers }],
852
- };
853
- }
854
- export function getTests({ describe, test, expect }) { /*...*/ } // optional — see cli.md
855
- ```
856
-
857
- An example item may carry an optional **`intentHandlers`** map — per-example
858
- mocks (keyed by request name) that override the module's real
859
- `getIntentHandlers()` for that one instance, so two examples of the same
860
- component show different responses side by side. Return a fixture, `throw` for
861
- the error path, or never resolve to hold a loading state. Full treatment, plus
862
- the `on` lifecycle hooks, in [storybook.md](./storybook.md).
863
-
864
- Best practice: have `getComponents()` return **every** component the module
865
- defines — child and helper components included — and give each one at least
866
- one item in `getExamples()`. A component left out of `getComponents()` is
867
- invisible to `tutuca lint`/`render`/`test`, so it silently loses linting and
868
- render coverage. If your components already live behind a differently named
869
- export, alias it instead of teaching tools a new name:
870
-
871
- ```js
872
- export { allMyComponents as getComponents } from "./app.js";
873
- ```
874
-
875
- Put these exports in a co-located **`*.dev.js`** file (a dev-only module
876
- holding stories + tests, never shipped) and `tutuca storybook` auto-discovers
877
- and renders them with no setup — see [cli.md](./cli.md). The same shape is
878
- consumed by the shipped `tutuca/storybook` library if you want to embed a
879
- storybook in your own page — see [storybook.md](./storybook.md).
880
-
881
- ## See also
882
-
883
- - [iteration.md](./iteration.md) — `@each` / `render-each`, `@when`,
884
- `@enrich-with`, `@loop-with` pagination, and the loop lifecycle.
885
- - [macros.md](./macros.md) — `macro()` definitions, `<x:name>` calls,
886
- slots, and registration.
887
- - [styles.md](./styles.md) — `style` / `commonStyle` / `globalStyle`
888
- scoping mechanics and pitfalls.
889
- - [component-design.md](./component-design.md) — design judgment for shaping a
890
- feature into components: responsibilities, where state lives, which channel to
891
- reach for, and a curated do's & don'ts list.
892
- - [messages-and-intents.md](./messages-and-intents.md) — addressed
893
- `send`-`receive` vs routed `intent`, the `ctx.at` `PathBuilder`, `$unknown`, and
894
- request-handler registration.
895
- - [advanced.md](./advanced.md) — dynamic bindings (`*x`), pseudo-`@x` for
896
- `<select>` / `<table>` / `<tr>`, drag & drop, custom seq types.
897
- - [margaui.md](./margaui.md) — setting up MargaUI styling: install
898
- (CDN / npm / vendoring), theme CSS, and `compileClassesToStyleText`.
899
- - [semantics.md](./semantics.md) — runtime semantics: path steps, the
900
- transaction lifecycle, dyn-var teleporting, and async key pinning
901
- (`livePath`).
902
- - [testing.md](./testing.md) — `getTests` shape and the handler calling
903
- convention for tests.
904
- - [storybook.md](./storybook.md) — authoring `*.dev.js` story modules and
905
- running / embedding the storybook.
906
- - [cli.md](./cli.md) — commands, flags, exit codes, and the full linter rule
907
- list.
908
- - [patterns/README.md](./patterns/README.md) — task-oriented recipes ("how do I
909
- iterate / filter / paginate / show-hide / build tabs / share state / …"),
910
- each linking back here and to a runnable example.