@llui/dom 0.0.34 → 0.0.36
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/dist/primitives/branch.d.ts.map +1 -1
- package/dist/primitives/branch.js +18 -3
- package/dist/primitives/branch.js.map +1 -1
- package/dist/primitives/child.d.ts.map +1 -1
- package/dist/primitives/child.js +15 -3
- package/dist/primitives/child.js.map +1 -1
- package/dist/primitives/each.d.ts.map +1 -1
- package/dist/primitives/each.js +36 -14
- package/dist/primitives/each.js.map +1 -1
- package/dist/primitives/foreign.d.ts.map +1 -1
- package/dist/primitives/foreign.js +15 -3
- package/dist/primitives/foreign.js.map +1 -1
- package/dist/primitives/sample.d.ts +32 -1
- package/dist/primitives/sample.d.ts.map +1 -1
- package/dist/primitives/sample.js +52 -2
- package/dist/primitives/sample.js.map +1 -1
- package/dist/primitives/show.d.ts.map +1 -1
- package/dist/primitives/show.js +13 -1
- package/dist/primitives/show.js.map +1 -1
- package/dist/primitives/virtual-each.d.ts.map +1 -1
- package/dist/primitives/virtual-each.js +25 -5
- package/dist/primitives/virtual-each.js.map +1 -1
- package/dist/render-context.d.ts +3 -0
- package/dist/render-context.d.ts.map +1 -1
- package/dist/render-context.js +27 -1
- package/dist/render-context.js.map +1 -1
- package/dist/update-loop.d.ts.map +1 -1
- package/dist/update-loop.js +174 -78
- package/dist/update-loop.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"foreign.d.ts","sourceRoot":"","sources":["../../src/primitives/foreign.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAQ,MAAM,aAAa,CAAA;AAOvD,wBAAgB,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,EACvE,IAAI,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,GACtC,IAAI,EAAE,
|
|
1
|
+
{"version":3,"file":"foreign.d.ts","sourceRoot":"","sources":["../../src/primitives/foreign.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAQ,MAAM,aAAa,CAAA;AAOvD,wBAAgB,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,EACvE,IAAI,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,GACtC,IAAI,EAAE,CA8HR"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getRenderContext } from '../render-context.js';
|
|
1
|
+
import { getRenderContext, enterAccessor, exitAccessor } from '../render-context.js';
|
|
2
2
|
import { createBinding } from '../binding.js';
|
|
3
3
|
import { createLifetime, addDisposer } from '../lifetime.js';
|
|
4
4
|
const FULL_MASK = 0xffffffff;
|
|
@@ -25,7 +25,19 @@ export function foreign(opts) {
|
|
|
25
25
|
const mountResult = opts.mount({ container, send: ctx.send });
|
|
26
26
|
let instance = undefined;
|
|
27
27
|
let disposed = false;
|
|
28
|
-
|
|
28
|
+
// `props` accessor wrapped so sample()/h.sample() inside throws a targeted
|
|
29
|
+
// error. Initial read and the per-update binding accessor both route
|
|
30
|
+
// through callProps.
|
|
31
|
+
const callProps = (state) => {
|
|
32
|
+
enterAccessor('foreign().props');
|
|
33
|
+
try {
|
|
34
|
+
return opts.props(state);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
exitAccessor();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
let latestProps = callProps(ctx.state);
|
|
29
41
|
let syncedProps = undefined;
|
|
30
42
|
const callSync = (props, prev) => {
|
|
31
43
|
if (typeof opts.sync === 'function') {
|
|
@@ -74,7 +86,7 @@ export function foreign(opts) {
|
|
|
74
86
|
createBinding(foreignScope, {
|
|
75
87
|
mask: FULL_MASK,
|
|
76
88
|
accessor: ((state) => {
|
|
77
|
-
const newProps =
|
|
89
|
+
const newProps = callProps(state);
|
|
78
90
|
// Shallow-diff against whichever we have: syncedProps is the
|
|
79
91
|
// truth once the instance is live; while still pending, latestProps
|
|
80
92
|
// is the most recent thing we've observed.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"foreign.js","sourceRoot":"","sources":["../../src/primitives/foreign.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"foreign.js","sourceRoot":"","sources":["../../src/primitives/foreign.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ForeignOptions, Send } from '../types.js'\nimport { getRenderContext, enterAccessor, exitAccessor } from '../render-context.js'\nimport { createBinding } from '../binding.js'\nimport { createLifetime, addDisposer } from '../lifetime.js'\n\nconst FULL_MASK = 0xffffffff\n\nexport function foreign<S, M, T extends Record<string, unknown>, Instance>(\n opts: ForeignOptions<S, M, T, Instance>,\n): Node[] {\n const ctx = getRenderContext('foreign')\n const parentLifetime = ctx.rootLifetime\n const foreignScope = createLifetime(parentLifetime)\n foreignScope._kind = 'foreign'\n\n // Create container element — cast to HTMLElement since foreign() is\n // HTML-only (no SVG/MathML foreign containers) and HTMLElement is the\n // contract callers see on the `mount({ container })` argument.\n const tag = opts.container?.tag ?? 'div'\n const container = ctx.dom.createElement(tag) as HTMLElement\n if (opts.container?.attrs) {\n for (const [key, value] of Object.entries(opts.container.attrs)) {\n container.setAttribute(key, value)\n }\n }\n\n // Instance may resolve synchronously or asynchronously. `instance ===\n // undefined` means \"not ready yet\" — `sync` is suppressed and the\n // binding below only tracks `latestProps`. When the instance arrives,\n // an initial `sync` runs with `latestProps` (may differ from the props\n // at mount time if state changed while awaiting).\n const mountResult = opts.mount({ container, send: ctx.send as Send<M> })\n let instance: Instance | undefined = undefined\n let disposed = false\n // `props` accessor wrapped so sample()/h.sample() inside throws a targeted\n // error. Initial read and the per-update binding accessor both route\n // through callProps.\n const callProps = (state: S): T => {\n enterAccessor('foreign().props')\n try {\n return opts.props(state)\n } finally {\n exitAccessor()\n }\n }\n let latestProps: T = callProps(ctx.state as S)\n let syncedProps: T | undefined = undefined\n\n const callSync = (props: T, prev: T | undefined) => {\n if (typeof opts.sync === 'function') {\n opts.sync({ instance: instance as Instance, props, prev })\n } else {\n for (const key of Object.keys(props) as Array<keyof T>) {\n if (!prev || !Object.is(props[key], prev[key])) {\n const handler = opts.sync[key]\n if (handler) {\n handler({ instance: instance as Instance, value: props[key], prev: prev?.[key] })\n }\n }\n }\n }\n syncedProps = props\n }\n\n const isPromise = (v: unknown): v is Promise<Instance> =>\n typeof v === 'object' && v !== null && typeof (v as { then?: unknown }).then === 'function'\n\n if (isPromise(mountResult)) {\n mountResult.then(\n (resolved) => {\n // Dispose-before-resolve: still destroy so the library cleans up\n // whatever it allocated. Bail before calling sync.\n if (disposed) {\n opts.destroy(resolved)\n return\n }\n instance = resolved\n // Initial sync runs with the latest props the binding observed\n // while we were awaiting. `prev: undefined` matches the sync-\n // path semantics for the first sync call.\n callSync(latestProps, undefined)\n },\n (err) => {\n // Async mount failures shouldn't crash the app. Log once and\n // leave the container empty — caller can inspect the DOM to\n // detect the mount failed. `errorBoundary` catches sync mount\n // throws but can't reach async rejections through the microtask\n // queue, which is why we log here instead of re-throwing.\n console.error('[LLui] foreign({ mount }) promise rejected:', err)\n },\n )\n } else {\n instance = mountResult\n callSync(latestProps, undefined)\n }\n\n // Register a binding for the props accessor — fires when state changes\n createBinding(foreignScope, {\n mask: FULL_MASK,\n accessor: ((state: S) => {\n const newProps = callProps(state)\n // Shallow-diff against whichever we have: syncedProps is the\n // truth once the instance is live; while still pending, latestProps\n // is the most recent thing we've observed.\n const compareTo = syncedProps ?? latestProps\n const changed = !shallowEqual(newProps, compareTo)\n\n if (changed) {\n latestProps = newProps\n // Only sync if the instance is ready. If not, `latestProps`\n // is updated and resolve will flush it.\n if (instance !== undefined) {\n callSync(newProps, syncedProps)\n }\n }\n\n return newProps\n }) as (state: never) => unknown,\n kind: 'text', // kind doesn't matter — applyBinding won't be called\n node: container,\n perItem: false,\n })\n\n // Destroy on scope disposal. If the mount promise is still pending,\n // flip the disposed flag so the resolve handler takes the\n // dispose-before-resolve path.\n addDisposer(foreignScope, () => {\n disposed = true\n if (instance !== undefined) {\n opts.destroy(instance)\n }\n // If instance is still undefined, the promise will destroy on resolve.\n })\n\n return [container]\n}\n\nfunction shallowEqual<T extends Record<string, unknown>>(a: T, b: T): boolean {\n for (const key of Object.keys(a) as Array<keyof T>) {\n if (!Object.is(a[key], b[key])) return false\n }\n for (const key of Object.keys(b) as Array<keyof T>) {\n if (!(key in a)) return false\n }\n return true\n}\n"]}
|
|
@@ -12,6 +12,36 @@
|
|
|
12
12
|
* Use `each` + `ItemAccessor` instead — see the "List of editable
|
|
13
13
|
* rows" recipe in the cookbook.
|
|
14
14
|
*
|
|
15
|
+
* **Don't use inside an accessor** (`each().key`, `each().items`,
|
|
16
|
+
* `branch().on`, `show().when`, `scope().on`, `child().props`,
|
|
17
|
+
* `foreign().props`, or a binding accessor like `text(s => …)`).
|
|
18
|
+
* Accessors must be pure functions of their parameter — the compiler's
|
|
19
|
+
* mask analysis only sees reads of the parameter, so a `sample()` read
|
|
20
|
+
* is invisible. The result is a hidden dependency that breaks
|
|
21
|
+
* reconciliation: structural blocks gate out updates that should fire,
|
|
22
|
+
* binding accessors miss state changes, and `key` callbacks create
|
|
23
|
+
* keys that don't track outer state correctly.
|
|
24
|
+
*
|
|
25
|
+
* To depend on outer state inside an accessor, **lift it into the
|
|
26
|
+
* accessor's parameter**. For `each().key` reading a sibling field
|
|
27
|
+
* `rev`:
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* // ❌ wrong — sample() in key is invisible to mask gating
|
|
31
|
+
* each({
|
|
32
|
+
* items: (s) => s.items,
|
|
33
|
+
* key: (it) => `${it.id}|${sample(s => s.rev)}`,
|
|
34
|
+
* render: …,
|
|
35
|
+
* })
|
|
36
|
+
*
|
|
37
|
+
* // ✅ right — bake outer state into items, key is pure of T
|
|
38
|
+
* each({
|
|
39
|
+
* items: (s) => s.items.map((it) => ({ it, rev: s.rev })),
|
|
40
|
+
* key: (r) => `${r.it.id}|${r.rev}`,
|
|
41
|
+
* render: …,
|
|
42
|
+
* })
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
15
45
|
* **Use for** passing a state snapshot to an imperative renderer
|
|
16
46
|
* (foreign libraries, third-party canvas/svg builders), reading a
|
|
17
47
|
* value to compute a static piece of structure that doesn't need to
|
|
@@ -24,7 +54,8 @@
|
|
|
24
54
|
* context is live — including `each.render`, whose bag intentionally
|
|
25
55
|
* does not carry View methods.
|
|
26
56
|
*
|
|
27
|
-
* Throws if called outside a render context
|
|
57
|
+
* Throws if called outside a render context, or from inside an
|
|
58
|
+
* accessor.
|
|
28
59
|
*/
|
|
29
60
|
export declare function sample<S, R>(selector: (s: S) => R): R;
|
|
30
61
|
//# sourceMappingURL=sample.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sample.d.ts","sourceRoot":"","sources":["../../src/primitives/sample.ts"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"sample.d.ts","sourceRoot":"","sources":["../../src/primitives/sample.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAwBrD"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getRenderContext } from '../render-context.js';
|
|
1
|
+
import { getRenderContext, currentAccessor } from '../render-context.js';
|
|
2
2
|
/**
|
|
3
3
|
* Read current state inside a render context and return the result of
|
|
4
4
|
* `selector(state)`. No binding is created, no mask is assigned — this
|
|
@@ -13,6 +13,36 @@ import { getRenderContext } from '../render-context.js';
|
|
|
13
13
|
* Use `each` + `ItemAccessor` instead — see the "List of editable
|
|
14
14
|
* rows" recipe in the cookbook.
|
|
15
15
|
*
|
|
16
|
+
* **Don't use inside an accessor** (`each().key`, `each().items`,
|
|
17
|
+
* `branch().on`, `show().when`, `scope().on`, `child().props`,
|
|
18
|
+
* `foreign().props`, or a binding accessor like `text(s => …)`).
|
|
19
|
+
* Accessors must be pure functions of their parameter — the compiler's
|
|
20
|
+
* mask analysis only sees reads of the parameter, so a `sample()` read
|
|
21
|
+
* is invisible. The result is a hidden dependency that breaks
|
|
22
|
+
* reconciliation: structural blocks gate out updates that should fire,
|
|
23
|
+
* binding accessors miss state changes, and `key` callbacks create
|
|
24
|
+
* keys that don't track outer state correctly.
|
|
25
|
+
*
|
|
26
|
+
* To depend on outer state inside an accessor, **lift it into the
|
|
27
|
+
* accessor's parameter**. For `each().key` reading a sibling field
|
|
28
|
+
* `rev`:
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* // ❌ wrong — sample() in key is invisible to mask gating
|
|
32
|
+
* each({
|
|
33
|
+
* items: (s) => s.items,
|
|
34
|
+
* key: (it) => `${it.id}|${sample(s => s.rev)}`,
|
|
35
|
+
* render: …,
|
|
36
|
+
* })
|
|
37
|
+
*
|
|
38
|
+
* // ✅ right — bake outer state into items, key is pure of T
|
|
39
|
+
* each({
|
|
40
|
+
* items: (s) => s.items.map((it) => ({ it, rev: s.rev })),
|
|
41
|
+
* key: (r) => `${r.it.id}|${r.rev}`,
|
|
42
|
+
* render: …,
|
|
43
|
+
* })
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
16
46
|
* **Use for** passing a state snapshot to an imperative renderer
|
|
17
47
|
* (foreign libraries, third-party canvas/svg builders), reading a
|
|
18
48
|
* value to compute a static piece of structure that doesn't need to
|
|
@@ -25,9 +55,29 @@ import { getRenderContext } from '../render-context.js';
|
|
|
25
55
|
* context is live — including `each.render`, whose bag intentionally
|
|
26
56
|
* does not carry View methods.
|
|
27
57
|
*
|
|
28
|
-
* Throws if called outside a render context
|
|
58
|
+
* Throws if called outside a render context, or from inside an
|
|
59
|
+
* accessor.
|
|
29
60
|
*/
|
|
30
61
|
export function sample(selector) {
|
|
62
|
+
const accessor = currentAccessor();
|
|
63
|
+
if (accessor !== null) {
|
|
64
|
+
throw new Error(`[LLui] sample() must not be called from inside ${accessor}. ` +
|
|
65
|
+
`Accessors must be pure functions of their parameter — sample() reads ` +
|
|
66
|
+
`state outside the parameter, which is invisible to the compiler's mask ` +
|
|
67
|
+
`analysis and breaks reconciliation.\n\n` +
|
|
68
|
+
`To depend on outer state, lift it into the accessor's parameter. For ` +
|
|
69
|
+
`each().key reading a sibling field, bake it into the items map:\n` +
|
|
70
|
+
` // ❌ wrong\n` +
|
|
71
|
+
` each({\n` +
|
|
72
|
+
` items: (s) => s.rows,\n` +
|
|
73
|
+
` key: (it) => \`\${it.id}|\${sample(s => s.rev)}\`,\n` +
|
|
74
|
+
` })\n` +
|
|
75
|
+
` // ✅ right\n` +
|
|
76
|
+
` each({\n` +
|
|
77
|
+
` items: (s) => s.rows.map((it) => ({ it, rev: s.rev })),\n` +
|
|
78
|
+
` key: (r) => \`\${r.it.id}|\${r.rev}\`,\n` +
|
|
79
|
+
` })`);
|
|
80
|
+
}
|
|
31
81
|
const ctx = getRenderContext('sample');
|
|
32
82
|
return selector(ctx.state);
|
|
33
83
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sample.js","sourceRoot":"","sources":["../../src/primitives/sample.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"sample.js","sourceRoot":"","sources":["../../src/primitives/sample.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAExE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,MAAM,UAAU,MAAM,CAAO,QAAqB;IAChD,MAAM,QAAQ,GAAG,eAAe,EAAE,CAAA;IAClC,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CACb,kDAAkD,QAAQ,IAAI;YAC5D,uEAAuE;YACvE,yEAAyE;YACzE,yCAAyC;YACzC,uEAAuE;YACvE,mEAAmE;YACnE,gBAAgB;YAChB,YAAY;YACZ,6BAA6B;YAC7B,0DAA0D;YAC1D,QAAQ;YACR,gBAAgB;YAChB,YAAY;YACZ,+DAA+D;YAC/D,8CAA8C;YAC9C,MAAM,CACT,CAAA;IACH,CAAC;IACD,MAAM,GAAG,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAA;IACtC,OAAO,QAAQ,CAAC,GAAG,CAAC,KAAU,CAAC,CAAA;AACjC,CAAC","sourcesContent":["import { getRenderContext, currentAccessor } from '../render-context.js'\n\n/**\n * Read current state inside a render context and return the result of\n * `selector(state)`. No binding is created, no mask is assigned — this\n * is a one-shot imperative read at view-construction time.\n *\n * **Don't use for variable-length lists.** Wrapping a list-render in\n * `sample` looks idiomatic but silently breaks reactivity: the\n * `.map(...)` runs once at construction, captures the row objects in\n * closure, and never re-runs when state updates in place. The cells\n * inside the captured rows show stale data; only a full structural\n * rebuild (e.g. a parent `branch` swapping arms) will refresh them.\n * Use `each` + `ItemAccessor` instead — see the \"List of editable\n * rows\" recipe in the cookbook.\n *\n * **Don't use inside an accessor** (`each().key`, `each().items`,\n * `branch().on`, `show().when`, `scope().on`, `child().props`,\n * `foreign().props`, or a binding accessor like `text(s => …)`).\n * Accessors must be pure functions of their parameter — the compiler's\n * mask analysis only sees reads of the parameter, so a `sample()` read\n * is invisible. The result is a hidden dependency that breaks\n * reconciliation: structural blocks gate out updates that should fire,\n * binding accessors miss state changes, and `key` callbacks create\n * keys that don't track outer state correctly.\n *\n * To depend on outer state inside an accessor, **lift it into the\n * accessor's parameter**. For `each().key` reading a sibling field\n * `rev`:\n *\n * ```ts\n * // ❌ wrong — sample() in key is invisible to mask gating\n * each({\n * items: (s) => s.items,\n * key: (it) => `${it.id}|${sample(s => s.rev)}`,\n * render: …,\n * })\n *\n * // ✅ right — bake outer state into items, key is pure of T\n * each({\n * items: (s) => s.items.map((it) => ({ it, rev: s.rev })),\n * key: (r) => `${r.it.id}|${r.rev}`,\n * render: …,\n * })\n * ```\n *\n * **Use for** passing a state snapshot to an imperative renderer\n * (foreign libraries, third-party canvas/svg builders), reading a\n * value to compute a static piece of structure that doesn't need to\n * react, or any case where a reactive binding would be semantically\n * wrong (e.g. capturing a value at *this exact moment* for a\n * one-shot side effect).\n *\n * Also exposed as `h.sample` on the View bag for destructure-from-`h`\n * ergonomics. The top-level import form works everywhere a render\n * context is live — including `each.render`, whose bag intentionally\n * does not carry View methods.\n *\n * Throws if called outside a render context, or from inside an\n * accessor.\n */\nexport function sample<S, R>(selector: (s: S) => R): R {\n const accessor = currentAccessor()\n if (accessor !== null) {\n throw new Error(\n `[LLui] sample() must not be called from inside ${accessor}. ` +\n `Accessors must be pure functions of their parameter — sample() reads ` +\n `state outside the parameter, which is invisible to the compiler's mask ` +\n `analysis and breaks reconciliation.\\n\\n` +\n `To depend on outer state, lift it into the accessor's parameter. For ` +\n `each().key reading a sibling field, bake it into the items map:\\n` +\n ` // ❌ wrong\\n` +\n ` each({\\n` +\n ` items: (s) => s.rows,\\n` +\n ` key: (it) => \\`\\${it.id}|\\${sample(s => s.rev)}\\`,\\n` +\n ` })\\n` +\n ` // ✅ right\\n` +\n ` each({\\n` +\n ` items: (s) => s.rows.map((it) => ({ it, rev: s.rev })),\\n` +\n ` key: (r) => \\`\\${r.it.id}|\\${r.rev}\\`,\\n` +\n ` })`,\n )\n }\n const ctx = getRenderContext('sample')\n return selector(ctx.state as S)\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"show.d.ts","sourceRoot":"","sources":["../../src/primitives/show.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"show.d.ts","sourceRoot":"","sources":["../../src/primitives/show.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAM9C,wBAAgB,IAAI,CAAC,CAAC,EAAE,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,CAyBpE"}
|
package/dist/primitives/show.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { enterAccessor, exitAccessor } from '../render-context.js';
|
|
1
2
|
import { branch } from './branch.js';
|
|
2
3
|
const EMPTY = () => [];
|
|
3
4
|
export function show(opts) {
|
|
@@ -8,7 +9,18 @@ export function show(opts) {
|
|
|
8
9
|
// branch.on is string-only; stringify the boolean for the case lookup.
|
|
9
10
|
// JS object literals stringify boolean keys, so `cases.{true, false}`
|
|
10
11
|
// matches `String(true)` / `String(false)`.
|
|
11
|
-
|
|
12
|
+
//
|
|
13
|
+
// Label as `show().when` (not `branch().on`) so a sample() inside the
|
|
14
|
+
// user's `when` callback yields an error pointing at the right surface.
|
|
15
|
+
on: (s) => {
|
|
16
|
+
enterAccessor('show().when');
|
|
17
|
+
try {
|
|
18
|
+
return String(opts.when(s));
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
exitAccessor();
|
|
22
|
+
}
|
|
23
|
+
},
|
|
12
24
|
cases: { true: opts.render, false: opts.fallback ?? EMPTY },
|
|
13
25
|
enter: opts.enter,
|
|
14
26
|
leave: opts.leave,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"show.js","sourceRoot":"","sources":["../../src/primitives/show.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC,MAAM,KAAK,GAAG,GAAG,EAAE,CAAC,EAAY,CAAA;AAEhC,MAAM,UAAU,IAAI,CAAiB,IAAuB;IAC1D,sEAAsE;IACtE,wEAAwE;IACxE,2BAA2B;IAC3B,OAAO,MAAM,CAAO;QAClB,uEAAuE;QACvE,sEAAsE;QACtE,4CAA4C;QAC5C,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"show.js","sourceRoot":"","sources":["../../src/primitives/show.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAClE,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC,MAAM,KAAK,GAAG,GAAG,EAAE,CAAC,EAAY,CAAA;AAEhC,MAAM,UAAU,IAAI,CAAiB,IAAuB;IAC1D,sEAAsE;IACtE,wEAAwE;IACxE,2BAA2B;IAC3B,OAAO,MAAM,CAAO;QAClB,uEAAuE;QACvE,sEAAsE;QACtE,4CAA4C;QAC5C,EAAE;QACF,sEAAsE;QACtE,wEAAwE;QACxE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE;YACR,aAAa,CAAC,aAAa,CAAC,CAAA;YAC5B,IAAI,CAAC;gBACH,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;YAC7B,CAAC;oBAAS,CAAC;gBACT,YAAY,EAAE,CAAA;YAChB,CAAC;QACH,CAAC;QACD,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,QAAQ,IAAI,KAAK,EAAE;QAC3D,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,eAAe,EAAE,WAAW;KAC7B,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import type { ShowOptions } from '../types.js'\nimport { enterAccessor, exitAccessor } from '../render-context.js'\nimport { branch } from './branch.js'\n\nconst EMPTY = () => [] as Node[]\n\nexport function show<S, M = unknown>(opts: ShowOptions<S, M>): Node[] {\n // `__disposalCause` is read by branch.ts when it disposes the leaving\n // arm — it lets the disposer log distinguish show/hide transitions from\n // multi-case branch swaps.\n return branch<S, M>({\n // branch.on is string-only; stringify the boolean for the case lookup.\n // JS object literals stringify boolean keys, so `cases.{true, false}`\n // matches `String(true)` / `String(false)`.\n //\n // Label as `show().when` (not `branch().on`) so a sample() inside the\n // user's `when` callback yields an error pointing at the right surface.\n on: (s) => {\n enterAccessor('show().when')\n try {\n return String(opts.when(s))\n } finally {\n exitAccessor()\n }\n },\n cases: { true: opts.render, false: opts.fallback ?? EMPTY },\n enter: opts.enter,\n leave: opts.leave,\n onTransition: opts.onTransition,\n __disposalCause: 'show-hide',\n })\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"virtual-each.d.ts","sourceRoot":"","sources":["../../src/primitives/virtual-each.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAY,IAAI,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"virtual-each.d.ts","sourceRoot":"","sources":["../../src/primitives/virtual-each.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAY,IAAI,EAAE,MAAM,aAAa,CAAA;AAc/D,MAAM,WAAW,kBAAkB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,OAAO;IACnD,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAA;IACpB,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,MAAM,CAAA;IACjC,qFAAqF;IACrF,UAAU,EAAE,MAAM,CAAA;IAClB,6CAA6C;IAC7C,eAAe,EAAE,MAAM,CAAA;IACvB,sFAAsF;IACtF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,CAAC,IAAI,EAAE;QACb,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;QACb,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,CAAA;QACrB,GAAG,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC,CAAA;QAC1C,KAAK,EAAE,MAAM,MAAM,CAAA;KACpB,KAAK,IAAI,EAAE,CAAA;CACb;AAkBD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,kBAAkB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,CAsNxF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getRenderContext, setRenderContext, clearRenderContext, } from '../render-context.js';
|
|
1
|
+
import { getRenderContext, setRenderContext, clearRenderContext, enterAccessor, exitAccessor, } from '../render-context.js';
|
|
2
2
|
import { createLifetime, disposeLifetime, addDisposer } from '../lifetime.js';
|
|
3
3
|
import { getFlatBindings, setFlatBindings } from '../binding.js';
|
|
4
4
|
import { FULL_MASK } from '../update-loop.js';
|
|
@@ -63,8 +63,28 @@ export function virtualEach(opts) {
|
|
|
63
63
|
const end = Math.min(length, Math.ceil((scrollTop + opts.containerHeight) / opts.itemHeight) + overscan);
|
|
64
64
|
return [start, end];
|
|
65
65
|
};
|
|
66
|
+
// Wrap accessor invocations to make sample()/h.sample() inside throw a
|
|
67
|
+
// targeted error. Mirrors the wrappers in each.ts.
|
|
68
|
+
const callItems = (state) => {
|
|
69
|
+
enterAccessor('virtualEach().items');
|
|
70
|
+
try {
|
|
71
|
+
return opts.items(state);
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
exitAccessor();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const callKey = (item) => {
|
|
78
|
+
enterAccessor('virtualEach().key');
|
|
79
|
+
try {
|
|
80
|
+
return opts.key(item);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
exitAccessor();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
66
86
|
const buildEntry = (item, index, state) => {
|
|
67
|
-
const key =
|
|
87
|
+
const key = callKey(item);
|
|
68
88
|
const scope = createLifetime(parentLifetime);
|
|
69
89
|
const wrapper = ctx.dom.createElement('div');
|
|
70
90
|
wrapper.style.position = 'absolute';
|
|
@@ -127,7 +147,7 @@ export function virtualEach(opts) {
|
|
|
127
147
|
return entry;
|
|
128
148
|
};
|
|
129
149
|
const reconcile = (state) => {
|
|
130
|
-
const items =
|
|
150
|
+
const items = callItems(state);
|
|
131
151
|
lastItems = items;
|
|
132
152
|
// Update spacer total height
|
|
133
153
|
spacer.style.height = `${items.length * opts.itemHeight}px`;
|
|
@@ -147,7 +167,7 @@ export function virtualEach(opts) {
|
|
|
147
167
|
const visibleKeys = new Map();
|
|
148
168
|
for (let i = start; i < end; i++) {
|
|
149
169
|
const item = items[i];
|
|
150
|
-
visibleKeys.set(
|
|
170
|
+
visibleKeys.set(callKey(item), { item, index: i });
|
|
151
171
|
}
|
|
152
172
|
// Dispose entries no longer visible
|
|
153
173
|
for (const [key, entry] of entries) {
|
|
@@ -188,7 +208,7 @@ export function virtualEach(opts) {
|
|
|
188
208
|
const block = {
|
|
189
209
|
mask: FULL_MASK,
|
|
190
210
|
reconcile(state) {
|
|
191
|
-
const newItems =
|
|
211
|
+
const newItems = callItems(state);
|
|
192
212
|
if (newItems === lastItems)
|
|
193
213
|
return;
|
|
194
214
|
reconcile(state);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"virtual-each.js","sourceRoot":"","sources":["../../src/primitives/virtual-each.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ItemAccessor, Lifetime, Send } from '../types.js'\nimport {\n getRenderContext,\n setRenderContext,\n clearRenderContext,\n type RenderContext,\n} from '../render-context.js'\nimport { createLifetime, disposeLifetime, addDisposer } from '../lifetime.js'\nimport { getFlatBindings, setFlatBindings } from '../binding.js'\nimport { FULL_MASK } from '../update-loop.js'\nimport type { StructuralBlock } from '../structural.js'\n\nexport interface VirtualEachOptions<S, T, M = unknown> {\n items: (s: S) => T[]\n key: (item: T) => string | number\n /** Fixed pixel height per item. Required — dynamic heights are not supported yet. */\n itemHeight: number\n /** Scrollable container height in pixels. */\n containerHeight: number\n /** Extra rows to render above/below the viewport for smooth scrolling. Default: 3. */\n overscan?: number\n /** Optional class for the scroll container. */\n class?: string\n render: (opts: {\n send: Send<M>\n item: ItemAccessor<T>\n acc: <R>(selector: (t: T) => R) => () => R\n index: () => number\n }) => Node[]\n}\n\ninterface VirtualEntry<T> {\n key: string | number\n current: T\n index: number\n scope: Lifetime\n wrapper: HTMLElement\n}\n\nconst buildCtx: RenderContext = {\n rootLifetime: null as unknown as Lifetime,\n state: null,\n allBindings: [],\n structuralBlocks: [],\n dom: null as unknown as import('../dom-env.js').DomEnv,\n}\n\n/**\n * Virtualized list — renders only the rows visible in the scroll viewport.\n * Use for lists with 1k+ items where a regular `each()` would be too slow.\n *\n * Current limitations:\n * - Fixed row height (`itemHeight`) — dynamic heights not supported\n * - No transitions / animations\n * - No cross-container reuse (items outside view are fully disposed)\n *\n * ```ts\n * view: ({ text }) => [\n * ...virtualEach({\n * items: (s) => s.rows,\n * key: (r) => r.id,\n * itemHeight: 40,\n * containerHeight: 600,\n * render: ({ item }) => [\n * div({ class: 'row' }, [text(item.label)]),\n * ],\n * }),\n * ]\n * ```\n */\nexport function virtualEach<S, T, M = unknown>(opts: VirtualEachOptions<S, T, M>): Node[] {\n const ctx = getRenderContext('virtualEach')\n const parentLifetime = ctx.rootLifetime\n const blocks = ctx.structuralBlocks\n const send = ctx.send as (msg: M) => void\n\n const overscan = opts.overscan ?? 3\n\n // Scroll container\n const scroll = ctx.dom.createElement('div') as HTMLDivElement\n scroll.style.overflow = 'auto'\n scroll.style.position = 'relative'\n scroll.style.height = `${opts.containerHeight}px`\n scroll.dataset.virtualContainer = ''\n if (opts.class) scroll.className = opts.class\n\n // Inner spacer sized to full content height\n const spacer = ctx.dom.createElement('div') as HTMLDivElement\n spacer.style.position = 'relative'\n spacer.style.width = '100%'\n spacer.dataset.virtualSpacer = ''\n scroll.appendChild(spacer)\n\n // Map of key → entry\n const entries = new Map<string | number, VirtualEntry<T>>()\n let lastItems: T[] = []\n let scrollTop = 0\n\n const computeRange = (length: number): [number, number] => {\n if (length === 0) return [0, 0]\n const start = Math.max(0, Math.floor(scrollTop / opts.itemHeight) - overscan)\n const end = Math.min(\n length,\n Math.ceil((scrollTop + opts.containerHeight) / opts.itemHeight) + overscan,\n )\n return [start, end]\n }\n\n const buildEntry = (item: T, index: number, state: S): VirtualEntry<T> => {\n const key = opts.key(item)\n const scope = createLifetime(parentLifetime)\n\n const wrapper = ctx.dom.createElement('div') as HTMLDivElement\n wrapper.style.position = 'absolute'\n wrapper.style.top = `${index * opts.itemHeight}px`\n wrapper.style.left = '0'\n wrapper.style.right = '0'\n wrapper.style.height = `${opts.itemHeight}px`\n wrapper.dataset.virtualItem = ''\n wrapper.dataset.virtualKey = String(key)\n\n const entry: VirtualEntry<T> = { key, current: item, index, scope, wrapper }\n\n // Item accessor: item(selector) and item.field\n const itemFn = <R>(selector: (t: T) => R): (() => R) => {\n const accessor = (): R => selector(entry.current)\n ;(accessor as unknown as { __perItem: true }).__perItem = true\n return accessor\n }\n\n let itemProxy: ItemAccessor<T> | null = null\n const getItemProxy = (): ItemAccessor<T> => {\n if (itemProxy) return itemProxy\n const fieldCache = new Map<string, () => unknown>()\n itemProxy = new Proxy(itemFn as object, {\n get(target, prop) {\n if (typeof prop === 'symbol' || prop === 'then' || prop === 'prototype') {\n return Reflect.get(target, prop)\n }\n const k = prop as string\n const cached = fieldCache.get(k)\n if (cached) return cached\n const accessor = (): unknown => (entry.current as Record<string, unknown>)[k]\n ;(accessor as unknown as { __perItem: true }).__perItem = true\n fieldCache.set(k, accessor)\n return accessor\n },\n }) as ItemAccessor<T>\n return itemProxy\n }\n\n const indexAccessor = (): number => entry.index\n\n buildCtx.rootLifetime = scope\n buildCtx.state = state\n buildCtx.allBindings = ctx.allBindings\n buildCtx.structuralBlocks = ctx.structuralBlocks\n buildCtx.dom = ctx.dom\n buildCtx.instance = ctx.instance\n const prevFlat = getFlatBindings()\n setFlatBindings(ctx.allBindings)\n setRenderContext(buildCtx)\n\n const nodes = opts.render({\n send,\n item: getItemProxy(),\n acc: itemFn,\n index: indexAccessor,\n })\n\n clearRenderContext()\n setFlatBindings(prevFlat)\n setRenderContext(ctx)\n\n for (const node of nodes) wrapper.appendChild(node)\n return entry\n }\n\n const reconcile = (state: S): void => {\n const items = opts.items(state)\n lastItems = items\n\n // Update spacer total height\n spacer.style.height = `${items.length * opts.itemHeight}px`\n\n if (items.length === 0) {\n // Dispose all entries\n for (const entry of entries.values()) {\n entry.scope.disposalCause = 'each-remove'\n disposeLifetime(entry.scope)\n if (entry.wrapper.parentNode) entry.wrapper.parentNode.removeChild(entry.wrapper)\n }\n entries.clear()\n return\n }\n\n const [start, end] = computeRange(items.length)\n\n // Build a map of key → {item, index} for the visible range\n const visibleKeys = new Map<string | number, { item: T; index: number }>()\n for (let i = start; i < end; i++) {\n const item = items[i]!\n visibleKeys.set(opts.key(item), { item, index: i })\n }\n\n // Dispose entries no longer visible\n for (const [key, entry] of entries) {\n if (!visibleKeys.has(key)) {\n entry.scope.disposalCause = 'each-remove'\n disposeLifetime(entry.scope)\n if (entry.wrapper.parentNode) entry.wrapper.parentNode.removeChild(entry.wrapper)\n entries.delete(key)\n }\n }\n\n // Create new entries + update existing\n for (const [key, { item, index }] of visibleKeys) {\n const existing = entries.get(key)\n if (existing) {\n existing.current = item\n if (existing.index !== index) {\n existing.index = index\n existing.wrapper.style.top = `${index * opts.itemHeight}px`\n }\n } else {\n const entry = buildEntry(item, index, state)\n entries.set(key, entry)\n spacer.appendChild(entry.wrapper)\n }\n }\n }\n\n // Scroll handler — reconcile visible range without touching component state\n const onScroll = (): void => {\n scrollTop = scroll.scrollTop\n reconcile(ctx.state as S)\n }\n scroll.addEventListener('scroll', onScroll, { passive: true })\n\n // Register as a structural block BEFORE initial render so this block\n // precedes any nested blocks its rows register. See branch.ts for the\n // full rationale (Phase 1 iteration safety).\n const block: StructuralBlock = {\n mask: FULL_MASK,\n reconcile(state: unknown) {\n const newItems = opts.items(state as S)\n if (newItems === lastItems) return\n reconcile(state as S)\n },\n }\n blocks.push(block)\n\n // Initial render\n reconcile(ctx.state as S)\n\n // Cleanup on parent disposal\n addDisposer(parentLifetime, () => {\n scroll.removeEventListener('scroll', onScroll)\n for (const entry of entries.values()) {\n disposeLifetime(entry.scope)\n }\n entries.clear()\n })\n\n return [scroll]\n}\n"]}
|
|
1
|
+
{"version":3,"file":"virtual-each.js","sourceRoot":"","sources":["../../src/primitives/virtual-each.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ItemAccessor, Lifetime, Send } from '../types.js'\nimport {\n getRenderContext,\n setRenderContext,\n clearRenderContext,\n enterAccessor,\n exitAccessor,\n type RenderContext,\n} from '../render-context.js'\nimport { createLifetime, disposeLifetime, addDisposer } from '../lifetime.js'\nimport { getFlatBindings, setFlatBindings } from '../binding.js'\nimport { FULL_MASK } from '../update-loop.js'\nimport type { StructuralBlock } from '../structural.js'\n\nexport interface VirtualEachOptions<S, T, M = unknown> {\n items: (s: S) => T[]\n key: (item: T) => string | number\n /** Fixed pixel height per item. Required — dynamic heights are not supported yet. */\n itemHeight: number\n /** Scrollable container height in pixels. */\n containerHeight: number\n /** Extra rows to render above/below the viewport for smooth scrolling. Default: 3. */\n overscan?: number\n /** Optional class for the scroll container. */\n class?: string\n render: (opts: {\n send: Send<M>\n item: ItemAccessor<T>\n acc: <R>(selector: (t: T) => R) => () => R\n index: () => number\n }) => Node[]\n}\n\ninterface VirtualEntry<T> {\n key: string | number\n current: T\n index: number\n scope: Lifetime\n wrapper: HTMLElement\n}\n\nconst buildCtx: RenderContext = {\n rootLifetime: null as unknown as Lifetime,\n state: null,\n allBindings: [],\n structuralBlocks: [],\n dom: null as unknown as import('../dom-env.js').DomEnv,\n}\n\n/**\n * Virtualized list — renders only the rows visible in the scroll viewport.\n * Use for lists with 1k+ items where a regular `each()` would be too slow.\n *\n * Current limitations:\n * - Fixed row height (`itemHeight`) — dynamic heights not supported\n * - No transitions / animations\n * - No cross-container reuse (items outside view are fully disposed)\n *\n * ```ts\n * view: ({ text }) => [\n * ...virtualEach({\n * items: (s) => s.rows,\n * key: (r) => r.id,\n * itemHeight: 40,\n * containerHeight: 600,\n * render: ({ item }) => [\n * div({ class: 'row' }, [text(item.label)]),\n * ],\n * }),\n * ]\n * ```\n */\nexport function virtualEach<S, T, M = unknown>(opts: VirtualEachOptions<S, T, M>): Node[] {\n const ctx = getRenderContext('virtualEach')\n const parentLifetime = ctx.rootLifetime\n const blocks = ctx.structuralBlocks\n const send = ctx.send as (msg: M) => void\n\n const overscan = opts.overscan ?? 3\n\n // Scroll container\n const scroll = ctx.dom.createElement('div') as HTMLDivElement\n scroll.style.overflow = 'auto'\n scroll.style.position = 'relative'\n scroll.style.height = `${opts.containerHeight}px`\n scroll.dataset.virtualContainer = ''\n if (opts.class) scroll.className = opts.class\n\n // Inner spacer sized to full content height\n const spacer = ctx.dom.createElement('div') as HTMLDivElement\n spacer.style.position = 'relative'\n spacer.style.width = '100%'\n spacer.dataset.virtualSpacer = ''\n scroll.appendChild(spacer)\n\n // Map of key → entry\n const entries = new Map<string | number, VirtualEntry<T>>()\n let lastItems: T[] = []\n let scrollTop = 0\n\n const computeRange = (length: number): [number, number] => {\n if (length === 0) return [0, 0]\n const start = Math.max(0, Math.floor(scrollTop / opts.itemHeight) - overscan)\n const end = Math.min(\n length,\n Math.ceil((scrollTop + opts.containerHeight) / opts.itemHeight) + overscan,\n )\n return [start, end]\n }\n\n // Wrap accessor invocations to make sample()/h.sample() inside throw a\n // targeted error. Mirrors the wrappers in each.ts.\n const callItems = (state: S): T[] => {\n enterAccessor('virtualEach().items')\n try {\n return opts.items(state)\n } finally {\n exitAccessor()\n }\n }\n const callKey = (item: T): string | number => {\n enterAccessor('virtualEach().key')\n try {\n return opts.key(item)\n } finally {\n exitAccessor()\n }\n }\n\n const buildEntry = (item: T, index: number, state: S): VirtualEntry<T> => {\n const key = callKey(item)\n const scope = createLifetime(parentLifetime)\n\n const wrapper = ctx.dom.createElement('div') as HTMLDivElement\n wrapper.style.position = 'absolute'\n wrapper.style.top = `${index * opts.itemHeight}px`\n wrapper.style.left = '0'\n wrapper.style.right = '0'\n wrapper.style.height = `${opts.itemHeight}px`\n wrapper.dataset.virtualItem = ''\n wrapper.dataset.virtualKey = String(key)\n\n const entry: VirtualEntry<T> = { key, current: item, index, scope, wrapper }\n\n // Item accessor: item(selector) and item.field\n const itemFn = <R>(selector: (t: T) => R): (() => R) => {\n const accessor = (): R => selector(entry.current)\n ;(accessor as unknown as { __perItem: true }).__perItem = true\n return accessor\n }\n\n let itemProxy: ItemAccessor<T> | null = null\n const getItemProxy = (): ItemAccessor<T> => {\n if (itemProxy) return itemProxy\n const fieldCache = new Map<string, () => unknown>()\n itemProxy = new Proxy(itemFn as object, {\n get(target, prop) {\n if (typeof prop === 'symbol' || prop === 'then' || prop === 'prototype') {\n return Reflect.get(target, prop)\n }\n const k = prop as string\n const cached = fieldCache.get(k)\n if (cached) return cached\n const accessor = (): unknown => (entry.current as Record<string, unknown>)[k]\n ;(accessor as unknown as { __perItem: true }).__perItem = true\n fieldCache.set(k, accessor)\n return accessor\n },\n }) as ItemAccessor<T>\n return itemProxy\n }\n\n const indexAccessor = (): number => entry.index\n\n buildCtx.rootLifetime = scope\n buildCtx.state = state\n buildCtx.allBindings = ctx.allBindings\n buildCtx.structuralBlocks = ctx.structuralBlocks\n buildCtx.dom = ctx.dom\n buildCtx.instance = ctx.instance\n const prevFlat = getFlatBindings()\n setFlatBindings(ctx.allBindings)\n setRenderContext(buildCtx)\n\n const nodes = opts.render({\n send,\n item: getItemProxy(),\n acc: itemFn,\n index: indexAccessor,\n })\n\n clearRenderContext()\n setFlatBindings(prevFlat)\n setRenderContext(ctx)\n\n for (const node of nodes) wrapper.appendChild(node)\n return entry\n }\n\n const reconcile = (state: S): void => {\n const items = callItems(state)\n lastItems = items\n\n // Update spacer total height\n spacer.style.height = `${items.length * opts.itemHeight}px`\n\n if (items.length === 0) {\n // Dispose all entries\n for (const entry of entries.values()) {\n entry.scope.disposalCause = 'each-remove'\n disposeLifetime(entry.scope)\n if (entry.wrapper.parentNode) entry.wrapper.parentNode.removeChild(entry.wrapper)\n }\n entries.clear()\n return\n }\n\n const [start, end] = computeRange(items.length)\n\n // Build a map of key → {item, index} for the visible range\n const visibleKeys = new Map<string | number, { item: T; index: number }>()\n for (let i = start; i < end; i++) {\n const item = items[i]!\n visibleKeys.set(callKey(item), { item, index: i })\n }\n\n // Dispose entries no longer visible\n for (const [key, entry] of entries) {\n if (!visibleKeys.has(key)) {\n entry.scope.disposalCause = 'each-remove'\n disposeLifetime(entry.scope)\n if (entry.wrapper.parentNode) entry.wrapper.parentNode.removeChild(entry.wrapper)\n entries.delete(key)\n }\n }\n\n // Create new entries + update existing\n for (const [key, { item, index }] of visibleKeys) {\n const existing = entries.get(key)\n if (existing) {\n existing.current = item\n if (existing.index !== index) {\n existing.index = index\n existing.wrapper.style.top = `${index * opts.itemHeight}px`\n }\n } else {\n const entry = buildEntry(item, index, state)\n entries.set(key, entry)\n spacer.appendChild(entry.wrapper)\n }\n }\n }\n\n // Scroll handler — reconcile visible range without touching component state\n const onScroll = (): void => {\n scrollTop = scroll.scrollTop\n reconcile(ctx.state as S)\n }\n scroll.addEventListener('scroll', onScroll, { passive: true })\n\n // Register as a structural block BEFORE initial render so this block\n // precedes any nested blocks its rows register. See branch.ts for the\n // full rationale (Phase 1 iteration safety).\n const block: StructuralBlock = {\n mask: FULL_MASK,\n reconcile(state: unknown) {\n const newItems = callItems(state as S)\n if (newItems === lastItems) return\n reconcile(state as S)\n },\n }\n blocks.push(block)\n\n // Initial render\n reconcile(ctx.state as S)\n\n // Cleanup on parent disposal\n addDisposer(parentLifetime, () => {\n scroll.removeEventListener('scroll', onScroll)\n for (const entry of entries.values()) {\n disposeLifetime(entry.scope)\n }\n entries.clear()\n })\n\n return [scroll]\n}\n"]}
|
package/dist/render-context.d.ts
CHANGED
|
@@ -19,5 +19,8 @@ export interface RenderContext {
|
|
|
19
19
|
}
|
|
20
20
|
export declare function setRenderContext(ctx: RenderContext): void;
|
|
21
21
|
export declare function clearRenderContext(): void;
|
|
22
|
+
export declare function enterAccessor(label: string): void;
|
|
23
|
+
export declare function exitAccessor(): void;
|
|
24
|
+
export declare function currentAccessor(): string | null;
|
|
22
25
|
export declare function getRenderContext(primitiveName?: string): RenderContext;
|
|
23
26
|
//# sourceMappingURL=render-context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-context.d.ts","sourceRoot":"","sources":["../src/render-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAE1C,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,QAAQ,CAAA;IACtB,KAAK,EAAE,OAAO,CAAA;IACd,WAAW,EAAE,OAAO,EAAE,CAAA;IACtB,gBAAgB,EAAE,eAAe,EAAE,CAAA;IACnC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;IAC7B;;;;;;OAMG;IACH,GAAG,EAAE,MAAM,CAAA;CAMZ;AAID,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAEzD;AAED,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED,wBAAgB,gBAAgB,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,aAAa,
|
|
1
|
+
{"version":3,"file":"render-context.d.ts","sourceRoot":"","sources":["../src/render-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAE1C,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,QAAQ,CAAA;IACtB,KAAK,EAAE,OAAO,CAAA;IACd,WAAW,EAAE,OAAO,EAAE,CAAA;IACtB,gBAAgB,EAAE,eAAe,EAAE,CAAA;IACnC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;IAC7B;;;;;;OAMG;IACH,GAAG,EAAE,MAAM,CAAA;CAMZ;AAID,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAEzD;AAED,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAaD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAEjD;AAED,wBAAgB,YAAY,IAAI,IAAI,CAEnC;AAED,wBAAgB,eAAe,IAAI,MAAM,GAAG,IAAI,CAG/C;AAED,wBAAgB,gBAAgB,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,aAAa,CAyCtE"}
|
package/dist/render-context.js
CHANGED
|
@@ -5,6 +5,26 @@ export function setRenderContext(ctx) {
|
|
|
5
5
|
export function clearRenderContext() {
|
|
6
6
|
currentContext = null;
|
|
7
7
|
}
|
|
8
|
+
// Accessor stack — tracks which structural-primitive or binding accessor is
|
|
9
|
+
// currently executing. `sample()` reads the top of this stack to detect calls
|
|
10
|
+
// from inside an accessor (forbidden — accessors must be pure functions of
|
|
11
|
+
// their parameter, since their reads drive mask gating).
|
|
12
|
+
//
|
|
13
|
+
// Implemented as an array (rather than a counter) so the targeted error can
|
|
14
|
+
// name the innermost accessor: "inside each().key" rather than just "inside
|
|
15
|
+
// an accessor". Nested primitives push/pop in LIFO order; the top is the
|
|
16
|
+
// site that called sample().
|
|
17
|
+
const accessorStack = [];
|
|
18
|
+
export function enterAccessor(label) {
|
|
19
|
+
accessorStack.push(label);
|
|
20
|
+
}
|
|
21
|
+
export function exitAccessor() {
|
|
22
|
+
accessorStack.pop();
|
|
23
|
+
}
|
|
24
|
+
export function currentAccessor() {
|
|
25
|
+
const len = accessorStack.length;
|
|
26
|
+
return len > 0 ? accessorStack[len - 1] : null;
|
|
27
|
+
}
|
|
8
28
|
export function getRenderContext(primitiveName) {
|
|
9
29
|
if (!currentContext) {
|
|
10
30
|
const name = primitiveName ? `${primitiveName}()` : 'primitives';
|
|
@@ -33,7 +53,13 @@ export function getRenderContext(primitiveName) {
|
|
|
33
53
|
`from inside the component's view callback so their result can be spread ` +
|
|
34
54
|
`into the returned node tree.\n` +
|
|
35
55
|
` 3. Calling a primitive from a setTimeout / Promise / event handler — ` +
|
|
36
|
-
`the render context only persists during the synchronous view() call
|
|
56
|
+
`the render context only persists during the synchronous view() call.\n` +
|
|
57
|
+
` 4. Calling a primitive from a structural accessor (each().key, ` +
|
|
58
|
+
`each().items, branch().on, show().when, child().props, …) or a ` +
|
|
59
|
+
`binding accessor (text(s => …), el({attr: s => …})) during reconcile — ` +
|
|
60
|
+
`accessors run during the update phase with no render context. They must ` +
|
|
61
|
+
`be pure functions of their parameter; reads outside the parameter break ` +
|
|
62
|
+
`mask gating.` +
|
|
37
63
|
sampleGuidance);
|
|
38
64
|
}
|
|
39
65
|
return currentContext;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-context.js","sourceRoot":"","sources":["../src/render-context.ts"],"names":[],"mappings":"AA2BA,IAAI,cAAc,GAAyB,IAAI,CAAA;AAE/C,MAAM,UAAU,gBAAgB,CAAC,GAAkB;IACjD,cAAc,GAAG,GAAG,CAAA;AACtB,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,cAAc,GAAG,IAAI,CAAA;AACvB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,aAAsB;IACrD,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,MAAM,IAAI,GAAG,aAAa,CAAC,CAAC,CAAC,GAAG,aAAa,IAAI,CAAC,CAAC,CAAC,YAAY,CAAA;QAChE,kEAAkE;QAClE,mEAAmE;QACnE,kEAAkE;QAClE,mEAAmE;QACnE,kEAAkE;QAClE,2DAA2D;QAC3D,MAAM,cAAc,GAClB,aAAa,KAAK,QAAQ;YACxB,CAAC,CAAC,iEAAiE;gBACjE,iEAAiE;gBACjE,qEAAqE;gBACrE,YAAY;gBACZ,wCAAwC;gBACxC,0CAA0C;gBAC1C,uDAAuD;gBACvD,oDAAoD;gBACpD,MAAM;YACR,CAAC,CAAC,EAAE,CAAA;QACR,MAAM,IAAI,KAAK,CACb,UAAU,IAAI,4DAA4D;YACxE,0DAA0D;YAC1D,sEAAsE;YACtE,yEAAyE;YACzE,0EAA0E;YAC1E,0EAA0E;YAC1E,gCAAgC;YAChC,yEAAyE;YACzE,
|
|
1
|
+
{"version":3,"file":"render-context.js","sourceRoot":"","sources":["../src/render-context.ts"],"names":[],"mappings":"AA2BA,IAAI,cAAc,GAAyB,IAAI,CAAA;AAE/C,MAAM,UAAU,gBAAgB,CAAC,GAAkB;IACjD,cAAc,GAAG,GAAG,CAAA;AACtB,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,cAAc,GAAG,IAAI,CAAA;AACvB,CAAC;AAED,4EAA4E;AAC5E,8EAA8E;AAC9E,2EAA2E;AAC3E,yDAAyD;AACzD,EAAE;AACF,4EAA4E;AAC5E,4EAA4E;AAC5E,yEAAyE;AACzE,6BAA6B;AAC7B,MAAM,aAAa,GAAa,EAAE,CAAA;AAElC,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;AAC3B,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,aAAa,CAAC,GAAG,EAAE,CAAA;AACrB,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,MAAM,GAAG,GAAG,aAAa,CAAC,MAAM,CAAA;IAChC,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,GAAG,GAAG,CAAC,CAAE,CAAC,CAAC,CAAC,IAAI,CAAA;AACjD,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,aAAsB;IACrD,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,MAAM,IAAI,GAAG,aAAa,CAAC,CAAC,CAAC,GAAG,aAAa,IAAI,CAAC,CAAC,CAAC,YAAY,CAAA;QAChE,kEAAkE;QAClE,mEAAmE;QACnE,kEAAkE;QAClE,mEAAmE;QACnE,kEAAkE;QAClE,2DAA2D;QAC3D,MAAM,cAAc,GAClB,aAAa,KAAK,QAAQ;YACxB,CAAC,CAAC,iEAAiE;gBACjE,iEAAiE;gBACjE,qEAAqE;gBACrE,YAAY;gBACZ,wCAAwC;gBACxC,0CAA0C;gBAC1C,uDAAuD;gBACvD,oDAAoD;gBACpD,MAAM;YACR,CAAC,CAAC,EAAE,CAAA;QACR,MAAM,IAAI,KAAK,CACb,UAAU,IAAI,4DAA4D;YACxE,0DAA0D;YAC1D,sEAAsE;YACtE,yEAAyE;YACzE,0EAA0E;YAC1E,0EAA0E;YAC1E,gCAAgC;YAChC,yEAAyE;YACzE,wEAAwE;YACxE,mEAAmE;YACnE,iEAAiE;YACjE,yEAAyE;YACzE,0EAA0E;YAC1E,0EAA0E;YAC1E,cAAc;YACd,cAAc,CACjB,CAAA;IACH,CAAC;IACD,OAAO,cAAc,CAAA;AACvB,CAAC","sourcesContent":["import type { Lifetime, Binding } from './types.js'\nimport type { StructuralBlock } from './structural.js'\nimport type { ComponentInstance } from './update-loop.js'\nimport type { DomEnv } from './dom-env.js'\n\nexport interface RenderContext {\n rootLifetime: Lifetime\n state: unknown\n allBindings: Binding[]\n structuralBlocks: StructuralBlock[]\n container?: Element\n send?: (msg: unknown) => void\n /**\n * The DOM implementation backing this render pass. Seeded by\n * `mountApp` / `hydrateApp` / `renderToString` and threaded through\n * every nested primitive via context spread. Primitives construct\n * DOM through `ctx.dom.createElement(...)` etc. instead of reaching\n * for `globalThis.document`.\n */\n dom: DomEnv\n /** @internal dev-only — the owning ComponentInstance. Set by mount /\n * hydrate / child to let primitives (currently `each`) emit tracker\n * data to `inst._eachDiffLog`. Nested contexts pass through via\n * spread (e.g. `{ ...ctx, rootLifetime }`). Undefined outside dev. */\n instance?: ComponentInstance\n}\n\nlet currentContext: RenderContext | null = null\n\nexport function setRenderContext(ctx: RenderContext): void {\n currentContext = ctx\n}\n\nexport function clearRenderContext(): void {\n currentContext = null\n}\n\n// Accessor stack — tracks which structural-primitive or binding accessor is\n// currently executing. `sample()` reads the top of this stack to detect calls\n// from inside an accessor (forbidden — accessors must be pure functions of\n// their parameter, since their reads drive mask gating).\n//\n// Implemented as an array (rather than a counter) so the targeted error can\n// name the innermost accessor: \"inside each().key\" rather than just \"inside\n// an accessor\". Nested primitives push/pop in LIFO order; the top is the\n// site that called sample().\nconst accessorStack: string[] = []\n\nexport function enterAccessor(label: string): void {\n accessorStack.push(label)\n}\n\nexport function exitAccessor(): void {\n accessorStack.pop()\n}\n\nexport function currentAccessor(): string | null {\n const len = accessorStack.length\n return len > 0 ? accessorStack[len - 1]! : null\n}\n\nexport function getRenderContext(primitiveName?: string): RenderContext {\n if (!currentContext) {\n const name = primitiveName ? `${primitiveName}()` : 'primitives'\n // `sample()` is specifically the one users reach for from adapter\n // send wrappers / event handlers / async callbacks expecting it to\n // be \"imperative and safe.\" It isn't — it's a view-primitive that\n // reads the render-time state snapshot, and the context is cleared\n // as soon as view() returns. Point at the sanctioned escape hatch\n // in the thrown message so the caller doesn't have to dig.\n const sampleGuidance =\n primitiveName === 'sample'\n ? '\\n\\nFor the \"read state inside a callback / handler\" case: use ' +\n 'AppHandle.getState() instead. It is safe to call from anywhere ' +\n '(event handlers, adapter send wrappers, async callbacks, timers).\\n' +\n 'Example:\\n' +\n ' const handle = mountApp(root, App)\\n' +\n \" el.addEventListener('click', () => {\\n\" +\n ' const { count } = handle.getState() as AppState\\n' +\n \" if (count > 0) handle.send({ type: 'tick' })\\n\" +\n ' })'\n : ''\n throw new Error(\n `[LLui] ${name} can only be called inside a component's view() function. ` +\n `It was called outside a render context. Common causes:\\n` +\n ` 1. Calling a primitive at module scope instead of inside view().\\n` +\n ` 2. Calling an overlay helper (dialog.overlay, popover.overlay, …) at ` +\n `module scope — these internally use show()/branch() and must be invoked ` +\n `from inside the component's view callback so their result can be spread ` +\n `into the returned node tree.\\n` +\n ` 3. Calling a primitive from a setTimeout / Promise / event handler — ` +\n `the render context only persists during the synchronous view() call.\\n` +\n ` 4. Calling a primitive from a structural accessor (each().key, ` +\n `each().items, branch().on, show().when, child().props, …) or a ` +\n `binding accessor (text(s => …), el({attr: s => …})) during reconcile — ` +\n `accessors run during the update phase with no render context. They must ` +\n `be pure functions of their parameter; reads outside the parameter break ` +\n `mask gating.` +\n sampleGuidance,\n )\n }\n return currentContext\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update-loop.d.ts","sourceRoot":"","sources":["../src/update-loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACjE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAItD,OAAO,EAAE,KAAK,MAAM,EAAc,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"update-loop.d.ts","sourceRoot":"","sources":["../src/update-loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACjE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAItD,OAAO,EAAE,KAAK,MAAM,EAAc,MAAM,cAAc,CAAA;AAoBtD,eAAO,MAAM,SAAS,QAAiB,CAAA;AAMvC,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,CAAC,GAAG,EAAE;IAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,KAAK,IAAI,GAClE,IAAI,CAEN;AAED,MAAM,WAAW,iBAAiB,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO;IACtE,GAAG,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;IAC1B,KAAK,EAAE,CAAC,CAAA;IACR,cAAc,EAAE,CAAC,EAAE,CAAA;IACnB,YAAY,EAAE,QAAQ,CAAA;IACtB,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,EAAE,OAAO,EAAE,CAAA;IACtB,gBAAgB,EAAE,eAAe,EAAE,CAAA;IACnC,KAAK,EAAE,CAAC,EAAE,CAAA;IACV,kBAAkB,EAAE,OAAO,CAAA;IAC3B,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,CAAC,EAAE,CAAA;IAChB,IAAI,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,CAAA;IACtB,MAAM,EAAE,WAAW,CAAA;IACnB,eAAe,EAAE,eAAe,CAAA;CAkEjC;AAED,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,IAAI,EACvD,GAAG,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC7B,IAAI,CAAC,EAAE,CAAC,EACR,cAAc,GAAE,QAAQ,GAAG,IAAW,EACtC,GAAG,CAAC,EAAE,MAAM,GACX,iBAAiB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAgD5B;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAI7E;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,CAqExF;AA0LD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,iBAAiB,EACvB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC,CA2DtB;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,OAAO,EAAE,EACnB,oBAAoB,EAAE,MAAM,EAC5B,aAAa,CAAC,EAAE,MAAM,EAMtB,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,KAAK,IAAI,GAC/F,IAAI,CA6DN"}
|