tutuca 0.9.97 → 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 +208 -69
- package/dist/tutuca-components.js +2444 -0
- package/dist/tutuca-dev.ext.js +92 -45
- package/dist/tutuca-dev.js +92 -45
- package/dist/tutuca-dev.min.js +2 -2
- package/dist/tutuca-extra.ext.js +56 -28
- package/dist/tutuca-extra.js +56 -28
- package/dist/tutuca-extra.min.js +2 -2
- package/dist/tutuca-storybook.js +136 -10
- package/dist/tutuca.ext.js +56 -28
- package/dist/tutuca.js +56 -28
- package/dist/tutuca.min.js +2 -2
- package/package.json +5 -3
- package/skill/tutuca/advanced.md +14 -5
- package/skill/tutuca/cli.md +10 -68
- package/skill/tutuca/core.md +103 -89
- package/skill/tutuca/margaui.md +25 -13
- package/skill/tutuca/patterns/README.md +1 -0
- package/skill/tutuca/patterns/filter-a-list.md +3 -1
- package/skill/tutuca/patterns/filter-and-paginate.md +116 -0
- package/skill/tutuca/patterns/paginate-a-list.md +3 -1
- package/skill/tutuca/semantics.md +4 -3
- package/skill/tutuca/storybook.md +7 -2
- package/skill/tutuca/testing.md +14 -6
- package/skill/tutuca-source/tutuca.ext.js +56 -28
|
@@ -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
|
|
|
@@ -27,7 +27,7 @@ same file is a valid target for `tutuca lint` / `test` / `render` too.
|
|
|
27
27
|
|
|
28
28
|
| Export | Returns | Used for |
|
|
29
29
|
| ------ | ------- | -------- |
|
|
30
|
-
| `getComponents()` | `[Comp, ...]` | stories — return **every** component the module defines, children and helpers included |
|
|
30
|
+
| `getComponents()` | `[Comp, ...]` | stories — return **every** component the module defines, children and helpers included. Components dedup by identity, so re-listing a leaf that another module also lists is safe (a composition module can re-list every leaf it uses). |
|
|
31
31
|
| `getExamples()` | one section, or an array of sections | the catalog cards |
|
|
32
32
|
| `getTests({ describe, test, expect })` | tests | the pre-serve test run (optional) |
|
|
33
33
|
| `getMacros()` | `{ name: macro }` | macros referenced in views (optional) |
|
|
@@ -37,7 +37,12 @@ same file is a valid target for `tutuca lint` / `test` / `render` too.
|
|
|
37
37
|
## Authoring stories (`getExamples`)
|
|
38
38
|
|
|
39
39
|
Return one section, or an array of sections to group examples under multiple
|
|
40
|
-
headings. A section is `{ title, description?, items: [...] }
|
|
40
|
+
headings. A section is `{ title, description?, items: [...] }`. An array of one
|
|
41
|
+
section behaves exactly like returning that section directly — both go through
|
|
42
|
+
the same `Section.fromData`, which **throws** on a malformed section (missing
|
|
43
|
+
`title`, not an object) rather than rendering a placeholder title. (If you saw
|
|
44
|
+
broken titles from a one-element array in an older build, that predates array
|
|
45
|
+
support — it has shipped since well before 0.9.88.)
|
|
41
46
|
|
|
42
47
|
```js
|
|
43
48
|
import { component, html } from "tutuca";
|
package/skill/tutuca/testing.md
CHANGED
|
@@ -114,10 +114,21 @@ export function getTests({ describe, test, expect, drive }) {
|
|
|
114
114
|
- `drive(value, phase, opts?)` builds a transactor over `value`, dispatches the
|
|
115
115
|
phase's actions at the root, awaits the whole cascade (including async
|
|
116
116
|
requests), and returns the **settled** instance.
|
|
117
|
+
- `drive` **always originates at the root** — there is no `at:`/path option. To
|
|
118
|
+
exercise a handler on a nested child, call it directly with `.call(child, …)`.
|
|
117
119
|
- `phase` is the same shape as an example's `on.init`
|
|
118
120
|
(`{ send, bubble, request, input, do }`; see
|
|
119
121
|
[storybook.md](./storybook.md#lifecycle-hooks-on)). `args` may be a function
|
|
120
122
|
`(self) => [...]`.
|
|
123
|
+
- A `bubble` action is a **no-op under `drive`**: bubbles travel child→parent, and
|
|
124
|
+
at the root there is no ancestor to receive it (the root's own `bubble` handler
|
|
125
|
+
is skipped too). To test a `bubble` handler, call it directly. (`drive` warns
|
|
126
|
+
when a phase contains a `bubble`.)
|
|
127
|
+
- These are *action kinds*, not methods. `$`-prefixed **methods** (auto setters/
|
|
128
|
+
togglers, `$foo`) are not an action kind — `on`/`drive` can only reach state
|
|
129
|
+
through `input`/`receive`/`response` handlers. To put a component into a method-
|
|
130
|
+
driven state for a test, call the method directly or route it through an `input`
|
|
131
|
+
handler.
|
|
121
132
|
- `request` actions resolve against the module's `getRequestHandlers()`.
|
|
122
133
|
- `opts.onMessage(message, before, after)` observes every committed transaction —
|
|
123
134
|
`message` is `{ kind, name, args, path }`, `before`/`after` are the root values
|
|
@@ -258,12 +269,9 @@ The "bad" forms force every test to construct
|
|
|
258
269
|
`{ target: { value: "42" } }` (or a fuller stub when more fields are
|
|
259
270
|
read), which is brittle and obscures intent.
|
|
260
271
|
|
|
261
|
-
|
|
262
|
-
Handling
|
|
263
|
-
|
|
264
|
-
`isDownKey`, `isSend`, `isCancel`, `isTabKey`, `ctx`, `dragInfo`.
|
|
265
|
-
`ctx` is auto-appended last. Reach for `event` only when no narrower
|
|
266
|
-
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.
|
|
267
275
|
|
|
268
276
|
## Worked example
|
|
269
277
|
|
|
@@ -1744,37 +1744,47 @@ class Renderer {
|
|
|
1744
1744
|
renderEach(stack, iterInfo, node, viewName) {
|
|
1745
1745
|
const { seq, filter, loopWith } = iterInfo.eval(stack);
|
|
1746
1746
|
const r = [];
|
|
1747
|
-
const { iterData, start, end } = unpackLoopResult(loopWith.call(stack.it, seq), seq);
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1747
|
+
const { iterData, start, end, keys } = unpackLoopResult(loopWith.call(stack.it, seq, makeLoopCtx(stack, filter)), seq);
|
|
1748
|
+
const renderOne = (key, value, attrName) => {
|
|
1749
|
+
const dom = this.renderIt(stack.enter(value, { key }, true), node, key, viewName);
|
|
1750
|
+
this.pushEachEntry(r, node.nodeId, attrName, key, dom);
|
|
1751
|
+
};
|
|
1752
|
+
if (keys)
|
|
1753
|
+
imKeysIter(seq, renderOne, keys);
|
|
1754
|
+
else
|
|
1755
|
+
getSeqInfo(seq)(seq, (key, value, attrName) => {
|
|
1756
|
+
if (filter.call(stack.it, key, value, iterData))
|
|
1757
|
+
renderOne(key, value, attrName);
|
|
1758
|
+
}, start, end);
|
|
1754
1759
|
return r;
|
|
1755
1760
|
}
|
|
1756
1761
|
renderEachWhen(stack, iterInfo, view, nid) {
|
|
1757
1762
|
const { seq, filter, loopWith, enricher } = iterInfo.eval(stack);
|
|
1758
1763
|
const r = [];
|
|
1759
1764
|
const it = stack.it;
|
|
1760
|
-
const { iterData, start, end } = unpackLoopResult(loopWith.call(it, seq), seq);
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
this.cache.set(cachePath, cacheKey, dom);
|
|
1775
|
-
}
|
|
1765
|
+
const { iterData, start, end, keys } = unpackLoopResult(loopWith.call(it, seq, makeLoopCtx(stack, filter)), seq);
|
|
1766
|
+
const renderOne = (key, value, attrName) => {
|
|
1767
|
+
const cachePath = enricher ? [view, it, value] : [view, value];
|
|
1768
|
+
const binds = { key, value };
|
|
1769
|
+
const cacheKey = `${nid}-${key}`;
|
|
1770
|
+
if (enricher)
|
|
1771
|
+
enricher.call(it, binds, key, value, iterData);
|
|
1772
|
+
const cachedNode = this.cache.get(cachePath, cacheKey);
|
|
1773
|
+
if (cachedNode)
|
|
1774
|
+
this.pushEachEntry(r, nid, attrName, key, cachedNode);
|
|
1775
|
+
else {
|
|
1776
|
+
const dom = this.renderView(view, stack.enter(value, binds, false));
|
|
1777
|
+
this.pushEachEntry(r, nid, attrName, key, dom);
|
|
1778
|
+
this.cache.set(cachePath, cacheKey, dom);
|
|
1776
1779
|
}
|
|
1777
|
-
}
|
|
1780
|
+
};
|
|
1781
|
+
if (keys)
|
|
1782
|
+
imKeysIter(seq, renderOne, keys);
|
|
1783
|
+
else
|
|
1784
|
+
getSeqInfo(seq)(seq, (key, value, attrName) => {
|
|
1785
|
+
if (filter.call(it, key, value, iterData))
|
|
1786
|
+
renderOne(key, value, attrName);
|
|
1787
|
+
}, start, end);
|
|
1778
1788
|
return r;
|
|
1779
1789
|
}
|
|
1780
1790
|
renderView(view, stack) {
|
|
@@ -1810,8 +1820,17 @@ var filterAlwaysTrue = (_v, _k, _seq) => true;
|
|
|
1810
1820
|
var nullLoopWith = (seq) => ({ iterData: { seq } });
|
|
1811
1821
|
var unpackLoopResult = (result, seq) => {
|
|
1812
1822
|
const r = result ?? {};
|
|
1813
|
-
return { iterData: r.iterData ?? { seq }, start: r.start, end: r.end };
|
|
1823
|
+
return { iterData: r.iterData ?? { seq }, start: r.start, end: r.end, keys: r.keys };
|
|
1824
|
+
};
|
|
1825
|
+
var imKeysIter = (seq, visit, keys) => {
|
|
1826
|
+
const attrName = isIndexed(seq) ? "si" : "sk";
|
|
1827
|
+
for (const key of keys)
|
|
1828
|
+
visit(key, seq.get(key), attrName);
|
|
1814
1829
|
};
|
|
1830
|
+
var makeLoopCtx = (stack, filter) => ({
|
|
1831
|
+
lookup: (name) => stack.lookupBind(name),
|
|
1832
|
+
filter: (key, value, iterData) => filter.call(stack.it, key, value, iterData)
|
|
1833
|
+
});
|
|
1815
1834
|
var imIndexedIter = (seq, visit, start, end) => {
|
|
1816
1835
|
const [s, e] = normalizeRange(start, end, seq.size);
|
|
1817
1836
|
for (let i = s;i < e; i++)
|
|
@@ -2359,11 +2378,11 @@ class IterInfo {
|
|
|
2359
2378
|
return { seq, filter, loopWith, enricher };
|
|
2360
2379
|
}
|
|
2361
2380
|
enrichBinds(stack, key) {
|
|
2362
|
-
const { seq, loopWith, enricher } = this.eval(stack);
|
|
2381
|
+
const { seq, filter, loopWith, enricher } = this.eval(stack);
|
|
2363
2382
|
const value = seq?.get ? seq.get(key, null) : null;
|
|
2364
2383
|
const binds = { key, value };
|
|
2365
2384
|
if (enricher) {
|
|
2366
|
-
const { iterData } = unpackLoopResult(loopWith.call(stack.it, seq), seq);
|
|
2385
|
+
const { iterData } = unpackLoopResult(loopWith.call(stack.it, seq, makeLoopCtx(stack, filter)), seq);
|
|
2367
2386
|
enricher.call(stack.it, binds, key, value, iterData);
|
|
2368
2387
|
}
|
|
2369
2388
|
return binds;
|
|
@@ -3714,6 +3733,13 @@ function phaseOps(phase) {
|
|
|
3714
3733
|
function resolveArgs(args, self) {
|
|
3715
3734
|
return typeof args === "function" ? args(self) ?? [] : args ?? [];
|
|
3716
3735
|
}
|
|
3736
|
+
function phaseHasBubble(phase) {
|
|
3737
|
+
if (!phase)
|
|
3738
|
+
return false;
|
|
3739
|
+
if (phase.bubble?.length)
|
|
3740
|
+
return true;
|
|
3741
|
+
return (phase.do ?? []).some((op) => op.type === "bubble");
|
|
3742
|
+
}
|
|
3717
3743
|
function dispatchPhase(dispatcher, targetPath, phase, self) {
|
|
3718
3744
|
if (!phase)
|
|
3719
3745
|
return;
|
|
@@ -4007,7 +4033,8 @@ class FieldSet extends Field {
|
|
|
4007
4033
|
}
|
|
4008
4034
|
function mkCompField(field, scope, args) {
|
|
4009
4035
|
const Comp = scope?.lookupComponent(field.type) ?? null;
|
|
4010
|
-
|
|
4036
|
+
if (Comp === null)
|
|
4037
|
+
console.warn(scope ? `component field "${field.name}": component "${field.type}" not found in scope` : `component field "${field.name}": cannot resolve component "${field.type}" — built without a registered scope (use ${field.type}.make({}) as the default, or build via a registered component)`);
|
|
4011
4038
|
return Comp?.make({ ...field.args, ...args }, { scope }) ?? null;
|
|
4012
4039
|
}
|
|
4013
4040
|
|
|
@@ -4156,6 +4183,7 @@ export {
|
|
|
4156
4183
|
removeIn,
|
|
4157
4184
|
remove,
|
|
4158
4185
|
phaseOps,
|
|
4186
|
+
phaseHasBubble,
|
|
4159
4187
|
mergeWith,
|
|
4160
4188
|
mergeDeepWith,
|
|
4161
4189
|
mergeDeep,
|