tutuca 0.9.98 → 0.9.99
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/README.md +5 -3
- package/dist/tutuca-cli.js +19 -11
- package/dist/tutuca-components.js +2444 -0
- package/dist/tutuca-dev.ext.js +16 -16
- package/dist/tutuca-dev.js +16 -16
- package/dist/tutuca-dev.min.js +2 -2
- package/dist/tutuca-storybook.js +125 -6
- package/package.json +3 -1
- package/skill/tutuca/cli.md +10 -68
- package/skill/tutuca/core.md +25 -74
- package/skill/tutuca/semantics.md +4 -3
- package/skill/tutuca/testing.md +3 -6
package/dist/tutuca-storybook.js
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
|
-
// src/storybook.js
|
|
1
|
+
// src/storybook/index.js
|
|
2
2
|
import { component, dispatchPhase, html, injectCss, phaseHasBubble, tutuca } from "tutuca";
|
|
3
|
+
import { getComponents as getInspectorComponents } from "tutuca/components";
|
|
4
|
+
|
|
5
|
+
// src/storybook/inspect.js
|
|
6
|
+
import { buildInspectorViews, isComponentInstance } from "tutuca/components";
|
|
7
|
+
function buildTestIndex(modules) {
|
|
8
|
+
const index = new Map;
|
|
9
|
+
for (const m of modules) {
|
|
10
|
+
if (typeof m.getTests !== "function")
|
|
11
|
+
continue;
|
|
12
|
+
const components = m.getComponents?.() ?? [];
|
|
13
|
+
for (const c of components) {
|
|
14
|
+
if (!index.has(c.name))
|
|
15
|
+
index.set(c.name, { getTests: m.getTests, components });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return index;
|
|
19
|
+
}
|
|
20
|
+
async function buildExampleInspectors(example, scope, testIndex, dev) {
|
|
21
|
+
const value = example.value;
|
|
22
|
+
const comp = isComponentInstance(value) ? scope.getCompFor(value) : null;
|
|
23
|
+
const entry = comp ? testIndex.get(comp.name) : null;
|
|
24
|
+
const views = await buildInspectorViews(value, scope, {
|
|
25
|
+
getTests: entry?.getTests ?? null,
|
|
26
|
+
components: entry?.components ?? [],
|
|
27
|
+
dev
|
|
28
|
+
});
|
|
29
|
+
return example.setInstanceView(views.instanceView).setComponentView(views.componentView).setLintView(views.lintView).setTestView(views.testView).setHasInspect(views.hasInspect).setHasComponent(views.hasComponent).setHasLint(views.hasLint).setHasTest(views.hasTest);
|
|
30
|
+
}
|
|
31
|
+
async function attachInspectorViews(root, scope, modules, dev = null) {
|
|
32
|
+
const testIndex = buildTestIndex(modules);
|
|
33
|
+
let sections = root.sections;
|
|
34
|
+
for (let si = 0;si < sections.size; si++) {
|
|
35
|
+
const section = sections.get(si);
|
|
36
|
+
let items = section.items;
|
|
37
|
+
for (let ii = 0;ii < items.size; ii++) {
|
|
38
|
+
items = items.set(ii, await buildExampleInspectors(items.get(ii), scope, testIndex, dev));
|
|
39
|
+
}
|
|
40
|
+
sections = sections.set(si, section.setItems(items));
|
|
41
|
+
}
|
|
42
|
+
return root.setSections(sections);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/storybook/index.js
|
|
3
46
|
var Storybook = component({
|
|
4
47
|
name: "Storybook",
|
|
5
48
|
fields: {
|
|
@@ -247,7 +290,16 @@ var Example = component({
|
|
|
247
290
|
value: null,
|
|
248
291
|
view: "main",
|
|
249
292
|
requestHandlers: null,
|
|
250
|
-
on: null
|
|
293
|
+
on: null,
|
|
294
|
+
activeTab: "preview",
|
|
295
|
+
hasInspect: false,
|
|
296
|
+
hasComponent: false,
|
|
297
|
+
hasLint: false,
|
|
298
|
+
hasTest: false,
|
|
299
|
+
componentView: null,
|
|
300
|
+
instanceView: null,
|
|
301
|
+
lintView: null,
|
|
302
|
+
testView: null
|
|
251
303
|
},
|
|
252
304
|
requestOverridesField: "requestHandlers",
|
|
253
305
|
statics: {
|
|
@@ -317,8 +369,72 @@ var Example = component({
|
|
|
317
369
|
</div>
|
|
318
370
|
</h2>
|
|
319
371
|
<p class="text-md italic opacity-60" @text=".description"></p>
|
|
320
|
-
<div class="
|
|
321
|
-
<
|
|
372
|
+
<div role="tablist" class="tabs tabs-border" @show=".hasInspect">
|
|
373
|
+
<a
|
|
374
|
+
role="tab"
|
|
375
|
+
@if.class="equals? .activeTab 'preview'"
|
|
376
|
+
@then="'tab tab-active'"
|
|
377
|
+
@else="'tab'"
|
|
378
|
+
@on.click="$setActiveTab 'preview'"
|
|
379
|
+
>
|
|
380
|
+
Preview
|
|
381
|
+
</a>
|
|
382
|
+
<a
|
|
383
|
+
role="tab"
|
|
384
|
+
@show=".hasComponent"
|
|
385
|
+
@if.class="equals? .activeTab 'component'"
|
|
386
|
+
@then="'tab tab-active'"
|
|
387
|
+
@else="'tab'"
|
|
388
|
+
@on.click="$setActiveTab 'component'"
|
|
389
|
+
>
|
|
390
|
+
Component
|
|
391
|
+
</a>
|
|
392
|
+
<a
|
|
393
|
+
role="tab"
|
|
394
|
+
@if.class="equals? .activeTab 'instance'"
|
|
395
|
+
@then="'tab tab-active'"
|
|
396
|
+
@else="'tab'"
|
|
397
|
+
@on.click="$setActiveTab 'instance'"
|
|
398
|
+
>
|
|
399
|
+
Instance
|
|
400
|
+
</a>
|
|
401
|
+
<a
|
|
402
|
+
role="tab"
|
|
403
|
+
@show=".hasLint"
|
|
404
|
+
@if.class="equals? .activeTab 'lint'"
|
|
405
|
+
@then="'tab tab-active'"
|
|
406
|
+
@else="'tab'"
|
|
407
|
+
@on.click="$setActiveTab 'lint'"
|
|
408
|
+
>
|
|
409
|
+
Lint
|
|
410
|
+
</a>
|
|
411
|
+
<a
|
|
412
|
+
role="tab"
|
|
413
|
+
@show=".hasTest"
|
|
414
|
+
@if.class="equals? .activeTab 'test'"
|
|
415
|
+
@then="'tab tab-active'"
|
|
416
|
+
@else="'tab'"
|
|
417
|
+
@on.click="$setActiveTab 'test'"
|
|
418
|
+
>
|
|
419
|
+
Test
|
|
420
|
+
</a>
|
|
421
|
+
</div>
|
|
422
|
+
<div @show="equals? .activeTab 'preview'">
|
|
423
|
+
<div class="bg-base-100 p-3" @push-view=".view">
|
|
424
|
+
<x render=".value"></x>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
<div class="p-3" @show="equals? .activeTab 'component'">
|
|
428
|
+
<x render=".componentView"></x>
|
|
429
|
+
</div>
|
|
430
|
+
<div class="p-3" @show="equals? .activeTab 'instance'">
|
|
431
|
+
<x render=".instanceView"></x>
|
|
432
|
+
</div>
|
|
433
|
+
<div class="p-3" @show="equals? .activeTab 'lint'">
|
|
434
|
+
<x render=".lintView"></x>
|
|
435
|
+
</div>
|
|
436
|
+
<div class="p-3" @show="equals? .activeTab 'test'">
|
|
437
|
+
<x render=".testView"></x>
|
|
322
438
|
</div>
|
|
323
439
|
</div>
|
|
324
440
|
</div>`
|
|
@@ -360,7 +476,7 @@ function buildStorybook(modules) {
|
|
|
360
476
|
return Array.isArray(raw) ? raw : [raw];
|
|
361
477
|
});
|
|
362
478
|
const sections = rawSections.map((s) => Section.Class.fromData(s)).sort((a, b) => a.title.localeCompare(b.title));
|
|
363
|
-
const components = new Set([Storybook, Section, Example]);
|
|
479
|
+
const components = new Set([Storybook, Section, Example, ...getInspectorComponents()]);
|
|
364
480
|
const macros = {};
|
|
365
481
|
const requestHandlers = {};
|
|
366
482
|
const overrideNames = new Set;
|
|
@@ -413,7 +529,7 @@ function buildExampleRequestHandlers({ requestHandlers: reals, overrideNames })
|
|
|
413
529
|
handlers[name] = makeMeta(name);
|
|
414
530
|
return handlers;
|
|
415
531
|
}
|
|
416
|
-
async function mountStorybook(selector, modules, { compileCss, root, persistUrl = true } = {}) {
|
|
532
|
+
async function mountStorybook(selector, modules, { compileCss, root, persistUrl = true, dev = null } = {}) {
|
|
417
533
|
const app = tutuca(selector);
|
|
418
534
|
const built = buildStorybook(modules);
|
|
419
535
|
app.state.set(root ?? built.root);
|
|
@@ -424,6 +540,9 @@ async function mountStorybook(selector, modules, { compileCss, root, persistUrl
|
|
|
424
540
|
if (persistUrl) {
|
|
425
541
|
scope.registerRequestHandlers({ persistState });
|
|
426
542
|
}
|
|
543
|
+
if (dev && app.state.val?.sections) {
|
|
544
|
+
app.state.set(await attachInspectorViews(app.state.val, scope, modules, dev));
|
|
545
|
+
}
|
|
427
546
|
if (compileCss) {
|
|
428
547
|
injectCss("tutuca-storybook", await compileCss(app));
|
|
429
548
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tutuca",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.99",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Zero-dependency SPA framework with immutable state and virtual DOM",
|
|
6
6
|
"main": "./dist/tutuca.js",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"./extra-ext": "./dist/tutuca-extra.ext.js",
|
|
15
15
|
"./dev-ext": "./dist/tutuca-dev.ext.js",
|
|
16
16
|
"./storybook": "./dist/tutuca-storybook.js",
|
|
17
|
+
"./components": "./dist/tutuca-components.js",
|
|
17
18
|
"./immutable": "./dist/immutable.js",
|
|
18
19
|
"./chai": "./dist/chai.js",
|
|
19
20
|
"./package.json": "./package.json"
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
"dist/tutuca-extra.ext.js",
|
|
60
61
|
"dist/tutuca-dev.ext.js",
|
|
61
62
|
"dist/tutuca-storybook.js",
|
|
63
|
+
"dist/tutuca-components.js",
|
|
62
64
|
"dist/immutable.js",
|
|
63
65
|
"dist/chai.js",
|
|
64
66
|
"skill"
|
package/skill/tutuca/cli.md
CHANGED
|
@@ -151,77 +151,19 @@ Filters:
|
|
|
151
151
|
Default format is `cli` (a tree with ✓/✗/○ and per-test durations);
|
|
152
152
|
`-f md` and `-f json` work too.
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
```js
|
|
158
|
-
export function getTests({ describe, test, expect }) {
|
|
159
|
-
describe(Counter, () => {
|
|
160
|
-
describe("inc()", () => { // method
|
|
161
|
-
test("returns a Counter with count + 1", () => {
|
|
162
|
-
const next = Counter.make().inc();
|
|
163
|
-
expect(next).toBeInstanceOf(Counter.Class);
|
|
164
|
-
expect(next.count).toBe(1);
|
|
165
|
-
});
|
|
166
|
-
test("does not mutate the original instance", () => {
|
|
167
|
-
const c = Counter.make({ count: 7 });
|
|
168
|
-
c.inc();
|
|
169
|
-
expect(c.count).toBe(7); // immutability
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
describe("dec()", () => { // input handler
|
|
174
|
-
test("returns a Counter with count - 1", () => {
|
|
175
|
-
const next = Counter.input.dec.call(Counter.make());
|
|
176
|
-
expect(next.count).toBe(-1);
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test("inc and dec round-trip", () => { // untagged path
|
|
181
|
-
expect(Counter.input.dec.call(Counter.make().inc()).count).toBe(0);
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
`describe(Counter, fn)` auto-tags the suite path with `Counter.name`, so
|
|
188
|
-
`tutuca test <module> Counter` picks it up. Untagged `test(...)` at the
|
|
189
|
-
top of a tagged `describe` inherits the tag.
|
|
154
|
+
The `getTests` shape and the handler calling conventions (`Comp.method()`,
|
|
155
|
+
`Comp.input.x.call(inst, …)`, the `drive` cascade helper, iteration
|
|
156
|
+
handlers) are in [testing.md](./testing.md).
|
|
190
157
|
|
|
191
158
|
## storybook — live component catalog
|
|
192
159
|
|
|
193
|
-
`tutuca storybook [dir]` serves a browser storybook
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
tutuca storybook ./packages/ui # scan + serve another directory
|
|
201
|
-
tutuca storybook --port 4321 # preferred port (falls back to a free one if taken)
|
|
202
|
-
tutuca storybook --out ./_site # write a static index.html + bootstrap instead of serving
|
|
203
|
-
tutuca storybook --dry-run # do all the prep + print what would be shown, don't serve (smoke test)
|
|
204
|
-
tutuca storybook --dry-run --json # same, machine-readable for agents
|
|
205
|
-
tutuca storybook --no-tests # skip the pre-serve getTests() run
|
|
206
|
-
tutuca storybook --no-margaui # render unstyled (skip margaui)
|
|
207
|
-
tutuca storybook --no-check # skip the in-browser check(app)
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
It is **batteries-included by default**: before serving it runs each module's
|
|
211
|
-
`getTests()` in the terminal, the page wires margaui styling, and the browser
|
|
212
|
-
runs `check(app)`. Each is individually disablable with the `--no-*` flags.
|
|
213
|
-
|
|
214
|
-
How tutuca itself is resolved (convention over configuration): a local
|
|
215
|
-
`node_modules/tutuca` install if present, else the CLI's own `dist`, else the
|
|
216
|
-
version-pinned CDN. All tutuca specifiers resolve to a single runtime, which
|
|
217
|
-
component scope/identity requires. `--out` always pins the CDN so the static
|
|
218
|
-
artifact is portable (host it from the project root so `/*.dev.js` paths resolve).
|
|
219
|
-
|
|
220
|
-
### Authoring `.dev.js` story modules
|
|
221
|
-
|
|
222
|
-
The `*.dev.js` convention (a dev-only module holding `getComponents()` +
|
|
223
|
-
`getExamples()` + `getTests()`, never shipped), the example/section shape, and
|
|
224
|
-
per-example request mocking are covered in [storybook.md](./storybook.md).
|
|
160
|
+
`tutuca storybook [dir]` serves a browser storybook with no setup — it
|
|
161
|
+
discovers co-located `*.dev.js` modules, runs their `getTests()`, wires
|
|
162
|
+
margaui, and serves an ephemeral page. Its flags (`--port`, `--out`,
|
|
163
|
+
`--dry-run`, `--no-margaui`, `--no-check`, `--no-tests`) are in the
|
|
164
|
+
Commands table above. Authoring `.dev.js` modules, the example/section shape,
|
|
165
|
+
per-example request mocking, and runtime resolution are all in
|
|
166
|
+
[storybook.md](./storybook.md).
|
|
225
167
|
|
|
226
168
|
## Install skill assets
|
|
227
169
|
|
package/skill/tutuca/core.md
CHANGED
|
@@ -657,12 +657,11 @@ inbound sources (WebSocket, `postMessage`, timers) have no element to bind
|
|
|
657
657
|
— route those through `app.sendAtRoot` instead (see
|
|
658
658
|
[request-response.md](./request-response.md)).
|
|
659
659
|
|
|
660
|
-
Pitfall: binding camelCase JS
|
|
661
|
-
fails
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
custom elements for use with Tutuca. See *Attribute Binding* above.
|
|
660
|
+
Pitfall: binding a camelCase JS property on a custom element silently
|
|
661
|
+
fails — `:mapId=".id"` assigns to `node.mapid`, never invoking
|
|
662
|
+
`set mapId`. Author custom elements with kebab-case attributes /
|
|
663
|
+
lowercased setters and bind via `:kebab-name`. See *Attribute Binding*
|
|
664
|
+
above for the full explanation.
|
|
666
665
|
|
|
667
666
|
## Conditional Display
|
|
668
667
|
|
|
@@ -931,66 +930,29 @@ CSS via the extra build), see [margaui.md](./margaui.md).
|
|
|
931
930
|
|
|
932
931
|
## Triggers and Handlers
|
|
933
932
|
|
|
934
|
-
Tutuca has four orchestration channels. Each
|
|
935
|
-
|
|
933
|
+
Tutuca has four orchestration channels. Each pairs a trigger with a
|
|
934
|
+
same-shape handler block:
|
|
936
935
|
|
|
937
|
-
| Triggered by | Handler block |
|
|
938
|
-
| ------------------------------------------- | ------------------- |
|
|
939
|
-
| DOM event (`click`, `input`, …) | `input: { ... }` |
|
|
940
|
-
| `ctx.
|
|
941
|
-
| `ctx.
|
|
942
|
-
| `ctx.
|
|
936
|
+
| Triggered by | Handler block | Use for |
|
|
937
|
+
| ------------------------------------------- | ------------------- | --------------------------------------------------- |
|
|
938
|
+
| DOM event (`click`, `input`, …) | `input: { ... }` | the component handling its own events |
|
|
939
|
+
| `ctx.bubble(name)` — event up the tree | `bubble: { ... }` | aggregate state an ancestor owns (logs, selections) |
|
|
940
|
+
| `ctx.send(name)` — message to a target path | `receive: { ... }` | addressing one known component (or self) |
|
|
941
|
+
| `ctx.request(name)` — async request | `response: { ... }` | fetch / timer / IndexedDB, result routed back |
|
|
943
942
|
|
|
944
943
|
Every handler is called as `handler(...args, ctx)` and returns a
|
|
945
|
-
(possibly updated) instance of `this
|
|
946
|
-
|
|
947
|
-
beyond `input` —
|
|
948
|
-
|
|
949
|
-
are
|
|
950
|
-
|
|
944
|
+
(possibly updated) instance of `this`, which the framework swaps into
|
|
945
|
+
the dispatch path; `ctx` is always the trailing argument. The three
|
|
946
|
+
channels beyond `input` — plus `ctx.at`, the `$unknown` fallback,
|
|
947
|
+
per-call handler-name overrides, error handling, and request-handler
|
|
948
|
+
registration — are in [request-response.md](./request-response.md);
|
|
949
|
+
worked snippets in
|
|
950
|
+
[patterns/coordinate-components.md](./patterns/coordinate-components.md).
|
|
951
951
|
|
|
952
952
|
`alter` is a fifth handler block, but unlike the four above it isn't
|
|
953
953
|
event-triggered — the renderer invokes alter handlers to produce
|
|
954
954
|
binds, not to update state. See *Mental model* and *Scope Enrichment*.
|
|
955
955
|
|
|
956
|
-
## Orchestration channels (bubble / send-receive / request-response)
|
|
957
|
-
|
|
958
|
-
Beyond local `input` handlers, three channels move state between
|
|
959
|
-
components. Full mechanics — when-to-use guidance, the `ctx.at`
|
|
960
|
-
`PathBuilder`, error handling, per-call handler-name overrides, the
|
|
961
|
-
`$unknown` fallback, and request-handler registration — are in
|
|
962
|
-
[request-response.md](./request-response.md). The essentials:
|
|
963
|
-
|
|
964
|
-
- **`bubble`** — `ctx.bubble("name", args)` walks the dispatch path
|
|
965
|
-
toward the root; each ancestor with `bubble.<name>(...args, ctx)`
|
|
966
|
-
runs (after descendants transact); `ctx.stopPropagation()` halts it.
|
|
967
|
-
Use for aggregate state owned by an ancestor (logs, selections).
|
|
968
|
-
|
|
969
|
-
```js
|
|
970
|
-
input: { onClick(ctx) { ctx.bubble("itemSelected", [this]); return this; } },
|
|
971
|
-
bubble: { itemSelected(item, ctx) { return this.insertInLogAt(0, item.label); } },
|
|
972
|
-
```
|
|
973
|
-
|
|
974
|
-
- **`send` / `receive`** — `ctx.send("name", args)` delivers a message
|
|
975
|
-
to one target (self by default, or `ctx.at.field("x").send(...)` /
|
|
976
|
-
`.index(name, i)` / `.key(name, k)` for another); the target's
|
|
977
|
-
`receive.<name>(...args, ctx)` runs. `receive.init` is a convention,
|
|
978
|
-
not a lifecycle hook — dispatch it via `app.sendAtRoot("init")`.
|
|
979
|
-
|
|
980
|
-
- **`request` / `response`** — `ctx.request("name", args)` runs a
|
|
981
|
-
host-registered async handler (registered with
|
|
982
|
-
`scope.registerRequestHandlers({...})`) and routes the result to
|
|
983
|
-
`response.<name>(res, err, ctx)` — `res` set on success, `err` on
|
|
984
|
-
failure. Use for fetch / timer / IndexedDB work.
|
|
985
|
-
|
|
986
|
-
```js
|
|
987
|
-
receive: { init(ctx) { ctx.request("loadData"); return this.setIsLoading(true); } },
|
|
988
|
-
response: { loadData(res, err, ctx) { return this.setIsLoading(false).setItems(res); } },
|
|
989
|
-
```
|
|
990
|
-
|
|
991
|
-
`ctx` is always the last argument of every `bubble` / `receive` /
|
|
992
|
-
`response` handler.
|
|
993
|
-
|
|
994
956
|
## Macros
|
|
995
957
|
|
|
996
958
|
Pure template expansion — no state, no methods. Calls inside a macro
|
|
@@ -1115,22 +1077,11 @@ export function getTests({ describe, test, expect }) { /*...*/ } // optiona
|
|
|
1115
1077
|
```
|
|
1116
1078
|
|
|
1117
1079
|
An example item may carry an optional **`requestHandlers`** map — per-example
|
|
1118
|
-
mocks
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
loading state:
|
|
1124
|
-
|
|
1125
|
-
```js
|
|
1126
|
-
items: [
|
|
1127
|
-
{ title: "Loaded", value: Widget.make(),
|
|
1128
|
-
requestHandlers: { async load() { return [{ id: 1, name: "Ada" }]; } } },
|
|
1129
|
-
{ title: "Error", value: Widget.make(),
|
|
1130
|
-
requestHandlers: { async load() { throw new Error("boom"); } } },
|
|
1131
|
-
{ title: "Default", value: Widget.make() }, // no mock → real handler / 404
|
|
1132
|
-
]
|
|
1133
|
-
```
|
|
1080
|
+
mocks (keyed by request name) that override the module's real
|
|
1081
|
+
`getRequestHandlers()` for that one instance, so two examples of the same
|
|
1082
|
+
component show different responses side by side. Return a fixture, `throw` for
|
|
1083
|
+
the error path, or never resolve to hold a loading state. Full treatment, plus
|
|
1084
|
+
the `on` lifecycle hooks, in [storybook.md](./storybook.md).
|
|
1134
1085
|
|
|
1135
1086
|
Best practice: have `getComponents()` return **every** component the module
|
|
1136
1087
|
defines — child and helper components included — and give each one at least
|
|
@@ -83,9 +83,10 @@ return curLeaf !== newLeaf ? txnPath.setValue(curRoot, newLeaf) : curRoot;
|
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
The root swap is atomic and identity-cheap: unchanged subtrees keep their
|
|
86
|
-
references, so re-render is incremental.
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
references, so re-render is incremental. Per-dispatch completion is tracked
|
|
87
|
+
by `Completion` (counter-based): `whenSettled()` resolves once a
|
|
88
|
+
transaction's own work finishes, `whenSubtreeSettled()` once the subtree it
|
|
89
|
+
spawned (requests, follow-on sends) settles too.
|
|
89
90
|
|
|
90
91
|
## Dispatch channels, semantically
|
|
91
92
|
|
package/skill/tutuca/testing.md
CHANGED
|
@@ -269,12 +269,9 @@ The "bad" forms force every test to construct
|
|
|
269
269
|
`{ target: { value: "42" } }` (or a fuller stub when more fields are
|
|
270
270
|
read), which is brittle and obscures intent.
|
|
271
271
|
|
|
272
|
-
|
|
273
|
-
Handling
|
|
274
|
-
|
|
275
|
-
`isDownKey`, `isSend`, `isCancel`, `isTabKey`, `ctx`, `dragInfo`.
|
|
276
|
-
`ctx` is auto-appended last. Reach for `event` only when no narrower
|
|
277
|
-
arg fits.
|
|
272
|
+
The built-in named args are listed in [core.md](./core.md) *Event
|
|
273
|
+
Handling*; `ctx` is auto-appended last. Reach for `event` only when no
|
|
274
|
+
narrower arg fits.
|
|
278
275
|
|
|
279
276
|
## Worked example
|
|
280
277
|
|