tutuca 0.9.80 → 0.9.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skill/tutuca/SKILL.md +1 -0
- package/skill/tutuca/advanced.md +10 -3
- package/skill/tutuca/component-design.md +151 -0
- package/skill/tutuca/core.md +6 -0
- package/skill/tutuca/patterns/README.md +1 -1
- package/skill/tutuca/patterns/edit-through-a-dynamic-target.md +1 -1
- package/skill/tutuca/patterns/{share-state-without-prop-drilling.md → share-state-across-the-tree.md} +9 -1
package/package.json
CHANGED
package/skill/tutuca/SKILL.md
CHANGED
|
@@ -28,6 +28,7 @@ When authoring tutuca code, also load these if available:
|
|
|
28
28
|
| Task | File |
|
|
29
29
|
| ---------------------------------------------------------------------------------------------- | ------------------------------- |
|
|
30
30
|
| Authoring `component({...})`, `html`...`` views, macros, fields, events, lists, styles | [core.md](./core.md) |
|
|
31
|
+
| Designing components — responsibilities, state ownership, channel choice, do's & don'ts | [component-design.md](./component-design.md) |
|
|
31
32
|
| CLI commands, flags, exit codes, full linter rule list | [cli.md](./cli.md) |
|
|
32
33
|
| `bubble` / `send`-`receive` / async `request`-`response` channels, `$unknown`, request-handler registration | [request-response.md](./request-response.md) |
|
|
33
34
|
| Drag & drop, dynamic bindings (`*x`), pseudo-`x`, custom seq types, Tailwind/MargaUI | [advanced.md](./advanced.md) |
|
package/skill/tutuca/advanced.md
CHANGED
|
@@ -42,9 +42,16 @@ Touch is wired up too (drag fires after a small move threshold).
|
|
|
42
42
|
|
|
43
43
|
## Dynamic Bindings
|
|
44
44
|
|
|
45
|
-
For passing values "context-style"
|
|
46
|
-
|
|
47
|
-
resolve as `*name`.
|
|
45
|
+
For passing values "context-style" to a deep descendant without threading
|
|
46
|
+
them through every component in between. **`provide`** on the producer;
|
|
47
|
+
**`lookup`** on consumers; resolve as `*name`.
|
|
48
|
+
|
|
49
|
+
> **Best practice:** keep state local to the component and reach for
|
|
50
|
+
> `provide` / `lookup` only when it is genuinely the only solution. Dynamic
|
|
51
|
+
> bindings couple a consumer to a producer that may not be in scope — prefer
|
|
52
|
+
> keeping components as self-contained as possible: let a child render the
|
|
53
|
+
> field it needs from its owner, and lift state only as far up the tree as it
|
|
54
|
+
> actually needs to live.
|
|
48
55
|
|
|
49
56
|
```js
|
|
50
57
|
const Theme = component({
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Tutuca Cheatsheet — Component Design
|
|
2
|
+
|
|
3
|
+
How to *shape* a feature into one or more tutuca components — responsibilities,
|
|
4
|
+
where state lives, which channel to reach for — before you reach for syntax. The
|
|
5
|
+
mechanics live elsewhere: component skeleton, fields, directives, and the
|
|
6
|
+
post-edit verification recipe in [core.md](./core.md); the orchestration channels
|
|
7
|
+
in [request-response.md](./request-response.md); dynamic bindings (`provide` /
|
|
8
|
+
`lookup` / `*x`) in [advanced.md](./advanced.md); task recipes in
|
|
9
|
+
[patterns/README.md](./patterns/README.md). This file is a router with judgment
|
|
10
|
+
attached — every rule points at its canonical home rather than restating it.
|
|
11
|
+
Read it when deciding how to split a feature into components, where a piece of
|
|
12
|
+
state should live, how two components should talk, or when reviewing a component
|
|
13
|
+
for design smells.
|
|
14
|
+
|
|
15
|
+
## Decide in this order
|
|
16
|
+
|
|
17
|
+
Walk these top-down whenever you add or reshape a component:
|
|
18
|
+
|
|
19
|
+
1. **What single responsibility is this?** If the answer has an "and" in it, split
|
|
20
|
+
it into separate components.
|
|
21
|
+
2. **Who owns each piece of state?** The component that reads and mutates a value
|
|
22
|
+
owns it. Put the field there; let others render or message it.
|
|
23
|
+
3. **How do these components talk?** Pick the narrowest channel that reaches the
|
|
24
|
+
owner — see the ladder below.
|
|
25
|
+
4. **Where does the outside world cross the boundary?** Outbound I/O goes through
|
|
26
|
+
`ctx.request` / `response`; inbound external events go through
|
|
27
|
+
`app.sendAtRoot` to the root. Keep the logic inside tutuca on both sides.
|
|
28
|
+
|
|
29
|
+
## Communication decision ladder
|
|
30
|
+
|
|
31
|
+
Reach for the *narrowest* channel that does the job, and only move further down
|
|
32
|
+
the ladder when the one above can't express it:
|
|
33
|
+
|
|
34
|
+
- **The component owns the state needed to respond** → call a plain **`method`** on
|
|
35
|
+
itself (stays self-contained). See [core.md](./core.md) Methods.
|
|
36
|
+
- **An ancestor owns aggregate state** (a log, a selection, a total) → **`ctx.bubble`**
|
|
37
|
+
up toward the root; the first ancestor with a matching handler runs. See
|
|
38
|
+
[request-response.md](./request-response.md) "When to bubble".
|
|
39
|
+
- **You need to reach one known component** → **`ctx.send` / `receive`**, addressing a
|
|
40
|
+
specific path with `ctx.at` (defaults to self). See
|
|
41
|
+
[request-response.md](./request-response.md) "When to send".
|
|
42
|
+
- **The work is async or host-side** (fetch, timer, IndexedDB, an SDK) →
|
|
43
|
+
**`ctx.request` / `response`**, which goes out to the host and routes the result
|
|
44
|
+
back into component state. See [request-response.md](./request-response.md).
|
|
45
|
+
- **An external event pushes *into* the app** (WebSocket, `postMessage`, …) →
|
|
46
|
+
**`app.sendAtRoot`**, which lands the inbound event on the root. See
|
|
47
|
+
[request-response.md](./request-response.md) "Integrating with the outside world".
|
|
48
|
+
- **A deep descendant needs a value owned far away** and nothing in between should
|
|
49
|
+
know about it → **`provide` / `lookup` (`*name`)** across the tree — the last
|
|
50
|
+
resort. See [advanced.md](./advanced.md).
|
|
51
|
+
|
|
52
|
+
A compact worked version of the first four (`method`, `bubble`, `send`/`receive`,
|
|
53
|
+
`request`/`response`) lives in
|
|
54
|
+
[patterns/coordinate-components.md](./patterns/coordinate-components.md).
|
|
55
|
+
|
|
56
|
+
## Do's & Don'ts
|
|
57
|
+
|
|
58
|
+
- **Do create single-purpose components. Don't pack multiple responsibilities
|
|
59
|
+
into one.** A component that draws its own view from its own fields is the unit
|
|
60
|
+
of reuse and the unit of testing. → [patterns/render-a-child-component.md](./patterns/render-a-child-component.md)
|
|
61
|
+
|
|
62
|
+
- **Don't add a `kind` / `type` field and branch the view on it. Do make one
|
|
63
|
+
component per kind and render it with `<x render=".item">`.** Conditional-on-kind
|
|
64
|
+
views grow into tangled `@if` chains; a component per kind keeps each view flat
|
|
65
|
+
and each concern isolated. This is also why pathing into nested data is barred —
|
|
66
|
+
model the nested thing as a component instead. → [core.md](./core.md) "Common
|
|
67
|
+
pitfalls" (Paths are not allowed in values) and
|
|
68
|
+
[patterns/render-a-child-component.md](./patterns/render-a-child-component.md)
|
|
69
|
+
|
|
70
|
+
- **Do keep state in the component that owns and uses it, and lift it only as far
|
|
71
|
+
up the tree as it needs to live. Don't thread a value through every component in
|
|
72
|
+
between** (the React "prop drilling" reflex) — let a child render the field it
|
|
73
|
+
needs from its owner. → [patterns/share-state-across-the-tree.md](./patterns/share-state-across-the-tree.md)
|
|
74
|
+
|
|
75
|
+
- **Do reach for `provide` / `lookup` (`*name`) last** — only when a deep
|
|
76
|
+
descendant needs a value owned far away and nothing in between should know about
|
|
77
|
+
it. Dynamic bindings couple a consumer to a producer that may not be in scope.
|
|
78
|
+
→ [advanced.md](./advanced.md)
|
|
79
|
+
|
|
80
|
+
- **Do pick the channel by direction (the ladder above). Don't `bubble` an event
|
|
81
|
+
no ancestor consumes, and don't `send` to self when a plain method call would
|
|
82
|
+
do.** `bubble` emits an *event* any ancestor can observe; `send` delivers a
|
|
83
|
+
*message* to one target. → [request-response.md](./request-response.md) "When to
|
|
84
|
+
bubble" / "When to send"
|
|
85
|
+
|
|
86
|
+
- **Do keep logic inside the tutuca app when integrating with the outside world.**
|
|
87
|
+
Route outbound work through `ctx.request` / `response` and inbound external
|
|
88
|
+
events through `app.sendAtRoot` to the root (which forwards deeper with `ctx.at`),
|
|
89
|
+
so handlers stay the single owner of state changes. **Don't mutate `app.state`
|
|
90
|
+
directly or `addEventListener` outside the model** — state changed that way
|
|
91
|
+
bypasses the immutable `return this.set…()` discipline and is invisible to the
|
|
92
|
+
component that owns it. → [request-response.md](./request-response.md)
|
|
93
|
+
"Integrating with the outside world" (and its ⚠️ note)
|
|
94
|
+
|
|
95
|
+
- **Do handle every DOM event with tutuca's built-in `@on.` handlers — including
|
|
96
|
+
custom events fired by web components.** `@on.click`, `@on.input`,
|
|
97
|
+
`@on.<custom-event>` (the event `detail` surfaces as `value`) keep the event
|
|
98
|
+
inside the model, so it flows through a handler and returns a new `this`. **Don't
|
|
99
|
+
reach in from the outside with `addEventListener`** — a listener attached out of
|
|
100
|
+
band mutates state the owning component can't see and bypasses the transactor.
|
|
101
|
+
→ [core.md](./core.md) "Event Handling" and "Web Components & Custom Events"
|
|
102
|
+
|
|
103
|
+
- **Do use inline predicates and auto-generated mutators. Don't hand-write
|
|
104
|
+
`is*Selected()` / `select*()` boilerplate.** A single field plus
|
|
105
|
+
`equals? .activeSection 'todo'` / `empty?` and the generated `$setActiveSection`
|
|
106
|
+
/ `toggleX` often *is* the whole state machine. → [core.md](./core.md) "Methods
|
|
107
|
+
as Predicates & Computed Values" and "Field Types & Auto-generated API"
|
|
108
|
+
|
|
109
|
+
- **Do remember a rendered child gets a clean namespace.** Parent `@` bindings
|
|
110
|
+
(`@each`, `@enrich-with`) don't cross a `<x render>` boundary — pass a value
|
|
111
|
+
across it with `*name`, not by assuming the binding leaks in. → [advanced.md](./advanced.md)
|
|
112
|
+
|
|
113
|
+
- **Do add a decoy view when a margaui class is assembled at runtime.** The margaui
|
|
114
|
+
compiler only scans `class=` / `:class=` *literals*, so classes that appear only
|
|
115
|
+
in `@if.class` `@then`/`@else` payloads or are composed dynamically in an
|
|
116
|
+
`@enrich-with` / `:class` handler emit no CSS and render unstyled. Add a hidden,
|
|
117
|
+
never-rendered view (e.g. `_margauiClasses: html\`<p class="btn-success btn-ghost
|
|
118
|
+
…"></p>\``) listing each such class as a real literal so the scanner picks it up.
|
|
119
|
+
→ [advanced.md](./advanced.md) "Pitfall: `@if.class` payloads are invisible to
|
|
120
|
+
the scanner", and the worked decoy view in `personal-site.js`.
|
|
121
|
+
|
|
122
|
+
- **Do close the loop after every change** with `tutuca <module> lint` → `test` →
|
|
123
|
+
`render`. → [core.md](./core.md) "Verifying changes"
|
|
124
|
+
|
|
125
|
+
## Smells & refactors
|
|
126
|
+
|
|
127
|
+
- **`is*Selected()` + `select*()` methods → predicate + generated setter.**
|
|
128
|
+
Replace `@on.click="selectTodo"` / `@show="$isTodoSelected"` with
|
|
129
|
+
`@on.click="$setActiveSection 'todo'"` / `@show="equals? .activeSection 'todo'"`,
|
|
130
|
+
derive the current value from one field. (See `composability.js`, commit `808574e`.)
|
|
131
|
+
- **A view that `@if`-branches on a `kind` field → one component per kind**, each
|
|
132
|
+
rendered with `<x render>`.
|
|
133
|
+
- **A value passed down through three components that don't use it → move the
|
|
134
|
+
state up to the nearest common owner** and let the leaf render it directly; only
|
|
135
|
+
if nothing in between should know it, use `provide` / `lookup`.
|
|
136
|
+
- **Host code poking `app.state` or attaching a listener → an `app.sendAtRoot`
|
|
137
|
+
handler on the root**, with the mutation expressed as `return this.set…()`.
|
|
138
|
+
|
|
139
|
+
## See also
|
|
140
|
+
|
|
141
|
+
- [core.md](./core.md) — component skeleton, fields, directives, predicates, the
|
|
142
|
+
verification recipe, and the "Common pitfalls" list.
|
|
143
|
+
- [request-response.md](./request-response.md) — the channels in depth
|
|
144
|
+
(`bubble`, `send`/`receive`, `request`/`response`, `sendAtRoot`) and integrating
|
|
145
|
+
with the outside world.
|
|
146
|
+
- [advanced.md](./advanced.md) — `provide` / `lookup` / `*name` and the
|
|
147
|
+
clean-namespace boundary.
|
|
148
|
+
- [patterns/coordinate-components.md](./patterns/coordinate-components.md),
|
|
149
|
+
[patterns/share-state-across-the-tree.md](./patterns/share-state-across-the-tree.md),
|
|
150
|
+
[patterns/render-a-child-component.md](./patterns/render-a-child-component.md) —
|
|
151
|
+
runnable recipes for the rules above.
|
package/skill/tutuca/core.md
CHANGED
|
@@ -22,6 +22,9 @@ the `tutuca` CLI.
|
|
|
22
22
|
> the task touches them. Task-oriented "how do I do X" recipes (iteration,
|
|
23
23
|
> filtering, slicing, conditional content, conditional attributes, dynamic
|
|
24
24
|
> vars, composition, events, …): see [patterns/README.md](./patterns/README.md).
|
|
25
|
+
> Design judgment — shaping a feature into components, where state lives, which
|
|
26
|
+
> channel to reach for, and a do's & don'ts list: see
|
|
27
|
+
> [component-design.md](./component-design.md).
|
|
25
28
|
|
|
26
29
|
## Verifying changes
|
|
27
30
|
|
|
@@ -976,6 +979,9 @@ export function getTests({ describe, test, expect }) { /*...*/ } // optiona
|
|
|
976
979
|
|
|
977
980
|
## See also
|
|
978
981
|
|
|
982
|
+
- [component-design.md](./component-design.md) — design judgment for shaping a
|
|
983
|
+
feature into components: responsibilities, where state lives, which channel to
|
|
984
|
+
reach for, and a curated do's & don'ts list.
|
|
979
985
|
- [request-response.md](./request-response.md) — `bubble` / `send`-`receive` /
|
|
980
986
|
`request`-`response` channels, the `ctx.at` `PathBuilder`, `$unknown`, and
|
|
981
987
|
request-handler registration.
|
|
@@ -24,7 +24,7 @@ task.
|
|
|
24
24
|
|
|
25
25
|
## Context & dynamic variables
|
|
26
26
|
|
|
27
|
-
- [Share state
|
|
27
|
+
- [Share state across the tree](share-state-across-the-tree.md) — `provide` / `lookup` and reading `*name`.
|
|
28
28
|
- [Edit through a dynamic target](edit-through-a-dynamic-target.md) — render `*name` and teleport edits back to the owner.
|
|
29
29
|
|
|
30
30
|
## Composition
|
|
@@ -24,4 +24,4 @@ fired inside the rendered child is *teleported*: the mutation skips the
|
|
|
24
24
|
intermediate components and lands on `Workspace.sheet`, so the owner and any
|
|
25
25
|
other view of the same value update in lock-step. A `provide` can even point at
|
|
26
26
|
a seq-access (`.items[.selectedKey]`) to expose "the selected item". This is
|
|
27
|
-
the **edit** counterpart of the share-state-
|
|
27
|
+
the **edit** counterpart of the share-state-across-the-tree recipe.
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
# Share state
|
|
1
|
+
# Share state across the tree
|
|
2
2
|
|
|
3
3
|
**Problem:** a deep descendant needs a value owned by a distant ancestor, and
|
|
4
4
|
you don't want to thread it through every component in between.
|
|
5
5
|
|
|
6
|
+
> **Reach for this last.** Keep state local to the component and use
|
|
7
|
+
> `provide` / `lookup` only when it is genuinely the only solution — a value
|
|
8
|
+
> owned far away that a deep descendant needs and nothing in between should
|
|
9
|
+
> know about. Dynamic bindings couple a consumer to a producer that may not be
|
|
10
|
+
> in scope, so keep components as self-contained as possible: let a child
|
|
11
|
+
> render the field it needs from its owner, and lift state only as far up the
|
|
12
|
+
> tree as it needs to live.
|
|
13
|
+
|
|
6
14
|
```js
|
|
7
15
|
// producer — exposes one of its fields under a name
|
|
8
16
|
const Producer = component({
|