tutuca 0.11.0 → 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 (116) hide show
  1. package/dist/tutuca-cli.js +264 -361
  2. package/dist/tutuca-components.js +175 -175
  3. package/dist/tutuca-dev.ext.js +250 -323
  4. package/dist/tutuca-dev.js +250 -323
  5. package/dist/tutuca-dev.min.js +4 -4
  6. package/dist/tutuca-extra.ext.js +55 -46
  7. package/dist/tutuca-extra.js +55 -46
  8. package/dist/tutuca-extra.min.js +3 -3
  9. package/dist/tutuca-storybook.js +29 -29
  10. package/dist/tutuca.ext.js +55 -46
  11. package/dist/tutuca.js +55 -46
  12. package/dist/tutuca.min.js +3 -3
  13. package/package.json +4 -3
  14. package/skill/margaui/SKILL.md +0 -105
  15. package/skill/margaui/components/accordion.md +0 -127
  16. package/skill/margaui/components/alert.md +0 -174
  17. package/skill/margaui/components/aura.md +0 -97
  18. package/skill/margaui/components/avatar.md +0 -220
  19. package/skill/margaui/components/badge.md +0 -193
  20. package/skill/margaui/components/breadcrumbs.md +0 -103
  21. package/skill/margaui/components/button.md +0 -322
  22. package/skill/margaui/components/calendar.md +0 -67
  23. package/skill/margaui/components/card.md +0 -373
  24. package/skill/margaui/components/carousel.md +0 -387
  25. package/skill/margaui/components/chat.md +0 -171
  26. package/skill/margaui/components/checkbox.md +0 -101
  27. package/skill/margaui/components/collapse.md +0 -172
  28. package/skill/margaui/components/countdown.md +0 -165
  29. package/skill/margaui/components/diff.md +0 -53
  30. package/skill/margaui/components/divider.md +0 -107
  31. package/skill/margaui/components/dock.md +0 -173
  32. package/skill/margaui/components/drawer.md +0 -184
  33. package/skill/margaui/components/dropdown.md +0 -388
  34. package/skill/margaui/components/fab.md +0 -346
  35. package/skill/margaui/components/fieldset.md +0 -88
  36. package/skill/margaui/components/file-input.md +0 -84
  37. package/skill/margaui/components/filter.md +0 -52
  38. package/skill/margaui/components/footer.md +0 -583
  39. package/skill/margaui/components/hero.md +0 -135
  40. package/skill/margaui/components/hover-3d.md +0 -129
  41. package/skill/margaui/components/hover-gallery.md +0 -49
  42. package/skill/margaui/components/indicator.md +0 -265
  43. package/skill/margaui/components/input.md +0 -389
  44. package/skill/margaui/components/join.md +0 -100
  45. package/skill/margaui/components/kbd.md +0 -127
  46. package/skill/margaui/components/label.md +0 -102
  47. package/skill/margaui/components/link.md +0 -96
  48. package/skill/margaui/components/list.md +0 -182
  49. package/skill/margaui/components/loading.md +0 -105
  50. package/skill/margaui/components/mask.md +0 -168
  51. package/skill/margaui/components/megamenu.md +0 -131
  52. package/skill/margaui/components/menu.md +0 -887
  53. package/skill/margaui/components/mockup-browser.md +0 -39
  54. package/skill/margaui/components/mockup-code.md +0 -81
  55. package/skill/margaui/components/mockup-phone.md +0 -39
  56. package/skill/margaui/components/mockup-window.md +0 -33
  57. package/skill/margaui/components/modal.md +0 -196
  58. package/skill/margaui/components/navbar.md +0 -282
  59. package/skill/margaui/components/otp.md +0 -171
  60. package/skill/margaui/components/pagination.md +0 -122
  61. package/skill/margaui/components/progress.md +0 -135
  62. package/skill/margaui/components/radial-progress.md +0 -67
  63. package/skill/margaui/components/radio.md +0 -133
  64. package/skill/margaui/components/range.md +0 -134
  65. package/skill/margaui/components/rating.md +0 -170
  66. package/skill/margaui/components/select.md +0 -225
  67. package/skill/margaui/components/skeleton.md +0 -64
  68. package/skill/margaui/components/stack.md +0 -142
  69. package/skill/margaui/components/stat.md +0 -254
  70. package/skill/margaui/components/status.md +0 -73
  71. package/skill/margaui/components/steps.md +0 -138
  72. package/skill/margaui/components/swap.md +0 -152
  73. package/skill/margaui/components/tab.md +0 -248
  74. package/skill/margaui/components/table.md +0 -1018
  75. package/skill/margaui/components/text-rotate.md +0 -91
  76. package/skill/margaui/components/textarea.md +0 -85
  77. package/skill/margaui/components/theme-controller.md +0 -266
  78. package/skill/margaui/components/timeline.md +0 -1356
  79. package/skill/margaui/components/toast.md +0 -165
  80. package/skill/margaui/components/toggle.md +0 -135
  81. package/skill/margaui/components/tooltip.md +0 -181
  82. package/skill/margaui/components/validator.md +0 -163
  83. package/skill/tutuca/SKILL.md +0 -56
  84. package/skill/tutuca/advanced.md +0 -212
  85. package/skill/tutuca/cli.md +0 -239
  86. package/skill/tutuca/component-design.md +0 -168
  87. package/skill/tutuca/core.md +0 -908
  88. package/skill/tutuca/iteration.md +0 -207
  89. package/skill/tutuca/macros.md +0 -86
  90. package/skill/tutuca/margaui.md +0 -175
  91. package/skill/tutuca/messages-and-intents.md +0 -399
  92. package/skill/tutuca/patterns/README.md +0 -48
  93. package/skill/tutuca/patterns/add-a-story.md +0 -26
  94. package/skill/tutuca/patterns/bind-text-and-attributes.md +0 -30
  95. package/skill/tutuca/patterns/conditional-attribute-value.md +0 -29
  96. package/skill/tutuca/patterns/coordinate-components.md +0 -54
  97. package/skill/tutuca/patterns/edit-through-a-dynamic-target.md +0 -27
  98. package/skill/tutuca/patterns/enrich-each-item.md +0 -25
  99. package/skill/tutuca/patterns/file-input.md +0 -39
  100. package/skill/tutuca/patterns/filter-a-list.md +0 -25
  101. package/skill/tutuca/patterns/filter-and-paginate.md +0 -60
  102. package/skill/tutuca/patterns/handle-events.md +0 -38
  103. package/skill/tutuca/patterns/iterate-a-list.md +0 -18
  104. package/skill/tutuca/patterns/paginate-a-list.md +0 -29
  105. package/skill/tutuca/patterns/render-a-child-component.md +0 -21
  106. package/skill/tutuca/patterns/reuse-markup-with-macros.md +0 -36
  107. package/skill/tutuca/patterns/share-state-across-the-tree.md +0 -38
  108. package/skill/tutuca/patterns/show-or-hide-content.md +0 -23
  109. package/skill/tutuca/patterns/switch-between-views.md +0 -30
  110. package/skill/tutuca/patterns/tabbed-interface.md +0 -43
  111. package/skill/tutuca/semantics.md +0 -195
  112. package/skill/tutuca/storybook.md +0 -270
  113. package/skill/tutuca/styles.md +0 -48
  114. package/skill/tutuca/testing.md +0 -346
  115. package/skill/tutuca-source/SKILL.md +0 -33
  116. package/skill/tutuca-source/tutuca.ext.js +0 -4235
@@ -1,908 +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 bubble" 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
- Available as `Comp.Class.<name>(...)` alongside the auto-generated
449
- `Comp.Class.make(...)` (which `Comp.make(...)` aliases). Inside a static,
450
- `this` is the class itself.
451
-
452
- Common use: a `fromData` factory that recursively builds instances from
453
- plain JS data:
454
-
455
- ```js
456
- statics: {
457
- fromData({ items = [] }) {
458
- return this.make({ items: items.map((v) => Item.Class.fromData(v)) });
459
- },
460
- }
461
- // usage: TreeRoot.Class.fromData([...])
462
- ```
463
-
464
- > **Scopes own the `Class`.** A component is bound to a scope at
465
- > `registerComponents` time — that scope owns its `Class`, component tag,
466
- > and scope-bound `make`/statics — so a given component object is live in
467
- > one scope at a time. Each app/registry is a separate scope; the same
468
- > *object* registered into two of them rebinds (last wins). To run the
469
- > same definition in two genuinely separate registries at once, build an
470
- > independent copy with `component(Comp.spec)` (new id ⇒ separately
471
- > compiled CSS + separate identity — the price of isolation, not a way to
472
- > dedupe within one app). For reuse inside one scope, register the single
473
- > object once and re-export it.
474
-
475
- **Multi-scope caveat for statics.** A static like `fromData` that builds
476
- a *different* child type by naming the imported const directly
477
- (`Item.Class.fromData(v)` above) hardcodes the child's *original* scope.
478
- Fine in a single-scope app; across scopes, resolve the child through the
479
- caller's scope instead — `this.scope.lookupComponent("Item")` — so it
480
- deserializes into the right one. Recursion into the *same* type needs no
481
- lookup: `this.fromData(v)` / `this.make` already target the caller's scope.
482
-
483
- ## Text Rendering
484
-
485
- ```html
486
- <span @text=".str"></span> <!-- prepend text into span -->
487
- <x text=".bool"></x> <!-- text-only, no DOM element -->
488
- <x text="$getStrUpper"></x> <!-- $ calls a method -->
489
- <x text="@value"></x> <!-- loop binding -->
490
- ```
491
-
492
- Use `@text` when you already have a host element to put the text in; use
493
- `<x text=…>` for bare text with no wrapping element (e.g. text interleaved with
494
- other inline content, or a loop binding). Both take the same value forms
495
- (`.field`, `$method`, `@binding`).
496
-
497
- ## Attribute Binding
498
-
499
- ```html
500
- <input :value=".str" @on.input="setStr value" />
501
- <a :href=".url" :title="$'Hi {.name}'">link</a> <!-- string template -->
502
- <button :class="$'btn {.color}'">x</button>
503
- ```
504
-
505
- Plain attrs are static. `:attr="..."` is a dynamic expression. Boolean
506
- HTML attributes (`disabled`, `checked`, `hidden`, …) are auto-recognized;
507
- pass a boolean field.
508
-
509
- A static `class="…"` and a dynamic `:class`/`@if.class` **cannot coexist on the
510
- same element** — setting one attribute two ways is a lint error
511
- (`DUPLICATE_ATTR_DEFINITION`), and at runtime the dynamic value wins and the
512
- static class is dropped. Fold any structural classes into the bound expression,
513
- e.g. `:class="$'btn {.color}'"` (note `btn` is part of the template, not a
514
- separate `class="btn"`). The same applies to other attributes — see the
515
- duplicate-attribute note below.
516
-
517
- The HTML parser lowercases attribute names before Tutuca sees them, so
518
- `:mapId` arrives as `:mapid` and `<x:Card>` becomes `<x:card>`. Three
519
- consequences:
520
-
521
- - SVG attributes are case-sensitive. Tutuca special-cases `:viewbox` →
522
- `viewBox` so SVG roots work; for other camelCased SVG attrs, wrap them
523
- in components that emit raw markup.
524
- - Custom-element property setters defined in camelCase **will not fire**.
525
- `:mapId=".mapId"` runs `node.mapid = value`; if the
526
- element defined `set mapId(...)`, the lookup misses and JS silently
527
- creates an own data property `mapid` on the element instead of invoking
528
- the setter — no error, no warning, the bound state stays null. Author
529
- custom elements with kebab-case attributes plus lowercased property
530
- setters (or aliases), and bind via `:kebab-name` from Tutuca templates.
531
- - Macro registry keys are lowercased on insert for the same reason
532
- (see [macros.md](./macros.md)).
533
-
534
- Tutuca auto-namespaces by subtree: elements inside `<svg>` get the SVG
535
- namespace and elements inside `<math>` get MathML, with spec-cased local
536
- names preserved (`linearGradient`, `viewBox`). A `<foreignObject>` switches
537
- its children back to the HTML namespace. Customised built-in elements work
538
- via `is="..."` (e.g. `<button is="x-fancy">`); `is` is applied when the
539
- element is created, so it must be a static attribute — setting it later
540
- does not upgrade the element.
541
-
542
- ### When nothing renders (or renders unstyled)
543
-
544
- A few mistakes fail quietly — no error, just a blank or unstyled result, which
545
- is the slowest kind to debug. **Run `tutuca lint <module>` first**: it catches
546
- several of these. The usual suspects:
547
-
548
- - **Unparseable attribute value** → the attribute is silently dropped. A bare
549
- multi-word value isn't a string — quote it (`:label="'two words'"`) or make it
550
- a template (`:label="$'{.a} {.b}'"`). Lint flags this as `BAD_VALUE`.
551
- - **camelCase attribute on a custom element** → setter no-op (see the lowercasing
552
- note above). Use kebab-case attributes. Not lintable — the HTML parser
553
- lowercases the name before either Tutuca or the linter sees it.
554
- - **Forgotten margaui `_palette`/decoy view** → classes assembled in methods or
555
- interpolations render unstyled. See [margaui.md](./margaui.md). Not lintable.
556
- - **A whitespace-only `html\`\``** → blank render. A *leading* newline before the
557
- root element is fine (the parser trims it); a template with no element at all
558
- is not.
559
-
560
- ## Event Handling
561
-
562
- ```html
563
- <!-- every event names a receive handler (no prefix) -->
564
- <button @on.click="inc">+</button>
565
- <button @on.click="dec">-</button>
566
-
567
- <!-- pass args by name -->
568
- <input @on.input="setStr value" />
569
- <input @on.input="setN valueAsInt" />
570
- <button @on.click="pick @key isAlt">pick</button>
571
- <button @on.click="addItem JsonSelector">+</button> <!-- type as arg -->
572
- <button @on.click="loadAnotherWay">load</button> <!-- ctx auto-appended -->
573
- ```
574
-
575
- Every `@on.<event>` handler receives an `EventContext` as its trailing
576
- arg automatically — written args come first, `ctx` last. So
577
- `loadAnotherWay` is called as `loadAnotherWay(draft, ctx)`, and `pick @key isAlt`
578
- is called as `pick(draft, key, isAlt, ctx)`. You can still write `ctx` in the
579
- template (it resolves to a fresh `EventContext`), but it is redundant.
580
-
581
- Built-in handler argument names: `value`, `valueAsInt`, `valueAsFloat`,
582
- `target`, `event`, `isAlt`, `isShift`, `isCtrl`/`isCmd`, `key`, `keyCode`,
583
- `isUpKey`, `isDownKey`, `isSend`, `isCancel`, `isTabKey`, `ctx`,
584
- `dragInfo`.
585
-
586
- The content of `value` depends on the event source:
587
-
588
- | Source | What `value` resolves to |
589
- |-----------------------------|--------------------------------------------------|
590
- | `<input type="checkbox">` | `event.target.checked` (boolean) |
591
- | `CustomEvent` | `event.detail` |
592
- | anything else | `event.target.value` (string), or null if absent |
593
-
594
- For numeric inputs, prefer `valueAsInt` / `valueAsFloat` to skip the
595
- string parse.
596
-
597
- Ask for the most granular arg the handler actually uses — `value` /
598
- `valueAsInt` / `key`, not the raw `event` — when the specific value is
599
- all you need. A handler that takes `event` forces every test and
600
- storybook story to fabricate a DOM-event-shaped object
601
- (`{ target: { value: … } }`); one that takes `value` is called with a
602
- plain literal. (Genuine exceptions exist — e.g. a file input needs
603
- `event` to reach `event.target.files`.) See
604
- [testing.md](./testing.md) *Designing handlers so tests stay simple*.
605
-
606
- ### Event Modifiers
607
-
608
- `@on.<event>+<mod>+<mod>=...`
609
-
610
- Guards — run the handler only when they hold:
611
-
612
- - All events: `+ctrl`, `+cmd`/`+meta`, `+alt`
613
- - `keydown` only: `+send` (Enter), `+cancel` (Escape)
614
-
615
- Effects — act on the DOM event, on all events:
616
-
617
- - `+prevent` → `event.preventDefault()`
618
- - `+stop` → `event.stopPropagation()`
619
-
620
- ```html
621
- <input @on.keydown+send="submit value" @on.keydown+cancel="reset" />
622
- <button @on.click+ctrl="soloOnly">ctrl-click</button>
623
- <form @on.submit+prevent="save">…</form>
624
- <input @on.keydown+send+prevent="submit value" />
625
- ```
626
-
627
- Effects apply only when every guard on the same handler passed, whatever
628
- order they are written in: `+send+prevent` prevents on Enter and nothing
629
- else. They run from the app's root listener, which is where tutuca listens
630
- — that is invisible to `+prevent` (the default action happens after the
631
- event finishes propagating) but it makes `+stop` narrower than it looks:
632
- it stops listeners *outside* the app root only. DOM ancestors between the
633
- target and the root have already seen the event, and tutuca runs a single
634
- handler per event anyway, so `+stop` never suppresses another `@on`.
635
-
636
- ### Web Components & Custom Events
637
-
638
- Custom elements just work, and any `CustomEvent` they fire is reachable
639
- via `@on.<event-name>`. The event's `detail` surfaces as `value`:
640
-
641
- ```js
642
- import "https://cdn.jsdelivr.net/npm/emoji-picker-element/+esm";
643
-
644
- receive: { onPick(draft, detail) { draft.current = detail.unicode; } }
645
- view: html`<emoji-picker @on.emoji-click="onPick value"></emoji-picker>`,
646
- ```
647
-
648
- Handle these events declaratively with `@on.<event-name>` in the view —
649
- don't grab the node from host/glue code and `addEventListener` on it. A
650
- listener attached from outside the component runs outside the handler
651
- model: no draft transaction, no transactor batching, and the mutation
652
- is invisible to the component that owns the state (the same hazard as
653
- reaching into `app.state` directly). For any event with a real element in
654
- the tree, `@on.` is the only entry point you need. Genuinely external
655
- inbound sources (WebSocket, `postMessage`, timers) have no element to bind
656
- — route those through `app.sendAtRoot` instead (see
657
- [messages-and-intents.md](./messages-and-intents.md)).
658
-
659
- Pitfall: binding a camelCase JS property on a custom element silently
660
- fails — see the lowercasing rules in *Attribute Binding* above.
661
-
662
- ## Conditional Display
663
-
664
- ```html
665
- <div @show=".isLoading">Loading...</div>
666
- <div @hide=".isLoading">content</div>
667
-
668
- <!-- boolean predicates; equals? compares against a string literal -->
669
- <div @show="equals? .view 'detail'">detail view</div>
670
-
671
- <!-- @show / @hide also work as directives on `<x>` render ops:
672
- wraps the produced node, no extra DOM element. Allowed on
673
- text / render / render-it / render-each. First attr in
674
- source order becomes the outermost wrapper. -->
675
- <x text=".name" @show=".isOpen"></x>
676
- <x render-it @hide=".isHidden"></x>
677
- <x render-each=".items" @when="filter" @show=".isOpen"></x>
678
-
679
- <!-- Single @if: shorthand @then/@else (attr inferred) -->
680
- <button @if.class=".isActive" @then="'btn btn-success'" @else="'btn btn-ghost'">
681
- ...
682
- </button>
683
-
684
- <!-- Multiple @if on same element: name the attr explicitly -->
685
- <button
686
- @if.class=".isActive"
687
- @then="'on'"
688
- @else="'off'"
689
- @if.title=".isActive"
690
- @then.title="'On'"
691
- @else.title="'Off'"
692
- >
693
- ...
694
- </button>
695
- ```
696
-
697
- > HTML disallows duplicate attrs, so with multiple `@if.<attr>` on one
698
- > element every `@then`/`@else` after the first **must** include the attr
699
- > name — otherwise the parser drops it before tutuca sees it.
700
-
701
- ## List Iteration & Scope Enrichment
702
-
703
- ```html
704
- <li @each=".items"><span @text="@key"></span>: <x text="@value"></x></li>
705
- <x render-each=".items"></x>
706
- ```
707
-
708
- Auto-bound names inside a loop are `@key` and `@value`. Iteration
709
- (`@each` / `render-each`), filtering (`@when`), item and scope
710
- enrichment (`@enrich-with`), pagination and the `@loop-with` return
711
- shape, and the `@each` lifecycle: see [iteration.md](./iteration.md).
712
-
713
- ## Rendering Components
714
-
715
- ```html
716
- <x render=".item"></x> <!-- default ("main") view -->
717
- <x render=".item" as="edit"></x> <!-- specific view (literal) -->
718
- <x render=".item" as=".mode"></x> <!-- view chosen by a field at runtime -->
719
- <x render-it></x> <!-- only inside @each / render-each -->
720
- <x render=".byIndex[.currentIndex]"></x> <!-- list item access -->
721
- <x render=".byKey[.currentKey]"></x> <!-- map item access -->
722
- <x render="*active"></x> <!-- dynamic binding — see advanced.md -->
723
- <x render=".item" @show=".isOpen"></x> <!-- conditional wrap, see "Conditional Display" -->
724
- ```
725
-
726
- The top-level `view` is registered under `"main"` (the default); extras
727
- go under `views: { name: html\`...\` }`. `as` selects which view of the
728
- rendered component to use, falling back to `main` if absent. It accepts the
729
- same dynamic values as `@push-view` (a literal name like `edit`, or `.field`,
730
- `*dyn`, `@bind`, `$method`, `$'…{x}…'`), evaluated against the **host**
731
- component at render time. `as` only applies to the **direct** component — for
732
- whole-subtree control, use `@push-view` (next section). For `render-each` the
733
- selector is evaluated once against the host, so every item gets the same view.
734
-
735
- ## Multiple Views & View Stack
736
-
737
- ```js
738
- component({
739
- view: html`<p @text=".title"></p>`, // "main"
740
- views: { edit: html`<input :value=".title" @on.input="setTitle value" />` },
741
- });
742
- ```
743
-
744
- ```html
745
- <!-- @push-view pushes a name onto the rendering stack;
746
- descendants resolve to first matching view, falling back to "main" -->
747
- <div @push-view=".view"><x render-each=".items"></x></div>
748
- ```
749
-
750
- | Directive | Scope |
751
- |--------------------|--------------------------------------------------------------------------|
752
- | `as="edit"` / `as=".mode"` | One `<x render>` element only. Literal or dynamic (like `@push-view`), evaluated against the host. |
753
- | `@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. |
754
-
755
- ## Styles
756
-
757
- `style` is scoped to the main view, `commonStyle` to all views of the
758
- component, `globalStyle` is injected unscoped (see the *Component
759
- Skeleton* above). Scoping mechanics, styling the root element with bare
760
- declarations, and the at-rules that must live in `globalStyle`: see
761
- [styles.md](./styles.md). Tailwind / MargaUI utility classes:
762
- [margaui.md](./margaui.md).
763
-
764
- ## Triggers and Handlers
765
-
766
- Tutuca has **two** dispatch channels, and one question separates them:
767
- *does the sender know who handles this?*
768
-
769
- | Triggered by | Handler block | Use for |
770
- | ------------------------------------------------ | ------------------ | ------------------------------------------------ |
771
- | DOM event (`click`, `input`, …) | `receive: { ... }` | the component handling its own events |
772
- | `ctx.send(name)` — message to a target path | `receive: { ... }` | addressing one known component (or self) |
773
- | an answer to an intent this component raised | `receive: { ... }` | `<name>Ok` / `<name>Error` / `<name>Unhandled` |
774
- | `ctx.intent(name, args, opts)` — walks a route | `intent: { ... }` | a job the sender does not address |
775
-
776
- The first three rows are one bucket and **nothing tells them apart** — a
777
- component that answered its own click differently from the identical
778
- `ctx.send` from its parent could be driven neither from a test nor from a
779
- parent.
780
-
781
- An intent carries a **route**: `["dyn"]` walks the ancestors, `["lex"]`
782
- walks the handlers registered on the scope, and the default `["dyn","lex"]`
783
- tries both. The verb does not decide which scope answers — the route does,
784
- written at the call site.
785
-
786
- Every dispatched `receive` or `intent` handler is called as
787
- `handler(draft, ...args, ctx)`. `this` is the immutable current instance;
788
- `draft` is its Immer draft; `ctx` is always trailing. Mutate `draft` and
789
- return nothing (or return `draft`) to commit. Return any other value to swap
790
- the addressed component for that value. Mutating the draft and returning a
791
- replacement in the same handler is an error. An unchanged recipe preserves
792
- the current identity. Routes,
793
- `ctx.reply` / `ctx.fail` / `ctx.forward` / `ctx.stop`, the three
794
- outcomes, `ctx.at`, the `$unknown` fallback, and intent-handler
795
- registration are in [messages-and-intents.md](./messages-and-intents.md);
796
- worked snippets in
797
- [patterns/coordinate-components.md](./patterns/coordinate-components.md).
798
-
799
- `alter` is a third handler block, but it isn't event-triggered — the
800
- renderer invokes alter handlers with their existing read-only signature to
801
- produce binds, not state changes
802
- (see *Mental model*, and *Scope Enrichment* in
803
- [iteration.md](./iteration.md)).
804
-
805
- ## Macros
806
-
807
- Pure template expansion — `macro({ params }, html\`...\`)` definitions
808
- called as `<x:name>`, with `^param` references, slots, and named slots:
809
- see [macros.md](./macros.md). Registry keys are lowercased —
810
- `<x:Card>` resolves as `<x:card>`.
811
-
812
- ## Raw HTML (escape hatch)
813
-
814
- ```html
815
- <div @dangerouslysetinnerhtml=".trustedHtml"></div>
816
- ```
817
-
818
- Bypasses all escaping; children of the element are ignored when active.
819
-
820
- ## Immer Utilities
821
-
822
- Tutuca uses Immer internally but does not add Immer's API to the root export.
823
- Import recipe utilities explicitly when tests or host code need them:
824
-
825
- ```js
826
- import { produce, immerable } from "tutuca/immer";
827
- ```
828
-
829
- Generated component classes are already draftable. Add `[immerable] = true`
830
- to custom classes stored in state. Native `Map` and `Set` support is enabled by
831
- the `tutuca/immer` entry point.
832
-
833
- ## Conventional Module Exports
834
-
835
- Examples and the storybook glue follow this shape so files compose freely
836
- and the `tutuca` CLI can introspect any module without per-app glue:
837
-
838
- ```js
839
- export function getComponents() { return [Comp, ...]; }
840
- export function getMacros() { return { name: macro }; } // optional
841
- export function getIntentHandlers() { return { name: async fn }; } // optional
842
- export function getRoot() { return Root.make({...}); }
843
- export function getExamples() {
844
- // Return one section, or an array of sections.
845
- return {
846
- title: "...",
847
- description: "...",
848
- // value = Comp.make(...); intentHandlers (optional) mocks this example's requests
849
- items: [{ title, description, value, view, intentHandlers }],
850
- };
851
- }
852
- export function getTests({ describe, test, expect }) { /*...*/ } // optional — see cli.md
853
- ```
854
-
855
- An example item may carry an optional **`intentHandlers`** map — per-example
856
- mocks (keyed by request name) that override the module's real
857
- `getIntentHandlers()` for that one instance, so two examples of the same
858
- component show different responses side by side. Return a fixture, `throw` for
859
- the error path, or never resolve to hold a loading state. Full treatment, plus
860
- the `on` lifecycle hooks, in [storybook.md](./storybook.md).
861
-
862
- Best practice: have `getComponents()` return **every** component the module
863
- defines — child and helper components included — and give each one at least
864
- one item in `getExamples()`. A component left out of `getComponents()` is
865
- invisible to `tutuca lint`/`render`/`test`, so it silently loses linting and
866
- render coverage. If your components already live behind a differently named
867
- export, alias it instead of teaching tools a new name:
868
-
869
- ```js
870
- export { allMyComponents as getComponents } from "./app.js";
871
- ```
872
-
873
- Put these exports in a co-located **`*.dev.js`** file (a dev-only module
874
- holding stories + tests, never shipped) and `tutuca storybook` auto-discovers
875
- and renders them with no setup — see [cli.md](./cli.md). The same shape is
876
- consumed by the shipped `tutuca/storybook` library if you want to embed a
877
- storybook in your own page — see [storybook.md](./storybook.md).
878
-
879
- ## See also
880
-
881
- - [iteration.md](./iteration.md) — `@each` / `render-each`, `@when`,
882
- `@enrich-with`, `@loop-with` pagination, and the loop lifecycle.
883
- - [macros.md](./macros.md) — `macro()` definitions, `<x:name>` calls,
884
- slots, and registration.
885
- - [styles.md](./styles.md) — `style` / `commonStyle` / `globalStyle`
886
- scoping mechanics and pitfalls.
887
- - [component-design.md](./component-design.md) — design judgment for shaping a
888
- feature into components: responsibilities, where state lives, which channel to
889
- reach for, and a curated do's & don'ts list.
890
- - [messages-and-intents.md](./messages-and-intents.md) — addressed
891
- `send`-`receive` vs routed `intent`, the `ctx.at` `PathBuilder`, `$unknown`, and
892
- request-handler registration.
893
- - [advanced.md](./advanced.md) — dynamic bindings (`*x`), pseudo-`@x` for
894
- `<select>` / `<table>` / `<tr>`, drag & drop, custom seq types.
895
- - [margaui.md](./margaui.md) — setting up MargaUI styling: install
896
- (CDN / npm / vendoring), theme CSS, and `compileClassesToStyleText`.
897
- - [semantics.md](./semantics.md) — runtime semantics: path steps, the
898
- transaction lifecycle, dyn-var teleporting, and async key pinning
899
- (`livePath`).
900
- - [testing.md](./testing.md) — `getTests` shape and the handler calling
901
- convention for tests.
902
- - [storybook.md](./storybook.md) — authoring `*.dev.js` story modules and
903
- running / embedding the storybook.
904
- - [cli.md](./cli.md) — commands, flags, exit codes, and the full linter rule
905
- list.
906
- - [patterns/README.md](./patterns/README.md) — task-oriented recipes ("how do I
907
- iterate / filter / paginate / show-hide / build tabs / share state / …"),
908
- each linking back here and to a runnable example.