tutuca 0.9.78 → 0.9.80

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 (33) hide show
  1. package/README.md +1 -1
  2. package/dist/tutuca-cli.js +132 -58
  3. package/dist/tutuca-dev.ext.js +155 -83
  4. package/dist/tutuca-dev.js +155 -83
  5. package/dist/tutuca-dev.min.js +3 -3
  6. package/dist/tutuca-extra.ext.js +92 -72
  7. package/dist/tutuca-extra.js +92 -72
  8. package/dist/tutuca-extra.min.js +3 -3
  9. package/dist/tutuca.ext.js +93 -73
  10. package/dist/tutuca.js +92 -72
  11. package/dist/tutuca.min.js +3 -3
  12. package/package.json +1 -1
  13. package/skill/tutuca/SKILL.md +4 -3
  14. package/skill/tutuca/advanced.md +22 -18
  15. package/skill/tutuca/core.md +57 -2
  16. package/skill/tutuca/patterns/README.md +42 -0
  17. package/skill/tutuca/patterns/bind-text-and-attributes.md +30 -0
  18. package/skill/tutuca/patterns/conditional-attribute-value.md +29 -0
  19. package/skill/tutuca/patterns/coordinate-components.md +26 -0
  20. package/skill/tutuca/patterns/edit-through-a-dynamic-target.md +27 -0
  21. package/skill/tutuca/patterns/enrich-each-item.md +25 -0
  22. package/skill/tutuca/patterns/filter-a-list.md +23 -0
  23. package/skill/tutuca/patterns/handle-events.md +27 -0
  24. package/skill/tutuca/patterns/iterate-a-list.md +18 -0
  25. package/skill/tutuca/patterns/paginate-a-list.md +27 -0
  26. package/skill/tutuca/patterns/render-a-child-component.md +20 -0
  27. package/skill/tutuca/patterns/reuse-markup-with-macros.md +35 -0
  28. package/skill/tutuca/patterns/share-state-without-prop-drilling.md +30 -0
  29. package/skill/tutuca/patterns/show-or-hide-content.md +22 -0
  30. package/skill/tutuca/patterns/switch-between-views.md +26 -0
  31. package/skill/tutuca/patterns/tabbed-interface.md +38 -0
  32. package/skill/tutuca/semantics.md +2 -2
  33. package/skill/tutuca-source/tutuca.ext.js +93 -73
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tutuca",
3
- "version": "0.9.78",
3
+ "version": "0.9.80",
4
4
  "type": "module",
5
5
  "description": "Zero-dependency SPA framework with immutable state and virtual DOM",
6
6
  "main": "./dist/tutuca.js",
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: tutuca
3
- description: Use when authoring or reviewing tutuca modules — `component({...})` definitions, `html\`...\`` views, `@`-directives, `input` / `bubble` / `receive` / `response` / `alter` handlers, macros, or `getTests` exports — or when running the `tutuca` CLI (`lint` / `test` / `render` / `docs`). Covers the post-edit `tutuca <module> lint` → `test` → `render --title "<example>"` verification recipe.
3
+ description: Use when authoring or reviewing tutuca modules — `component({...})` definitions, `html`...`` views, `@`-directives, `input` / `bubble` / `receive` / `response` / `alter` handlers, macros, or `getTests` exports — or when running the `tutuca` CLI (`lint` / `test` / `render` / `docs`). Covers the post-edit `tutuca <module> lint` → `test` → `render --title "<example>"` verification recipe.
4
4
  ---
5
5
 
6
- <!-- This file is generated by scripts/build-skill.js from docs/llm/. Do not edit. -->
6
+ <!-- Source of truth: docs/skill/. The skill/tutuca/ copy is generated by scripts/build-skill.js edit docs/skill/, not skill/. -->
7
7
 
8
8
  # Tutuca
9
9
 
@@ -27,12 +27,13 @@ When authoring tutuca code, also load these if available:
27
27
 
28
28
  | Task | File |
29
29
  | ---------------------------------------------------------------------------------------------- | ------------------------------- |
30
- | Authoring `component({...})`, `html\`...\`` views, macros, fields, events, lists, styles | [core.md](./core.md) |
30
+ | Authoring `component({...})`, `html`...`` views, macros, fields, events, lists, styles | [core.md](./core.md) |
31
31
  | CLI commands, flags, exit codes, full linter rule list | [cli.md](./cli.md) |
32
32
  | `bubble` / `send`-`receive` / async `request`-`response` channels, `$unknown`, request-handler registration | [request-response.md](./request-response.md) |
33
33
  | Drag & drop, dynamic bindings (`*x`), pseudo-`x`, custom seq types, Tailwind/MargaUI | [advanced.md](./advanced.md) |
34
34
  | Runtime semantics — path steps, transaction lifecycle, dyn-var teleporting, async key pinning (`livePath`) | [semantics.md](./semantics.md) |
35
35
  | Authoring tests — `getTests` shape, calling methods/input/receive/bubble/response/alter handlers, designing handlers for testability | [testing.md](./testing.md) |
36
+ | Task-oriented recipes — iteration, filtering, conditional content, conditional attributes, dynamic vars, composition, events | [patterns/README.md](./patterns/README.md) |
36
37
 
37
38
  Read `core.md` first. Reach for the others only when the task touches
38
39
  them — each is referenced inline from `core.md` so you'll be pointed
@@ -43,30 +43,31 @@ Touch is wired up too (drag fires after a small move threshold).
43
43
  ## Dynamic Bindings
44
44
 
45
45
  For passing values "context-style" through nested components without prop
46
- drilling. Define on the producer; alias on consumers; resolve as `*name`.
46
+ drilling. **`provide`** on the producer; **`lookup`** on consumers;
47
+ resolve as `*name`.
47
48
 
48
49
  ```js
49
50
  const Theme = component({
50
51
  name: "Theme",
51
52
  fields: { color: "blue" },
52
- dynamic: { color: ".color" },
53
- on: {
54
- stackEnter() {
55
- return ["color"];
56
- },
57
- },
53
+ provide: { color: ".color" },
58
54
  });
59
55
  const Child = component({
60
- dynamic: { color: { for: "Theme.color", default: "'gray'" } },
56
+ lookup: { color: { for: "Theme.color", default: "'gray'" } },
61
57
  view: html`<p :style="$'color: {*color}'"></p>`,
62
58
  });
63
59
  ```
64
60
 
65
- `on.stackEnter()` is required only on the **producer** (the component
66
- declaring `dynamic: { name: ".field" }` to expose a value). It returns
67
- the list of dynamic-binding names this component pushes onto the stack
68
- when entering its render. Consumers (which only alias via
69
- `{ for: "Producer.name", default: ... }`) don't need it.
61
+ A **`provide`** maps an exported name to a field expression. Every
62
+ provide is evaluated and pushed onto the dynamic stack automatically
63
+ when the producer is entered during render there is no hook to opt in.
64
+
65
+ A **`lookup`** reads a value the `*name` way: the key is the name used
66
+ in views (`*color`), the value is `"Producer.provideName"`, or
67
+ `{ for: "Producer.provideName", default: ".field" }` to supply a
68
+ fallback when no producer is in scope (default is optional — without it
69
+ a miss resolves to `null`). A `*name` that names the component's own
70
+ `provide` resolves to the nearest provided value (including its own).
70
71
 
71
72
  ### Dynamic vars as render targets
72
73
 
@@ -80,23 +81,26 @@ component-render target and an iteration source:
80
81
  <div @each="*items"><x render-it></x></div> <!-- iterate a dynamic seq -->
81
82
  ```
82
83
 
83
- The producer's `dynamic` value is an **expression**, not only a bare
84
- field it can be a sequence/map item access:
84
+ A `provide` value must be **addressable** a `.field` or a `.seq[.key]`
85
+ seq-access, nothing else. (It is both read as `*name` *and* used as a
86
+ render-target / teleport path, so a method or constant — which has no
87
+ path — is a lint error.) A `lookup` `default`, by contrast, is only a
88
+ value fallback and accepts the full grammar, including constants like
89
+ `'gray'`. A `provide` can be a sequence/map item access:
85
90
 
86
91
  ```js
87
92
  const Root = component({
88
93
  name: "Root",
89
94
  fields: { items: IMap(), selectedKey: "" },
90
- dynamic: {
95
+ provide: {
91
96
  items: ".items", // the whole sequence
92
97
  selected: ".items[.selectedKey]", // seq-access to one entry
93
98
  },
94
- on: { stackEnter() { return ["items", "selected"]; } },
95
99
  });
96
100
  ```
97
101
 
98
102
  There is **no `*name[.key]` form** — a consumer never indexes a dynamic
99
- var. The seq-access lives in the producer's `dynamic` declaration; the
103
+ var. The seq-access lives in the producer's `provide` declaration; the
100
104
  consumer just reads the resolved value as `*name`.
101
105
 
102
106
  **Teleporting.** The component rendered via `<x render="*selected">`
@@ -19,7 +19,9 @@ the `tutuca` CLI.
19
19
  > [testing.md](./testing.md). Runtime semantics — path steps, the
20
20
  > transaction lifecycle, dyn-var teleporting, and async key pinning
21
21
  > (`livePath`): see [semantics.md](./semantics.md). Read those only when
22
- > the task touches them.
22
+ > the task touches them. Task-oriented "how do I do X" recipes (iteration,
23
+ > filtering, slicing, conditional content, conditional attributes, dynamic
24
+ > vars, composition, events, …): see [patterns/README.md](./patterns/README.md).
23
25
 
24
26
  ## Verifying changes
25
27
 
@@ -305,7 +307,7 @@ component({
305
307
  bubble: { itemPicked(item, ctx) { return this.setSelected(item); } },
306
308
  response:{ loadData(res, err, ctx) { return this.setItems(res); } },
307
309
  statics: { fromData(d) { return this.make({ count: d.n ?? 0 }); } },
308
- // dynamic: { ... }, on: { stackEnter() {...} } // see advanced.md
310
+ // provide: { ... }, lookup: { ... } // see advanced.md
309
311
  });
310
312
  ```
311
313
 
@@ -416,6 +418,48 @@ statics: {
416
418
  // usage: TreeRoot.Class.fromData([...])
417
419
  ```
418
420
 
421
+ ### One definition, multiple scopes (`clone`)
422
+
423
+ A component is built once and bound to a scope at `registerComponents`
424
+ time: that scope owns the component's `Class`, the per-instance
425
+ component tag, and the scope-bound `make`/statics. Re-registering the
426
+ *same* component object into another scope rebinds it (last wins) —
427
+ fine for a fresh re-setup, but it means a single definition can't be
428
+ live in two scopes at once.
429
+
430
+ To register the same definition into a second scope simultaneously, use
431
+ `Comp.clone()` — it returns a fresh, fully independent `Component` (new
432
+ id, its own `Class`) built from the same spec:
433
+
434
+ ```js
435
+ scopeA.registerComponents([Widget]);
436
+ scopeB.registerComponents([Widget.clone()]); // independent Class + scope
437
+ ```
438
+
439
+ Each clone has its own `Class`, so `getCompFor(instance)` and a static's
440
+ `this.scope` / `this.make` resolve unambiguously to the scope that
441
+ instance belongs to — even after immutable `.set()` updates, since the
442
+ component tag lives on the (per-scope) prototype.
443
+
444
+ **Caveat — statics that reach a child by its module-level const.** A
445
+ static like `fromData` that builds a *different* child type by naming the
446
+ imported const directly (`Item.Class.fromData(v)` above) hardcodes the
447
+ child's *original* scope. In a single-scope app that's the only scope, so
448
+ it's fine. But once you `clone()` either component into another scope, the
449
+ parent clone still deserializes children through the original `Item` —
450
+ wrong scope. For multi-scope safety, resolve the child through the
451
+ caller's scope instead of the module-level const:
452
+
453
+ ```js
454
+ fromData({ items = [] }) {
455
+ const Item = this.scope.lookupComponent("Item");
456
+ return this.make({ items: items.map((v) => Item.Class.fromData(v)) });
457
+ }
458
+ ```
459
+
460
+ Recursion into the *same* type needs no lookup — use `this.fromData(v)`
461
+ (or `this.make`), which already targets the caller's scope.
462
+
419
463
  ## Text Rendering
420
464
 
421
465
  ```html
@@ -459,6 +503,14 @@ consequences:
459
503
  - Macro registry keys are lowercased on insert for the same reason
460
504
  (see *Macros* below).
461
505
 
506
+ Tutuca auto-namespaces by subtree: elements inside `<svg>` get the SVG
507
+ namespace and elements inside `<math>` get MathML, with spec-cased local
508
+ names preserved (`linearGradient`, `viewBox`). A `<foreignObject>` switches
509
+ its children back to the HTML namespace. Customised built-in elements work
510
+ via `is="..."` (e.g. `<button is="x-fancy">`); `is` is applied when the
511
+ element is created, so it must be a static attribute — setting it later
512
+ does not upgrade the element.
513
+
462
514
  ## Event Handling
463
515
 
464
516
  ```html
@@ -937,3 +989,6 @@ export function getTests({ describe, test, expect }) { /*...*/ } // optiona
937
989
  convention for tests.
938
990
  - [cli.md](./cli.md) — commands, flags, exit codes, and the full linter rule
939
991
  list.
992
+ - [patterns/README.md](./patterns/README.md) — task-oriented recipes ("how do I
993
+ iterate / filter / paginate / show-hide / build tabs / share state / …"),
994
+ each linking back here and to a runnable example.
@@ -0,0 +1,42 @@
1
+ # Tutuca Patterns
2
+
3
+ Task-oriented recipes: "how do I do X" with a minimal working snippet and the
4
+ one pitfall worth knowing. Each recipe is self-contained and brief; for the
5
+ full directive *semantics* behind a pattern, see [core.md](../core.md) and its
6
+ spokes.
7
+
8
+ New to Tutuca? Read [core.md](../core.md) first, then reach here for a specific
9
+ task.
10
+
11
+ ## Iteration & lists
12
+
13
+ - [Iterate a list](iterate-a-list.md) — render one element per item with `@each` / `render-each`.
14
+ - [Filter a list](filter-a-list.md) — keep only matching items with `@when`.
15
+ - [Enrich each item](enrich-each-item.md) — expose derived per-item values as `@`-bindings.
16
+ - [Paginate a list](paginate-a-list.md) — slice the iteration with `@loop-with` `start`/`end`.
17
+
18
+ ## Conditional content & attributes
19
+
20
+ - [Show or hide content](show-or-hide-content.md) — `@show` / `@hide` and the boolean predicates.
21
+ - [Switch between views](switch-between-views.md) — pick a component's own view with `as=` or `@push-view`.
22
+ - [Conditional attribute value](conditional-attribute-value.md) — set a class/title by condition with `@if` / `@then` / `@else`.
23
+ - [Tabbed interface](tabbed-interface.md) — a `currentView` field + predicates to show the panel and highlight the active tab.
24
+
25
+ ## Context & dynamic variables
26
+
27
+ - [Share state without prop-drilling](share-state-without-prop-drilling.md) — `provide` / `lookup` and reading `*name`.
28
+ - [Edit through a dynamic target](edit-through-a-dynamic-target.md) — render `*name` and teleport edits back to the owner.
29
+
30
+ ## Composition
31
+
32
+ - [Render a child component](render-a-child-component.md) — `<x render=".field">` and multiple views.
33
+ - [Reuse markup with macros](reuse-markup-with-macros.md) — `macro(...)` with parameters and slots.
34
+
35
+ ## Data & events
36
+
37
+ - [Bind text and attributes](bind-text-and-attributes.md) — `@text`, `:attr`, `$'…'` templates, scope enrichment.
38
+ - [Handle events](handle-events.md) — `@on.<event>`, handler args, modifiers, custom events.
39
+
40
+ ## Component communication
41
+
42
+ - [Coordinate components](coordinate-components.md) — `bubble`, `send`/`receive`, async `request`/`response`.
@@ -0,0 +1,30 @@
1
+ # Bind text and attributes
2
+
3
+ **Problem:** display a field as text, bind it to an attribute, or compose a
4
+ string from several values.
5
+
6
+ ```html
7
+ <!-- text -->
8
+ <span @text=".str"></span> <!-- into a host element -->
9
+ <x text="$getStrUpper"></x> <!-- $ calls a method; no wrapping element -->
10
+
11
+ <!-- attributes: plain = static, :attr = dynamic -->
12
+ <input :value=".str" @on.input="$setStr value" />
13
+ <a :href=".url" :title="$'Hi {.name}'">link</a> <!-- $'…' string template -->
14
+ <button :class="$'btn btn-{.kind}'">x</button>
15
+
16
+ <!-- derive values for a subtree without putting them on the component -->
17
+ <div @enrich-with="enrichScope">Len: <x text="@len"></x></div>
18
+ ```
19
+
20
+ ```js
21
+ methods: { getStrUpper() { return this.str.toUpperCase(); } },
22
+ alter: { enrichScope() { return { len: this.text.length }; } }, // keys → @len, …
23
+ ```
24
+
25
+ Value slots take `.field`, `$method`, or `@binding` — never a path
26
+ (`.user.name` fails). Multi-word strings **must** be quoted (`'flex gap-3'`) or
27
+ written as a `$'…'` template (`$'btn {.kind}'`); a bare unquoted string returns
28
+ `null`. Boolean HTML attributes (`disabled`, `checked`, …) are auto-recognized
29
+ — pass a boolean field. Scope `@enrich-with` (no `@each` on the element) is the
30
+ path-free way to expose derived values to a subtree.
@@ -0,0 +1,29 @@
1
+ # Conditional attribute value
2
+
3
+ **Problem:** set an attribute (class, title, …) to one value or another
4
+ depending on a condition.
5
+
6
+ ```html
7
+ <button
8
+ @if.class=".isActive"
9
+ @then="'btn btn-success'"
10
+ @else="'btn btn-ghost'"
11
+ @on.click="$toggleIsActive"
12
+ >
13
+ toggle
14
+ </button>
15
+ ```
16
+
17
+ `@if.<attr>` takes the condition (a `.field`, a `$method`, or a predicate like
18
+ `equals? .tab 'x'`); `@then`/`@else` are the two values. String literals need
19
+ quotes (`'btn ok'`); a `$'…'` template works too. **Multiple `@if` on one
20
+ element:** every `@then`/`@else` after the first must name its attr
21
+ (`@then.title`, `@else.title`) — HTML forbids duplicate attribute names, so an
22
+ unnamed second `@then` is dropped silently.
23
+
24
+ ```html
25
+ <button
26
+ @if.class=".isActive" @then="'on'" @else="'off'"
27
+ @if.title=".isActive" @then.title="'On'" @else.title="'Off'"
28
+ ></button>
29
+ ```
@@ -0,0 +1,26 @@
1
+ # Coordinate components
2
+
3
+ **Problem:** move state between components — notify an ancestor, message a
4
+ specific component, or run async work and fold in the result.
5
+
6
+ ```js
7
+ // bubble — walk toward the root; the first ancestor with the handler runs
8
+ input: { onItemClick(ctx) { ctx.bubble("itemSelected", [this]); return this; } },
9
+ bubble: { itemSelected(item, ctx) { return this.insertInLogAt(0, item.label); } },
10
+
11
+ // send / receive — deliver to one target (self, or ctx.at.<step> for another)
12
+ methods: { submit(ctx) { ctx.at.field("status").send("flash", [this.draft]); return this; } },
13
+ receive: { flash(message, ctx) { return this.setMessage(message); } },
14
+
15
+ // request / response — async host work, result routed back
16
+ receive: { init(ctx) { ctx.request("loadData"); return this.setIsLoading(true); } },
17
+ response: { loadData(res, err, ctx) { return this.setIsLoading(false).setItems(res); } },
18
+ ```
19
+
20
+ Pick by direction: **bubble** for aggregate state an ancestor owns (logs,
21
+ selections); **send/receive** to address one known component
22
+ (`ctx.at.field("x")` / `.index(name, i)` / `.key(name, k)`, default self);
23
+ **request/response** for fetch/timer/IndexedDB — register the async fn with
24
+ `scope.registerRequestHandlers({...})`, and `response` gets `(res, err)`. `ctx`
25
+ is always the trailing arg. `receive.init` is a convention, not a lifecycle
26
+ hook — dispatch it with `app.sendAtRoot("init")`.
@@ -0,0 +1,27 @@
1
+ # Edit through a dynamic target
2
+
3
+ **Problem:** render a value owned by a distant ancestor *and* let edits made in
4
+ the child land back on the owner — without forwarding events up by hand.
5
+
6
+ ```js
7
+ // producer exposes a field (or a seq-access) as a dynamic
8
+ const Workspace = component({
9
+ name: "Workspace",
10
+ fields: { sheet: null },
11
+ provide: { active: ".sheet" }, // or ".items[.selectedKey]"
12
+ });
13
+
14
+ // a distant consumer renders it as a target
15
+ const Toolbar = component({
16
+ name: "Toolbar",
17
+ lookup: { active: { for: "Workspace.active", default: ".missing" } },
18
+ view: html`<x render="*active" as="edit"></x>`,
19
+ });
20
+ ```
21
+
22
+ Because `*active` resolves to a real **path** (not a copied value), the event
23
+ fired inside the rendered child is *teleported*: the mutation skips the
24
+ intermediate components and lands on `Workspace.sheet`, so the owner and any
25
+ other view of the same value update in lock-step. A `provide` can even point at
26
+ a seq-access (`.items[.selectedKey]`) to expose "the selected item". This is
27
+ the **edit** counterpart of the share-state-without-prop-drilling recipe.
@@ -0,0 +1,25 @@
1
+ # Enrich each item
2
+
3
+ **Problem:** show a value derived from each item (a count, a formatted label)
4
+ without storing it on the data.
5
+
6
+ ```html
7
+ <li @each=".items" @enrich-with="enrichItem">
8
+ <x text="@value"></x> (<x text="@count"></x> characters)
9
+ </li>
10
+ ```
11
+
12
+ ```js
13
+ alter: {
14
+ enrichItem(binds, _key, item) {
15
+ binds.count = item.length; // becomes @count in the template
16
+ },
17
+ }
18
+ ```
19
+
20
+ `@enrich-with` receives a **mutable** `binds` object (seeded with `{ key,
21
+ value }`); every key you set becomes an `@`-prefixed binding for that item's
22
+ subtree. The return value is ignored. Combine freely with `@when` and
23
+ `@loop-with` on the same element. Without an `@each` on the same element,
24
+ `@enrich-with` enriches the whole scope instead (see the bind-text-and-attributes
25
+ recipe).
@@ -0,0 +1,23 @@
1
+ # Filter a list
2
+
3
+ **Problem:** render only the items that match a condition.
4
+
5
+ ```html
6
+ <li @each=".items" @when="filterItem">
7
+ <span @text="@key"></span>: <x text="@value"></x>
8
+ </li>
9
+ <!-- on <x render-each> the prefix drops: when="filterItem" -->
10
+ ```
11
+
12
+ ```js
13
+ alter: {
14
+ filterItem(_key, item) {
15
+ return item.toLowerCase().includes(this.query.toLowerCase());
16
+ },
17
+ }
18
+ ```
19
+
20
+ `@when` names an `alter` handler called per item as `(key, value, iterData)`;
21
+ return `false` to skip. It filters *after* any `@loop-with` slice, so a page
22
+ can yield fewer than its window. Filtering reads other fields off `this`
23
+ directly (`this.query`) — there are no paths in the template.
@@ -0,0 +1,27 @@
1
+ # Handle events
2
+
3
+ **Problem:** respond to a DOM event and update state.
4
+
5
+ ```html
6
+ <button @on.click="$inc">+</button> <!-- $ calls a method -->
7
+ <button @on.click="dec">-</button> <!-- bare name = input handler -->
8
+
9
+ <!-- pass args by name; ctx is auto-appended last -->
10
+ <input @on.input="$setStr value" />
11
+ <input @on.input="$setN valueAsInt" />
12
+ <button @on.click="$addItem JsonSelector">+</button>
13
+
14
+ <!-- modifiers: keydown +send (Enter) / +cancel (Esc), and +ctrl/+cmd/+alt -->
15
+ <input @on.keydown+send="$submit value" @on.keydown+cancel="$reset" />
16
+
17
+ <!-- custom elements: any CustomEvent reaches @on.<name>, detail is `value` -->
18
+ <emoji-picker @on.emoji-click="onPick value"></emoji-picker>
19
+ ```
20
+
21
+ Handlers return a (new) instance of `this`. The first slot is a handler name
22
+ (`$method`, or a bare name in `input`/`alter`); later slots are built-in arg
23
+ names — `value`, `valueAsInt`/`valueAsFloat`, `event`, `key`, `isAlt`,
24
+ `isShift`, `isCtrl`/`isCmd`, `dragInfo`, … `value` resolves to
25
+ `event.target.value` (or `.checked` for a checkbox, or `event.detail` for a
26
+ `CustomEvent`). Bind events declaratively with `@on.` rather than reaching for
27
+ the node and `addEventListener` — an outside listener bypasses the transactor.
@@ -0,0 +1,18 @@
1
+ # Iterate a list
2
+
3
+ **Problem:** render one element per item in a list/map field.
4
+
5
+ ```html
6
+ <!-- a host element per item: @key and @value are bound in the loop -->
7
+ <li @each=".items"><span @text="@key"></span>: <x text="@value"></x></li>
8
+
9
+ <!-- a child component per item -->
10
+ <x render-each=".items"></x>
11
+ <div @each=".items"><x render-it></x></div>
12
+ ```
13
+
14
+ `@each` accepts a `.field` or a `*dynamic` (not a `$method` — a method result
15
+ has no addressable path for event dispatch). `@key`/`@value` are auto-bound on
16
+ host-element loops; under `render-each` / `render-it` each item is rendered as
17
+ its own component (no `@value`). Use `render-each` for lists of components,
18
+ `@each` for plain values.
@@ -0,0 +1,27 @@
1
+ # Paginate a list
2
+
3
+ **Problem:** show one page at a time without iterating or rendering the
4
+ off-page items.
5
+
6
+ ```html
7
+ <li @each=".items" @loop-with="paginate">
8
+ <span class="badge" @text="@key"></span> <x text="@value"></x>
9
+ </li>
10
+ ```
11
+
12
+ ```js
13
+ fields: { items: [], page: 0, pageSize: 5 },
14
+ alter: {
15
+ paginate(seq) { // runs once per render, before iteration
16
+ const start = this.page * this.pageSize;
17
+ return { iterData: { total: seq.size }, start, end: start + this.pageSize };
18
+ },
19
+ }
20
+ ```
21
+
22
+ `@loop-with` returns `{ iterData?, start?, end? }`, all optional. `start`/`end`
23
+ slice with `Array.prototype.slice` semantics (`end` exclusive, negatives count
24
+ from the end). Slicing is positional but **preserves each item's original
25
+ key** — `@key` is the index in the full list, so events and two-way binding
26
+ keep their identity across pages. `iterData` is the shared per-loop value
27
+ handed to `@when` / `@enrich-with`.
@@ -0,0 +1,20 @@
1
+ # Render a child component
2
+
3
+ **Problem:** a component holds another component in a field and wants to render
4
+ it (reaching into nested data is not allowed — `@text=".child.name"` fails).
5
+
6
+ ```js
7
+ fields: { greeting: Greeting.make({ name: "world" }) },
8
+ ```
9
+
10
+ ```html
11
+ <x render=".greeting"></x> <!-- default ("main") view -->
12
+ <x render=".greeting" as="edit"></x> <!-- a named view -->
13
+ ```
14
+
15
+ The child draws its own view from its own fields, so inside `Greeting`'s view
16
+ `@text=".name"` reads the child's `name`. This is the idiomatic way to display
17
+ nested structure: make the nested thing a component and render it, rather than
18
+ trying to path into it. For a list of children use `render-each` (see the
19
+ iterate-a-list recipe); to flip which view renders, see the switch-between-views
20
+ recipe.
@@ -0,0 +1,35 @@
1
+ # Reuse markup with macros
2
+
3
+ **Problem:** the same markup fragment repeats across a view and you want one
4
+ definition — but it has no state of its own.
5
+
6
+ ```js
7
+ import { macro, html } from "tutuca";
8
+
9
+ const badge = macro(
10
+ { label: "'New'", kind: "'info'" }, // defaults are *expressions*
11
+ html`<span :class="$'badge badge-{^kind}'" @text="^label"></span>`,
12
+ );
13
+
14
+ const card = macro(
15
+ { title: "'Card'" },
16
+ html`<div class="card"><h2 @text="^title"></h2><x:slot></x:slot></div>`,
17
+ );
18
+
19
+ export function getMacros() { return { badge, card }; }
20
+ ```
21
+
22
+ ```html
23
+ <x:badge></x:badge> <!-- defaults -->
24
+ <x:badge label="Sale"></x:badge> <!-- static string (no quotes needed) -->
25
+ <x:badge :label=".status"></x:badge> <!-- bind a field -->
26
+ <x:card title="Hi"><p>body</p></x:card> <!-- children fill <x:slot> -->
27
+ ```
28
+
29
+ A macro is pure template expansion — no fields, no handlers. Parameters are
30
+ read as `^name`; calls inside the body (`$method`, `.field`) resolve against
31
+ the *host* component. `<x:slot>` (or `<x:slot name="…">` for named slots)
32
+ receives the caller's children. Register with `scope.registerMacros(...)`;
33
+ registry keys are lowercased (`<x:Card>` → `card`). For repeated markup that
34
+ *does* need state, use a child component instead (see the render-a-child-component
35
+ recipe).
@@ -0,0 +1,30 @@
1
+ # Share state without prop-drilling
2
+
3
+ **Problem:** a deep descendant needs a value owned by a distant ancestor, and
4
+ you don't want to thread it through every component in between.
5
+
6
+ ```js
7
+ // producer — exposes one of its fields under a name
8
+ const Producer = component({
9
+ name: "EntryEditorAndSelector",
10
+ fields: { items: [] },
11
+ provide: { entries: ".items" },
12
+ });
13
+
14
+ // consumer — forwards to the producer's binding by "Component.name"
15
+ const Consumer = component({
16
+ name: "Selector",
17
+ lookup: { entries: { for: "EntryEditorAndSelector.entries", default: ".items" } },
18
+ });
19
+ ```
20
+
21
+ ```html
22
+ <!-- read the dynamic with the * prefix — iterate or render it -->
23
+ <option @each="*entries" :value="@value" @text="@label"></option>
24
+ ```
25
+
26
+ `provide` publishes a field under a name; a descendant's `lookup` resolves
27
+ `*name` to the nearest matching producer, falling back to `default` when none
28
+ is in scope. `*name` works wherever a `.field` does for iteration/rendering.
29
+ This is the **read** side; to edit the producer's value through the dynamic,
30
+ see the edit-through-a-dynamic-target recipe.
@@ -0,0 +1,22 @@
1
+ # Show or hide content
2
+
3
+ **Problem:** render an element only when a condition holds.
4
+
5
+ ```html
6
+ <div @show=".isOpen">Details</div>
7
+ <p @hide=".isOpen">(hidden when open)</p>
8
+
9
+ <!-- boolean predicates for one-field checks -->
10
+ <p @show="empty? .items">No results</p>
11
+ <p @show="truthy? .query">Searching…</p>
12
+ <div @show="equals? .view 'detail'">detail view</div>
13
+
14
+ <!-- on an <x> render op: wraps the produced node, no extra DOM element -->
15
+ <x text=".count" show=".isOpen"></x>
16
+ ```
17
+
18
+ The closed set of predicates is `empty?`, `truthy?`, `falsy?`, `null?`,
19
+ `equals?` (binary). For a condition spanning multiple fields, use a no-arg
20
+ method instead (`@show="$canSubmit"`). `@show`/`@hide` toggle visibility on a
21
+ host element; the wrapper form (`show=` / `hide=` on `<x>`) conditionally
22
+ emits the node with no surrounding element.
@@ -0,0 +1,26 @@
1
+ # Switch between views
2
+
3
+ **Problem:** render the *same* component in a different view (e.g. a read-only
4
+ "main" vs an "edit" form).
5
+
6
+ ```js
7
+ component({
8
+ view: html`<p @text=".title"></p>`, // "main"
9
+ views: { edit: html`<input :value=".title" @on.input="$setTitle value" />` },
10
+ });
11
+ ```
12
+
13
+ ```html
14
+ <!-- as= picks the view for one <x render> element only -->
15
+ <x render=".value"></x>
16
+ <x render=".value" as="edit"></x>
17
+
18
+ <!-- @push-view forces a view on every component rendered under the host -->
19
+ <div @push-view=".view"><x render-each=".items"></x></div>
20
+ ```
21
+
22
+ `as="edit"` applies to the direct component only and falls back to `main` if
23
+ the view is absent. `@push-view` pushes a view name onto the render stack so
24
+ every descendant picks the first matching view (else `main`) — use it to flip a
25
+ whole subtree (e.g. a list) into edit mode at once. To toggle *sibling panels*
26
+ by a field instead, see the tabbed-interface recipe.