@gyral/core 0.1.0 → 0.3.0
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/CHANGELOG.md +112 -0
- package/LICENSE +1 -1
- package/NOTICE +1 -1
- package/README.md +9 -7
- package/dist/children.d.ts +5 -2
- package/dist/children.js +8 -3
- package/dist/children.js.map +1 -1
- package/dist/command.js +6 -0
- package/dist/command.js.map +1 -1
- package/dist/compiler/ast.d.ts +21 -0
- package/dist/compiler/ast.js +145 -0
- package/dist/compiler/ast.js.map +1 -0
- package/dist/compiler/compile.d.ts +38 -0
- package/dist/compiler/compile.js +129 -0
- package/dist/compiler/compile.js.map +1 -0
- package/dist/compiler/hooks.d.ts +18 -0
- package/dist/compiler/hooks.js +186 -0
- package/dist/compiler/hooks.js.map +1 -0
- package/dist/compiler/parse5-check.d.ts +21 -0
- package/dist/compiler/parse5-check.js +55 -0
- package/dist/compiler/parse5-check.js.map +1 -0
- package/dist/compiler/scan.d.ts +31 -0
- package/dist/compiler/scan.js +152 -0
- package/dist/compiler/scan.js.map +1 -0
- package/dist/compiler/source.d.ts +34 -0
- package/dist/compiler/source.js +114 -0
- package/dist/compiler/source.js.map +1 -0
- package/dist/define.d.ts +2 -2
- package/dist/define.js +35 -334
- package/dist/define.js.map +1 -1
- package/dist/devtools-events.d.ts +5 -0
- package/dist/devtools-events.js.map +1 -1
- package/dist/devtools-off.d.ts +1 -0
- package/dist/devtools-off.js +1 -0
- package/dist/devtools-off.js.map +1 -1
- package/dist/devtools.d.ts +1 -0
- package/dist/devtools.js +3 -0
- package/dist/devtools.js.map +1 -1
- package/dist/drivers-scope.d.ts +1 -1
- package/dist/drivers-scope.js +1 -8
- package/dist/drivers-scope.js.map +1 -1
- package/dist/element-types.d.ts +5 -3
- package/dist/element-types.js.map +1 -1
- package/dist/element.d.ts +3 -0
- package/dist/element.js +269 -0
- package/dist/element.js.map +1 -0
- package/dist/eslint/imports.d.ts +24 -0
- package/dist/eslint/imports.js +78 -0
- package/dist/eslint/imports.js.map +1 -0
- package/dist/eslint/index.d.ts +15 -0
- package/dist/eslint/index.js +15 -0
- package/dist/eslint/index.js.map +1 -0
- package/dist/eslint/locate.d.ts +33 -0
- package/dist/eslint/locate.js +132 -0
- package/dist/eslint/locate.js.map +1 -0
- package/dist/eslint/row-purity-rule.d.ts +2 -0
- package/dist/eslint/row-purity-rule.js +103 -0
- package/dist/eslint/row-purity-rule.js.map +1 -0
- package/dist/eslint/template-rule.d.ts +5 -0
- package/dist/eslint/template-rule.js +66 -0
- package/dist/eslint/template-rule.js.map +1 -0
- package/dist/features.d.ts +39 -0
- package/dist/features.js +2 -0
- package/dist/features.js.map +1 -0
- package/dist/focus.d.ts +9 -2
- package/dist/focus.js +17 -2
- package/dist/focus.js.map +1 -1
- package/dist/hooks/invalid.d.ts +14 -0
- package/dist/hooks/invalid.js +72 -0
- package/dist/hooks/invalid.js.map +1 -0
- package/dist/hooks/labelled-by.d.ts +13 -0
- package/dist/hooks/labelled-by.js +51 -0
- package/dist/hooks/labelled-by.js.map +1 -0
- package/dist/host-model.d.ts +48 -0
- package/dist/host-model.js +155 -0
- package/dist/host-model.js.map +1 -0
- package/dist/hydration-client.d.ts +9 -0
- package/dist/hydration-client.js +35 -0
- package/dist/hydration-client.js.map +1 -0
- package/dist/hydration.d.ts +13 -19
- package/dist/hydration.js +45 -45
- package/dist/hydration.js.map +1 -1
- package/dist/index.d.ts +9 -16
- package/dist/index.js +9 -16
- package/dist/index.js.map +1 -1
- package/dist/intent.d.ts +31 -8
- package/dist/intent.js +82 -27
- package/dist/intent.js.map +1 -1
- package/dist/internal/interpreter.d.ts +7 -1
- package/dist/internal/interpreter.js +118 -73
- package/dist/internal/interpreter.js.map +1 -1
- package/dist/invokers-shim.d.ts +6 -0
- package/dist/invokers-shim.js +32 -0
- package/dist/invokers-shim.js.map +1 -0
- package/dist/invokers.d.ts +3 -5
- package/dist/invokers.js +11 -51
- package/dist/invokers.js.map +1 -1
- package/dist/islands.d.ts +0 -2
- package/dist/islands.js +4 -11
- package/dist/islands.js.map +1 -1
- package/dist/light-dom.d.ts +3 -52
- package/dist/light-dom.js +5 -100
- package/dist/light-dom.js.map +1 -1
- package/dist/prop.d.ts +66 -0
- package/dist/prop.js +22 -0
- package/dist/prop.js.map +1 -0
- package/dist/props.d.ts +52 -21
- package/dist/props.js +150 -34
- package/dist/props.js.map +1 -1
- package/dist/scheduler.d.ts +39 -0
- package/dist/scheduler.js +230 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/server-component.d.ts +10 -0
- package/dist/server-component.js +102 -0
- package/dist/server-component.js.map +1 -0
- package/dist/server-specs.d.ts +6 -0
- package/dist/server-specs.js +13 -0
- package/dist/server-specs.js.map +1 -0
- package/dist/server.d.ts +28 -0
- package/dist/server.js +50 -0
- package/dist/server.js.map +1 -0
- package/dist/settled.d.ts +10 -0
- package/dist/settled.js +40 -0
- package/dist/settled.js.map +1 -0
- package/dist/states.d.ts +3 -3
- package/dist/states.js +4 -7
- package/dist/states.js.map +1 -1
- package/dist/store-binding.d.ts +6 -3
- package/dist/store-binding.js +5 -6
- package/dist/store-binding.js.map +1 -1
- package/dist/store-scope.d.ts +6 -7
- package/dist/store-scope.js +11 -26
- package/dist/store-scope.js.map +1 -1
- package/dist/store.d.ts +3 -0
- package/dist/store.js +9 -2
- package/dist/store.js.map +1 -1
- package/dist/stores-provider.d.ts +2 -6
- package/dist/stores-provider.js +21 -55
- package/dist/stores-provider.js.map +1 -1
- package/dist/transitions.d.ts +5 -6
- package/dist/transitions.js +11 -20
- package/dist/transitions.js.map +1 -1
- package/dist/types.d.ts +19 -23
- package/dist/types.js.map +1 -1
- package/dist/view/attributes.d.ts +6 -0
- package/dist/view/attributes.js +10 -0
- package/dist/view/attributes.js.map +1 -0
- package/dist/view/compiled.d.ts +1 -0
- package/dist/view/compiled.js +6 -0
- package/dist/view/compiled.js.map +1 -0
- package/dist/view/dev-off.d.ts +1 -0
- package/dist/view/dev-off.js +4 -0
- package/dist/view/dev-off.js.map +1 -0
- package/dist/view/dev.d.ts +1 -0
- package/dist/view/dev.js +5 -0
- package/dist/view/dev.js.map +1 -0
- package/dist/view/flags.d.ts +1 -0
- package/dist/view/flags.js +4 -0
- package/dist/view/flags.js.map +1 -0
- package/dist/view/index.d.ts +21 -0
- package/dist/view/index.js +21 -0
- package/dist/view/index.js.map +1 -0
- package/dist/view/normalize/check.d.ts +20 -0
- package/dist/view/normalize/check.js +78 -0
- package/dist/view/normalize/check.js.map +1 -0
- package/dist/view/normalize/emit.d.ts +13 -0
- package/dist/view/normalize/emit.js +136 -0
- package/dist/view/normalize/emit.js.map +1 -0
- package/dist/view/normalize/entities.d.ts +4 -0
- package/dist/view/normalize/entities.js +38 -0
- package/dist/view/normalize/entities.js.map +1 -0
- package/dist/view/normalize/errors.d.ts +9 -0
- package/dist/view/normalize/errors.js +19 -0
- package/dist/view/normalize/errors.js.map +1 -0
- package/dist/view/normalize/id.d.ts +7 -0
- package/dist/view/normalize/id.js +48 -0
- package/dist/view/normalize/id.js.map +1 -0
- package/dist/view/normalize/normalize.d.ts +15 -0
- package/dist/view/normalize/normalize.js +36 -0
- package/dist/view/normalize/normalize.js.map +1 -0
- package/dist/view/normalize/repairs.d.ts +24 -0
- package/dist/view/normalize/repairs.js +166 -0
- package/dist/view/normalize/repairs.js.map +1 -0
- package/dist/view/normalize/rules.d.ts +30 -0
- package/dist/view/normalize/rules.js +90 -0
- package/dist/view/normalize/rules.js.map +1 -0
- package/dist/view/normalize/shape.d.ts +5 -0
- package/dist/view/normalize/shape.js +38 -0
- package/dist/view/normalize/shape.js.map +1 -0
- package/dist/view/normalize/source.d.ts +13 -0
- package/dist/view/normalize/source.js +34 -0
- package/dist/view/normalize/source.js.map +1 -0
- package/dist/view/normalize/tag-states.d.ts +23 -0
- package/dist/view/normalize/tag-states.js +192 -0
- package/dist/view/normalize/tag-states.js.map +1 -0
- package/dist/view/normalize/tokenizer.d.ts +5 -0
- package/dist/view/normalize/tokenizer.js +233 -0
- package/dist/view/normalize/tokenizer.js.map +1 -0
- package/dist/view/normalize/tokens.d.ts +51 -0
- package/dist/view/normalize/tokens.js +24 -0
- package/dist/view/normalize/tokens.js.map +1 -0
- package/dist/view/normalize/tree.d.ts +59 -0
- package/dist/view/normalize/tree.js +190 -0
- package/dist/view/normalize/tree.js.map +1 -0
- package/dist/view/normalize/types.d.ts +126 -0
- package/dist/view/normalize/types.js +26 -0
- package/dist/view/normalize/types.js.map +1 -0
- package/dist/view/normalize/whitespace.d.ts +2 -0
- package/dist/view/normalize/whitespace.js +182 -0
- package/dist/view/normalize/whitespace.js.map +1 -0
- package/dist/view/prepare-stub.d.ts +3 -0
- package/dist/view/prepare-stub.js +7 -0
- package/dist/view/prepare-stub.js.map +1 -0
- package/dist/view/prepare.d.ts +8 -0
- package/dist/view/prepare.js +57 -0
- package/dist/view/prepare.js.map +1 -0
- package/dist/view/registry.d.ts +63 -0
- package/dist/view/registry.js +19 -0
- package/dist/view/registry.js.map +1 -0
- package/dist/view/render/adopt-attr.d.ts +14 -0
- package/dist/view/render/adopt-attr.js +77 -0
- package/dist/view/render/adopt-attr.js.map +1 -0
- package/dist/view/render/adopt-plan.d.ts +10 -0
- package/dist/view/render/adopt-plan.js +26 -0
- package/dist/view/render/adopt-plan.js.map +1 -0
- package/dist/view/render/adopt.d.ts +3 -0
- package/dist/view/render/adopt.js +269 -0
- package/dist/view/render/adopt.js.map +1 -0
- package/dist/view/render/attr-parts.d.ts +61 -0
- package/dist/view/render/attr-parts.js +229 -0
- package/dist/view/render/attr-parts.js.map +1 -0
- package/dist/view/render/child-part.d.ts +51 -0
- package/dist/view/render/child-part.js +170 -0
- package/dist/view/render/child-part.js.map +1 -0
- package/dist/view/render/dev-check.d.ts +12 -0
- package/dist/view/render/dev-check.js +122 -0
- package/dist/view/render/dev-check.js.map +1 -0
- package/dist/view/render/hook-part.d.ts +3 -0
- package/dist/view/render/hook-part.js +20 -0
- package/dist/view/render/hook-part.js.map +1 -0
- package/dist/view/render/hooks.d.ts +40 -0
- package/dist/view/render/hooks.js +46 -0
- package/dist/view/render/hooks.js.map +1 -0
- package/dist/view/render/hydrate.d.ts +10 -0
- package/dist/view/render/hydrate.js +33 -0
- package/dist/view/render/hydrate.js.map +1 -0
- package/dist/view/render/instance.d.ts +33 -0
- package/dist/view/render/instance.js +107 -0
- package/dist/view/render/instance.js.map +1 -0
- package/dist/view/render/list.d.ts +20 -0
- package/dist/view/render/list.js +156 -0
- package/dist/view/render/list.js.map +1 -0
- package/dist/view/render/mismatch.d.ts +10 -0
- package/dist/view/render/mismatch.js +53 -0
- package/dist/view/render/mismatch.js.map +1 -0
- package/dist/view/render/nodes.d.ts +10 -0
- package/dist/view/render/nodes.js +37 -0
- package/dist/view/render/nodes.js.map +1 -0
- package/dist/view/render/place.d.ts +18 -0
- package/dist/view/render/place.js +69 -0
- package/dist/view/render/place.js.map +1 -0
- package/dist/view/render/plan.d.ts +26 -0
- package/dist/view/render/plan.js +124 -0
- package/dist/view/render/plan.js.map +1 -0
- package/dist/view/render/raw.d.ts +18 -0
- package/dist/view/render/raw.js +68 -0
- package/dist/view/render/raw.js.map +1 -0
- package/dist/view/render/render.d.ts +12 -0
- package/dist/view/render/render.js +60 -0
- package/dist/view/render/render.js.map +1 -0
- package/dist/view/render/reorder.d.ts +4 -0
- package/dist/view/render/reorder.js +101 -0
- package/dist/view/render/reorder.js.map +1 -0
- package/dist/view/render/rows.d.ts +23 -0
- package/dist/view/render/rows.js +95 -0
- package/dist/view/render/rows.js.map +1 -0
- package/dist/view/render/seen.d.ts +11 -0
- package/dist/view/render/seen.js +17 -0
- package/dist/view/render/seen.js.map +1 -0
- package/dist/view/render/values.d.ts +29 -0
- package/dist/view/render/values.js +13 -0
- package/dist/view/render/values.js.map +1 -0
- package/dist/view/render/warn.d.ts +14 -0
- package/dist/view/render/warn.js +69 -0
- package/dist/view/render/warn.js.map +1 -0
- package/dist/view/server/component.d.ts +9 -0
- package/dist/view/server/component.js +42 -0
- package/dist/view/server/component.js.map +1 -0
- package/dist/view/server/escape.d.ts +10 -0
- package/dist/view/server/escape.js +34 -0
- package/dist/view/server/escape.js.map +1 -0
- package/dist/view/server/index.d.ts +28 -0
- package/dist/view/server/index.js +72 -0
- package/dist/view/server/index.js.map +1 -0
- package/dist/view/server/sha256.d.ts +2 -0
- package/dist/view/server/sha256.js +75 -0
- package/dist/view/server/sha256.js.map +1 -0
- package/dist/view/server/tags.d.ts +57 -0
- package/dist/view/server/tags.js +121 -0
- package/dist/view/server/tags.js.map +1 -0
- package/dist/view/server/values.d.ts +16 -0
- package/dist/view/server/values.js +86 -0
- package/dist/view/server/values.js.map +1 -0
- package/dist/view/server/writer.d.ts +16 -0
- package/dist/view/server/writer.js +155 -0
- package/dist/view/server/writer.js.map +1 -0
- package/dist/view/styles.d.ts +25 -0
- package/dist/view/styles.js +58 -0
- package/dist/view/styles.js.map +1 -0
- package/dist/view/template-element.d.ts +3 -0
- package/dist/view/template-element.js +25 -0
- package/dist/view/template-element.js.map +1 -0
- package/dist/view/template.d.ts +21 -0
- package/dist/view/template.js +30 -0
- package/dist/view/template.js.map +1 -0
- package/dist/vite.d.ts +45 -6
- package/dist/vite.js +150 -25
- package/dist/vite.js.map +1 -1
- package/package.json +41 -7
- package/dist/accessible-name.d.ts +0 -19
- package/dist/accessible-name.js +0 -56
- package/dist/accessible-name.js.map +0 -1
- package/dist/element-directive.d.ts +0 -20
- package/dist/element-directive.js +0 -31
- package/dist/element-directive.js.map +0 -1
- package/dist/internal/runtime.d.ts +0 -7
- package/dist/internal/runtime.js +0 -8
- package/dist/internal/runtime.js.map +0 -1
- package/dist/invalid.d.ts +0 -21
- package/dist/invalid.js +0 -70
- package/dist/invalid.js.map +0 -1
- package/dist/live-boolean.d.ts +0 -22
- package/dist/live-boolean.js +0 -34
- package/dist/live-boolean.js.map +0 -1
- package/dist/styles.d.ts +0 -12
- package/dist/styles.js +0 -11
- package/dist/styles.js.map +0 -1
- package/dist/textarea.d.ts +0 -26
- package/dist/textarea.js +0 -85
- package/dist/textarea.js.map +0 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** The element with `id`, searched from `from`'s own root outward to the document. */
|
|
2
|
+
export declare function findInScope(from: Element, id: string): Element | null;
|
|
3
|
+
/**
|
|
4
|
+
* Names the element after the element with `id`, even outside this component's shadow root:
|
|
5
|
+
*
|
|
6
|
+
* <form ${labelledBy('page-title', 'Sign in')}>
|
|
7
|
+
*
|
|
8
|
+
* Uses `ariaLabelledByElements` (ARIA element reflection) where the browser has it, so the name
|
|
9
|
+
* stays in sync with the heading. Elsewhere it sets `aria-label` from `fallback`, or from the
|
|
10
|
+
* target's text. It has no server half (as before): render a plain `aria-label` too (from a
|
|
11
|
+
* `label` prop) for the page before scripts run; the reflected `aria-labelledby` wins over it.
|
|
12
|
+
*/
|
|
13
|
+
export declare const labelledBy: (id: string, fallback?: string | undefined) => import("../index.js").HookResult<[id: string, fallback?: string]>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// `labelledBy(id, fallback?)` as an element hook (view/02-bindings.md "Element hooks",
|
|
2
|
+
// gyral-czi.26): accessible names across shadow boundaries. `aria-labelledby="page-title"`
|
|
3
|
+
// inside a shadow root can't see an id in the page, so the id is resolved outward through the
|
|
4
|
+
// shadow-including ancestors.
|
|
5
|
+
import { defineHook } from '../view/index.js';
|
|
6
|
+
/** ARIA element reflection: newly available Baseline, so feature-detected (ADR 0003). */
|
|
7
|
+
const reflects = (el) => 'ariaLabelledByElements' in el;
|
|
8
|
+
/** The element with `id`, searched from `from`'s own root outward to the document. */
|
|
9
|
+
export function findInScope(from, id) {
|
|
10
|
+
for (let root = from.getRootNode();;) {
|
|
11
|
+
const found = 'getElementById' in root ? root.getElementById(id) : null;
|
|
12
|
+
if (found !== null && found !== from)
|
|
13
|
+
return found;
|
|
14
|
+
if (!(root instanceof ShadowRoot))
|
|
15
|
+
return null;
|
|
16
|
+
root = root.host.getRootNode();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Names `el`; false when the target wasn't found (it may render later). */
|
|
20
|
+
function name(el, id, fallback) {
|
|
21
|
+
const target = findInScope(el, id);
|
|
22
|
+
if (target !== null && reflects(el)) {
|
|
23
|
+
el.ariaLabelledByElements = [target];
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
const text = fallback ?? target?.textContent.trim() ?? '';
|
|
27
|
+
if (text !== '')
|
|
28
|
+
el.setAttribute('aria-label', text);
|
|
29
|
+
return target !== null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Names the element after the element with `id`, even outside this component's shadow root:
|
|
33
|
+
*
|
|
34
|
+
* <form ${labelledBy('page-title', 'Sign in')}>
|
|
35
|
+
*
|
|
36
|
+
* Uses `ariaLabelledByElements` (ARIA element reflection) where the browser has it, so the name
|
|
37
|
+
* stays in sync with the heading. Elsewhere it sets `aria-label` from `fallback`, or from the
|
|
38
|
+
* target's text. It has no server half (as before): render a plain `aria-label` too (from a
|
|
39
|
+
* `label` prop) for the page before scripts run; the reflected `aria-labelledby` wins over it.
|
|
40
|
+
*/
|
|
41
|
+
export const labelledBy = defineHook({
|
|
42
|
+
client: (el, [id, fallback]) => {
|
|
43
|
+
// The labelling element may render after this one (a later sibling): try again once.
|
|
44
|
+
if (!name(el, id, fallback)) {
|
|
45
|
+
queueMicrotask(() => {
|
|
46
|
+
name(el, id, fallback);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
//# sourceMappingURL=labelled-by.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"labelled-by.js","sourceRoot":"","sources":["../../src/hooks/labelled-by.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,2FAA2F;AAC3F,8FAA8F;AAC9F,8BAA8B;AAC9B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAM9C,yFAAyF;AACzF,MAAM,QAAQ,GAAG,CAAC,EAAW,EAAoB,EAAE,CAAC,wBAAwB,IAAI,EAAE,CAAC;AAInF,sFAAsF;AACtF,MAAM,UAAU,WAAW,CAAC,IAAa,EAAE,EAAU;IACnD,KAAK,IAAI,IAAI,GAAS,IAAI,CAAC,WAAW,EAAE,IAAK,CAAC;QAC5C,MAAM,KAAK,GAAG,gBAAgB,IAAI,IAAI,CAAC,CAAC,CAAE,IAAyB,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC9F,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QACnD,IAAI,CAAC,CAAC,IAAI,YAAY,UAAU,CAAC;YAAE,OAAO,IAAI,CAAC;QAC/C,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;IACjC,CAAC;AACH,CAAC;AAED,4EAA4E;AAC5E,SAAS,IAAI,CAAC,EAAW,EAAE,EAAU,EAAE,QAA4B;IACjE,MAAM,MAAM,GAAG,WAAW,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;IACnC,IAAI,MAAM,KAAK,IAAI,IAAI,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;QACpC,EAAE,CAAC,sBAAsB,GAAG,CAAC,MAAM,CAAC,CAAC;QACrC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,IAAI,GAAG,QAAQ,IAAI,MAAM,EAAE,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;IAC1D,IAAI,IAAI,KAAK,EAAE;QAAE,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;IACrD,OAAO,MAAM,KAAK,IAAI,CAAC;AACzB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,UAAU,CAAkC;IACpE,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE;QAC7B,qFAAqF;QACrF,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,CAAC;YAC5B,cAAc,CAAC,GAAG,EAAE;gBAClB,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC;YACzB,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;CACF,CAAC,CAAC","sourcesContent":["// `labelledBy(id, fallback?)` as an element hook (view/02-bindings.md \"Element hooks\",\n// gyral-czi.26): accessible names across shadow boundaries. `aria-labelledby=\"page-title\"`\n// inside a shadow root can't see an id in the page, so the id is resolved outward through the\n// shadow-including ancestors.\nimport { defineHook } from '../view/index.js';\n\ninterface Reflecting extends Element {\n ariaLabelledByElements: readonly Element[] | null;\n}\n\n/** ARIA element reflection: newly available Baseline, so feature-detected (ADR 0003). */\nconst reflects = (el: Element): el is Reflecting => 'ariaLabelledByElements' in el;\n\ntype DocumentOrShadow = Document | ShadowRoot;\n\n/** The element with `id`, searched from `from`'s own root outward to the document. */\nexport function findInScope(from: Element, id: string): Element | null {\n for (let root: Node = from.getRootNode(); ;) {\n const found = 'getElementById' in root ? (root as DocumentOrShadow).getElementById(id) : null;\n if (found !== null && found !== from) return found;\n if (!(root instanceof ShadowRoot)) return null;\n root = root.host.getRootNode();\n }\n}\n\n/** Names `el`; false when the target wasn't found (it may render later). */\nfunction name(el: Element, id: string, fallback: string | undefined): boolean {\n const target = findInScope(el, id);\n if (target !== null && reflects(el)) {\n el.ariaLabelledByElements = [target];\n return true;\n }\n const text = fallback ?? target?.textContent.trim() ?? '';\n if (text !== '') el.setAttribute('aria-label', text);\n return target !== null;\n}\n\n/**\n * Names the element after the element with `id`, even outside this component's shadow root:\n *\n * <form ${labelledBy('page-title', 'Sign in')}>\n *\n * Uses `ariaLabelledByElements` (ARIA element reflection) where the browser has it, so the name\n * stays in sync with the heading. Elsewhere it sets `aria-label` from `fallback`, or from the\n * target's text. It has no server half (as before): render a plain `aria-label` too (from a\n * `label` prop) for the page before scripts run; the reflected `aria-labelledby` wins over it.\n */\nexport const labelledBy = defineHook<[id: string, fallback?: string]>({\n client: (el, [id, fallback]) => {\n // The labelling element may render after this one (a later sibling): try again once.\n if (!name(el, id, fallback)) {\n queueMicrotask(() => {\n name(el, id, fallback);\n });\n }\n },\n});\n"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type Command, type Next } from './command.js';
|
|
2
|
+
import { type FeatureHost, type LocalHost, type StoreLink } from './features.js';
|
|
3
|
+
import type { Seed } from './hydration.js';
|
|
4
|
+
import type { ComponentSpec, Ctx, IntentRejected, Tagged } from './types.js';
|
|
5
|
+
type Bag = Readonly<Record<string, unknown>>;
|
|
6
|
+
/** A command as the model runs it. */
|
|
7
|
+
export type ModelCommand = Command<Tagged | IntentRejected>;
|
|
8
|
+
type Cmd = ModelCommand;
|
|
9
|
+
/** What the model needs from its element. */
|
|
10
|
+
export interface ModelHost {
|
|
11
|
+
readonly el: FeatureHost;
|
|
12
|
+
readonly tag: string;
|
|
13
|
+
/** The declared props as components see them (defaults applied). */
|
|
14
|
+
readonly props: () => Bag;
|
|
15
|
+
/** The host's root once it has one (focus commands run against it). */
|
|
16
|
+
readonly root: () => ParentNode | undefined;
|
|
17
|
+
/** Something changed that the next render must show; `onFrame`: in the frame lane (04). */
|
|
18
|
+
readonly invalidate: (onFrame: boolean) => void;
|
|
19
|
+
}
|
|
20
|
+
export declare class HostModel<S, P> implements LocalHost {
|
|
21
|
+
#private;
|
|
22
|
+
readonly el: FeatureHost;
|
|
23
|
+
readonly tag: string;
|
|
24
|
+
readonly root: () => ParentNode | undefined;
|
|
25
|
+
constructor(host: ModelHost, spec: ComponentSpec<S, Tagged, P>);
|
|
26
|
+
/** The store binding, created on first use. Only reached once `defineStore()` has run. */
|
|
27
|
+
readonly stores: () => StoreLink;
|
|
28
|
+
get ready(): boolean;
|
|
29
|
+
/** The current state, running `init` (and `initialMessages`) on first read. */
|
|
30
|
+
state(initialMessages?: readonly Tagged[]): S;
|
|
31
|
+
hasReducer(tag: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Resumes from a server seed (ADR 0012): the server's state, or `init(props)`'s when the
|
|
34
|
+
* seed left it out. Returns init's commands, to start after the first render.
|
|
35
|
+
*/
|
|
36
|
+
resume(seed: Seed): readonly Cmd[];
|
|
37
|
+
/** PropsChanged when props differ from the last ones seen (`Object.is` per prop). */
|
|
38
|
+
syncProps(names: readonly string[]): void;
|
|
39
|
+
dispatch(msg: Tagged, schedule?: boolean): void;
|
|
40
|
+
apply(next: Next<S, Tagged> | Next<S, Tagged | IntentRejected>): void;
|
|
41
|
+
/** Starts commands kept for later (init's, after a server-rendered host's first render). */
|
|
42
|
+
run(commands: readonly Cmd[]): void;
|
|
43
|
+
ctx(): Ctx<P>;
|
|
44
|
+
/** Resolves stores and runs kept commands. Returns true if store instances were bound. */
|
|
45
|
+
connect(): boolean;
|
|
46
|
+
disconnect(): void;
|
|
47
|
+
}
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// The model side of one Gyral host (docs/design-docs/view/05-element.md, ADR 0006, ADR 0013):
|
|
2
|
+
// state, reducers and commands. element.ts owns the DOM side (props, root, lifecycle,
|
|
3
|
+
// rendering) and asks this object for state and context. Reducers run at once, so `state` is
|
|
4
|
+
// always current; only the render waits (04). The interpreter and the store binding come from
|
|
5
|
+
// features.ts, registered by `command()` and `defineStore()`; marker drivers (focus, emit, store
|
|
6
|
+
// sends) carry their own `local` handler.
|
|
7
|
+
import { DEVTOOLS_ENABLED, devCommands, devOwner, devUpdate } from '#devtools';
|
|
8
|
+
import { splitNext } from './command.js';
|
|
9
|
+
import { features, } from './features.js';
|
|
10
|
+
import { runInit } from './init.js';
|
|
11
|
+
import { requestTransition } from './scheduler.js';
|
|
12
|
+
export class HostModel {
|
|
13
|
+
#host;
|
|
14
|
+
#spec;
|
|
15
|
+
#reducers;
|
|
16
|
+
#binding;
|
|
17
|
+
#model;
|
|
18
|
+
/** Props as of init or the last PropsChanged: the `prev` of the next PropsChanged. */
|
|
19
|
+
#seen;
|
|
20
|
+
#interpreter;
|
|
21
|
+
/** Commands kept while disconnected (init's, before the first connect); undefined once connected. */
|
|
22
|
+
#pending;
|
|
23
|
+
constructor(host, spec) {
|
|
24
|
+
/** Commands kept while disconnected (init's, before the first connect); undefined once connected. */
|
|
25
|
+
this.#pending = [];
|
|
26
|
+
/** The store binding, created on first use. Only reached once `defineStore()` has run. */
|
|
27
|
+
this.stores = () => (this.#binding ??= features.stores(this.#host.el, this.#host.tag, this.#spec.stores ?? [], (store, state, prev) => {
|
|
28
|
+
this.#storeChanged(store, state, prev);
|
|
29
|
+
}));
|
|
30
|
+
this.#host = host;
|
|
31
|
+
this.el = host.el;
|
|
32
|
+
this.tag = host.tag;
|
|
33
|
+
this.root = host.root;
|
|
34
|
+
this.#spec = spec;
|
|
35
|
+
// Sound: dispatch() only calls the reducer whose key equals msg._tag.
|
|
36
|
+
this.#reducers = spec.update;
|
|
37
|
+
}
|
|
38
|
+
get ready() {
|
|
39
|
+
return this.#model !== undefined;
|
|
40
|
+
}
|
|
41
|
+
/** The current state, running `init` (and `initialMessages`) on first read. */
|
|
42
|
+
state(initialMessages = []) {
|
|
43
|
+
if (this.#model === undefined) {
|
|
44
|
+
this.#seen = this.#host.props();
|
|
45
|
+
this.apply(runInit(this.#spec, this.#seen));
|
|
46
|
+
for (const msg of initialMessages)
|
|
47
|
+
this.dispatch(msg, false);
|
|
48
|
+
}
|
|
49
|
+
return this.#model.value;
|
|
50
|
+
}
|
|
51
|
+
hasReducer(tag) {
|
|
52
|
+
return this.#reducers[tag] !== undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resumes from a server seed (ADR 0012): the server's state, or `init(props)`'s when the
|
|
56
|
+
* seed left it out. Returns init's commands, to start after the first render.
|
|
57
|
+
*/
|
|
58
|
+
resume(seed) {
|
|
59
|
+
this.#seen = this.#host.props();
|
|
60
|
+
const [initial, commands] = splitNext(runInit(this.#spec, this.#seen));
|
|
61
|
+
// Sound: the seed is this component's own state, written by the server.
|
|
62
|
+
this.#model = { value: 'state' in seed ? seed.state : initial };
|
|
63
|
+
return commands;
|
|
64
|
+
}
|
|
65
|
+
/** PropsChanged when props differ from the last ones seen (`Object.is` per prop). */
|
|
66
|
+
syncProps(names) {
|
|
67
|
+
const prev = this.#seen;
|
|
68
|
+
if (prev === undefined)
|
|
69
|
+
return;
|
|
70
|
+
const props = this.#host.props();
|
|
71
|
+
if (names.every((name) => Object.is(props[name], prev[name])))
|
|
72
|
+
return;
|
|
73
|
+
this.#seen = props;
|
|
74
|
+
this.dispatch({ _tag: 'PropsChanged', props, prev }, false);
|
|
75
|
+
}
|
|
76
|
+
dispatch(msg, schedule = true) {
|
|
77
|
+
const reducer = this.#reducers[msg._tag];
|
|
78
|
+
if (reducer === undefined) {
|
|
79
|
+
// Framework messages have optional reducers; props and stores stay readable as context.
|
|
80
|
+
if (msg._tag !== 'PropsChanged' && msg._tag !== 'StoreChanged') {
|
|
81
|
+
console.warn(`<${this.#host.tag}> has no update for message "${msg._tag}".`);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const prev = this.state();
|
|
86
|
+
this.apply(reducer(prev, msg, this.ctx()));
|
|
87
|
+
const next = this.state();
|
|
88
|
+
if (DEVTOOLS_ENABLED)
|
|
89
|
+
devUpdate(this.#host.el, this.#host.tag, msg, prev, next);
|
|
90
|
+
if (!schedule)
|
|
91
|
+
return;
|
|
92
|
+
if (this.#spec.viewTransition?.(prev, next, msg) === true)
|
|
93
|
+
requestTransition();
|
|
94
|
+
this.#host.invalidate(this.#onFrame(msg._tag));
|
|
95
|
+
}
|
|
96
|
+
apply(next) {
|
|
97
|
+
const [state, commands] = splitNext(next);
|
|
98
|
+
this.#model = { value: state };
|
|
99
|
+
for (const cmd of commands) {
|
|
100
|
+
const local = cmd.driver.local;
|
|
101
|
+
if (local !== undefined)
|
|
102
|
+
local(this, cmd.input);
|
|
103
|
+
else if (this.#pending !== undefined)
|
|
104
|
+
this.#pending.push(cmd);
|
|
105
|
+
else
|
|
106
|
+
this.#commands().run(cmd);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Starts commands kept for later (init's, after a server-rendered host's first render). */
|
|
110
|
+
run(commands) {
|
|
111
|
+
this.apply([this.state(), commands]);
|
|
112
|
+
}
|
|
113
|
+
ctx() {
|
|
114
|
+
return { props: this.#host.props(), read: (store) => this.stores().read(store) };
|
|
115
|
+
}
|
|
116
|
+
/** Resolves stores and runs kept commands. Returns true if store instances were bound. */
|
|
117
|
+
connect() {
|
|
118
|
+
// The nearest <gyral-stores> may differ after a move.
|
|
119
|
+
const rebound = (this.#spec.stores?.length ?? 0) > 0 && this.stores().connect();
|
|
120
|
+
const pending = this.#pending ?? [];
|
|
121
|
+
this.#pending = undefined;
|
|
122
|
+
for (const cmd of pending)
|
|
123
|
+
this.#commands().run(cmd);
|
|
124
|
+
return rebound;
|
|
125
|
+
}
|
|
126
|
+
disconnect() {
|
|
127
|
+
this.#binding?.disconnect();
|
|
128
|
+
this.#interpreter?.dispose();
|
|
129
|
+
this.#interpreter = undefined;
|
|
130
|
+
this.#pending ??= [];
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The interpreter, started with the first command after connecting. Only reached for a
|
|
134
|
+
* command built by `command()`, which registered it (features.ts).
|
|
135
|
+
*/
|
|
136
|
+
#commands() {
|
|
137
|
+
const el = this.el;
|
|
138
|
+
return (this.#interpreter ??= features.commands(el, this.#spec.drivers, (msg) => {
|
|
139
|
+
this.dispatch(msg);
|
|
140
|
+
}, DEVTOOLS_ENABLED ? devCommands(() => devOwner(el, this.tag)) : undefined));
|
|
141
|
+
}
|
|
142
|
+
/** Does `tag` render in the frame lane (`spec.renderOnFrame`)? */
|
|
143
|
+
#onFrame(tag) {
|
|
144
|
+
return this.#spec.renderOnFrame?.includes(tag) === true;
|
|
145
|
+
}
|
|
146
|
+
#storeChanged(store, state, prev) {
|
|
147
|
+
if (this.#reducers['StoreChanged'] !== undefined) {
|
|
148
|
+
this.dispatch({ _tag: 'StoreChanged', store: store.name, state, prev });
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
this.#host.invalidate(this.#onFrame('StoreChanged'));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=host-model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-model.js","sourceRoot":"","sources":["../src/host-model.ts"],"names":[],"mappings":"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","sourcesContent":["// The model side of one Gyral host (docs/design-docs/view/05-element.md, ADR 0006, ADR 0013):\n// state, reducers and commands. element.ts owns the DOM side (props, root, lifecycle,\n// rendering) and asks this object for state and context. Reducers run at once, so `state` is\n// always current; only the render waits (04). The interpreter and the store binding come from\n// features.ts, registered by `command()` and `defineStore()`; marker drivers (focus, emit, store\n// sends) carry their own `local` handler.\nimport { DEVTOOLS_ENABLED, devCommands, devOwner, devUpdate } from '#devtools';\nimport { splitNext, type Command, type Next } from './command.js';\nimport {\n features,\n type FeatureHost,\n type LocalDriver,\n type LocalHost,\n type StoreLink,\n} from './features.js';\nimport type { Seed } from './hydration.js';\nimport { runInit } from './init.js';\nimport type { Interpreter } from './internal/interpreter.js';\nimport { requestTransition } from './scheduler.js';\nimport type { AnyStore } from './store.js';\nimport type { ComponentSpec, Ctx, IntentRejected, Tagged } from './types.js';\n\ntype Bag = Readonly<Record<string, unknown>>;\ntype Reducer<S> = (state: S, msg: Tagged, ctx: Ctx<object>) => Next<S, Tagged>;\n/** A command as the model runs it. */\nexport type ModelCommand = Command<Tagged | IntentRejected>;\ntype Cmd = ModelCommand;\n\n/** What the model needs from its element. */\nexport interface ModelHost {\n readonly el: FeatureHost;\n readonly tag: string;\n /** The declared props as components see them (defaults applied). */\n readonly props: () => Bag;\n /** The host's root once it has one (focus commands run against it). */\n readonly root: () => ParentNode | undefined;\n /** Something changed that the next render must show; `onFrame`: in the frame lane (04). */\n readonly invalidate: (onFrame: boolean) => void;\n}\n\nexport class HostModel<S, P> implements LocalHost {\n readonly el: FeatureHost;\n readonly tag: string;\n readonly root: () => ParentNode | undefined;\n readonly #host: ModelHost;\n readonly #spec: ComponentSpec<S, Tagged, P>;\n readonly #reducers: Readonly<Record<string, Reducer<S> | undefined>>;\n #binding: StoreLink | undefined;\n #model: { value: S } | undefined;\n /** Props as of init or the last PropsChanged: the `prev` of the next PropsChanged. */\n #seen: Bag | undefined;\n #interpreter: Interpreter<Tagged | IntentRejected> | undefined;\n /** Commands kept while disconnected (init's, before the first connect); undefined once connected. */\n #pending: Cmd[] | undefined = [];\n\n constructor(host: ModelHost, spec: ComponentSpec<S, Tagged, P>) {\n this.#host = host;\n this.el = host.el;\n this.tag = host.tag;\n this.root = host.root;\n this.#spec = spec;\n // Sound: dispatch() only calls the reducer whose key equals msg._tag.\n this.#reducers = spec.update as unknown as Readonly<Record<string, Reducer<S> | undefined>>;\n }\n\n /** The store binding, created on first use. Only reached once `defineStore()` has run. */\n readonly stores = (): StoreLink =>\n (this.#binding ??= (features.stores as NonNullable<typeof features.stores>)(\n this.#host.el,\n this.#host.tag,\n this.#spec.stores ?? [],\n (store, state, prev) => {\n this.#storeChanged(store, state, prev);\n },\n ));\n\n get ready(): boolean {\n return this.#model !== undefined;\n }\n\n /** The current state, running `init` (and `initialMessages`) on first read. */\n state(initialMessages: readonly Tagged[] = []): S {\n if (this.#model === undefined) {\n this.#seen = this.#host.props();\n this.apply(runInit(this.#spec, this.#seen as P));\n for (const msg of initialMessages) this.dispatch(msg, false);\n }\n return (this.#model as { value: S }).value;\n }\n\n hasReducer(tag: string): boolean {\n return this.#reducers[tag] !== undefined;\n }\n\n /**\n * Resumes from a server seed (ADR 0012): the server's state, or `init(props)`'s when the\n * seed left it out. Returns init's commands, to start after the first render.\n */\n resume(seed: Seed): readonly Cmd[] {\n this.#seen = this.#host.props();\n const [initial, commands] = splitNext(runInit(this.#spec, this.#seen as P));\n // Sound: the seed is this component's own state, written by the server.\n this.#model = { value: 'state' in seed ? (seed.state as S) : initial };\n return commands;\n }\n\n /** PropsChanged when props differ from the last ones seen (`Object.is` per prop). */\n syncProps(names: readonly string[]): void {\n const prev = this.#seen;\n if (prev === undefined) return;\n const props = this.#host.props();\n if (names.every((name) => Object.is(props[name], prev[name]))) return;\n this.#seen = props;\n this.dispatch({ _tag: 'PropsChanged', props, prev } as Tagged, false);\n }\n\n dispatch(msg: Tagged, schedule = true): void {\n const reducer = this.#reducers[msg._tag];\n if (reducer === undefined) {\n // Framework messages have optional reducers; props and stores stay readable as context.\n if (msg._tag !== 'PropsChanged' && msg._tag !== 'StoreChanged') {\n console.warn(`<${this.#host.tag}> has no update for message \"${msg._tag}\".`);\n }\n return;\n }\n const prev = this.state();\n this.apply(reducer(prev, msg, this.ctx() as Ctx<object>));\n const next = this.state();\n if (DEVTOOLS_ENABLED) devUpdate(this.#host.el, this.#host.tag, msg, prev, next);\n if (!schedule) return;\n if (this.#spec.viewTransition?.(prev, next, msg) === true) requestTransition();\n this.#host.invalidate(this.#onFrame(msg._tag));\n }\n\n apply(next: Next<S, Tagged> | Next<S, Tagged | IntentRejected>): void {\n const [state, commands] = splitNext(next as Next<S, Tagged>);\n this.#model = { value: state };\n for (const cmd of commands as readonly Cmd[]) {\n const local = (cmd.driver as Partial<LocalDriver>).local;\n if (local !== undefined) local(this, cmd.input);\n else if (this.#pending !== undefined) this.#pending.push(cmd);\n else this.#commands().run(cmd);\n }\n }\n\n /** Starts commands kept for later (init's, after a server-rendered host's first render). */\n run(commands: readonly Cmd[]): void {\n this.apply([this.state(), commands]);\n }\n\n ctx(): Ctx<P> {\n return { props: this.#host.props() as P, read: (store) => this.stores().read(store) };\n }\n\n /** Resolves stores and runs kept commands. Returns true if store instances were bound. */\n connect(): boolean {\n // The nearest <gyral-stores> may differ after a move.\n const rebound = (this.#spec.stores?.length ?? 0) > 0 && this.stores().connect();\n const pending = this.#pending ?? [];\n this.#pending = undefined;\n for (const cmd of pending) this.#commands().run(cmd);\n return rebound;\n }\n\n disconnect(): void {\n this.#binding?.disconnect();\n this.#interpreter?.dispose();\n this.#interpreter = undefined;\n this.#pending ??= [];\n }\n\n /**\n * The interpreter, started with the first command after connecting. Only reached for a\n * command built by `command()`, which registered it (features.ts).\n */\n #commands(): Interpreter<Tagged | IntentRejected> {\n const el = this.el;\n return (this.#interpreter ??= (features.commands as NonNullable<typeof features.commands>)(\n el,\n this.#spec.drivers,\n (msg) => {\n this.dispatch(msg);\n },\n DEVTOOLS_ENABLED ? devCommands(() => devOwner(el, this.tag)) : undefined,\n ));\n }\n\n /** Does `tag` render in the frame lane (`spec.renderOnFrame`)? */\n #onFrame(tag: string): boolean {\n return this.#spec.renderOnFrame?.includes(tag) === true;\n }\n\n #storeChanged(store: AnyStore, state: unknown, prev: unknown): void {\n if (this.#reducers['StoreChanged'] !== undefined) {\n this.dispatch({ _tag: 'StoreChanged', store: store.name, state, prev } as Tagged);\n } else {\n this.#host.invalidate(this.#onFrame('StoreChanged'));\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ChildValue, type SeenMarkup } from './view/index.js';
|
|
2
|
+
export { scheduleIsland } from './islands.js';
|
|
3
|
+
/**
|
|
4
|
+
* Hydrates a server-rendered host's root with its first view (view/07-hydration.md "Steps").
|
|
5
|
+
* A shadow root swaps styles in the same step (08 "Hydration"): the shared sheets are adopted
|
|
6
|
+
* and the server's `<style>` removed. On a mismatch development throws; production recovers
|
|
7
|
+
* this component only: it clears the root, renders fresh and warns.
|
|
8
|
+
*/
|
|
9
|
+
export declare function hydrateRoot(host: HTMLElement, tag: string, view: ChildValue, root: ShadowRoot | HTMLElement, sheets: CSSStyleSheet[] | undefined, seen: SeenMarkup): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// The lazily loaded half of hydration (view/07-hydration.md "Loading"): the parallel walk and
|
|
2
|
+
// islands. element.ts imports this module with import() when the first host that needs it
|
|
3
|
+
// connects (one with a seed or `defer-hydration`), so client-only pages never fetch it.
|
|
4
|
+
import { DEVTOOLS_ENABLED, devMismatch } from '#devtools';
|
|
5
|
+
import { DEV, hydrate, HydrationMismatch, render, } from './view/index.js';
|
|
6
|
+
export { scheduleIsland } from './islands.js';
|
|
7
|
+
/**
|
|
8
|
+
* Hydrates a server-rendered host's root with its first view (view/07-hydration.md "Steps").
|
|
9
|
+
* A shadow root swaps styles in the same step (08 "Hydration"): the shared sheets are adopted
|
|
10
|
+
* and the server's `<style>` removed. On a mismatch development throws; production recovers
|
|
11
|
+
* this component only: it clears the root, renders fresh and warns.
|
|
12
|
+
*/
|
|
13
|
+
export function hydrateRoot(host, tag, view, root, sheets, seen) {
|
|
14
|
+
if (sheets !== undefined) {
|
|
15
|
+
root.adoptedStyleSheets = sheets;
|
|
16
|
+
const first = root.firstChild;
|
|
17
|
+
if (sheets.length > 0 && first?.nodeName === 'STYLE')
|
|
18
|
+
first.remove();
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
hydrate(view, root, `<${tag}>`, seen);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (!(error instanceof HydrationMismatch))
|
|
25
|
+
throw error;
|
|
26
|
+
if (DEVTOOLS_ENABLED)
|
|
27
|
+
devMismatch(host, tag, error.message);
|
|
28
|
+
if (DEV)
|
|
29
|
+
throw error;
|
|
30
|
+
console.warn(`${error.message} <${tag}> was rendered fresh.`);
|
|
31
|
+
root.replaceChildren();
|
|
32
|
+
render(view, root, seen);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=hydration-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hydration-client.js","sourceRoot":"","sources":["../src/hydration-client.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,0FAA0F;AAC1F,wFAAwF;AACxF,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EACL,GAAG,EACH,OAAO,EACP,iBAAiB,EACjB,MAAM,GAGP,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,IAAiB,EACjB,GAAW,EACX,IAAgB,EAChB,IAA8B,EAC9B,MAAmC,EACnC,IAAgB;IAEhB,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACxB,IAAmB,CAAC,kBAAkB,GAAG,MAAM,CAAC;QACjD,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC;QAC9B,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,EAAE,QAAQ,KAAK,OAAO;YAAE,KAAK,CAAC,MAAM,EAAE,CAAC;IACvE,CAAC;IACD,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,GAAG,GAAG,EAAE,IAAI,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,CAAC,KAAK,YAAY,iBAAiB,CAAC;YAAE,MAAM,KAAK,CAAC;QACvD,IAAI,gBAAgB;YAAE,WAAW,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QAC5D,IAAI,GAAG;YAAE,MAAM,KAAK,CAAC;QACrB,OAAO,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,OAAO,KAAK,GAAG,uBAAuB,CAAC,CAAC;QAC9D,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAC3B,CAAC;AACH,CAAC","sourcesContent":["// The lazily loaded half of hydration (view/07-hydration.md \"Loading\"): the parallel walk and\n// islands. element.ts imports this module with import() when the first host that needs it\n// connects (one with a seed or `defer-hydration`), so client-only pages never fetch it.\nimport { DEVTOOLS_ENABLED, devMismatch } from '#devtools';\nimport {\n DEV,\n hydrate,\n HydrationMismatch,\n render,\n type ChildValue,\n type SeenMarkup,\n} from './view/index.js';\n\nexport { scheduleIsland } from './islands.js';\n\n/**\n * Hydrates a server-rendered host's root with its first view (view/07-hydration.md \"Steps\").\n * A shadow root swaps styles in the same step (08 \"Hydration\"): the shared sheets are adopted\n * and the server's `<style>` removed. On a mismatch development throws; production recovers\n * this component only: it clears the root, renders fresh and warns.\n */\nexport function hydrateRoot(\n host: HTMLElement,\n tag: string,\n view: ChildValue,\n root: ShadowRoot | HTMLElement,\n sheets: CSSStyleSheet[] | undefined,\n seen: SeenMarkup,\n): void {\n if (sheets !== undefined) {\n (root as ShadowRoot).adoptedStyleSheets = sheets;\n const first = root.firstChild;\n if (sheets.length > 0 && first?.nodeName === 'STYLE') first.remove();\n }\n try {\n hydrate(view, root, `<${tag}>`, seen);\n } catch (error) {\n if (!(error instanceof HydrationMismatch)) throw error;\n if (DEVTOOLS_ENABLED) devMismatch(host, tag, error.message);\n if (DEV) throw error;\n console.warn(`${error.message} <${tag}> was rendered fresh.`);\n root.replaceChildren();\n render(view, root, seen);\n }\n}\n"]}
|
package/dist/hydration.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { PropertyDeclaration } from 'lit';
|
|
2
1
|
/** Host attribute holding the JSON seed. Removed once the client has read it. */
|
|
3
2
|
export declare const SEED_ATTRIBUTE = "data-gyral-seed";
|
|
4
3
|
export interface Seed {
|
|
@@ -12,25 +11,20 @@ export interface Seed {
|
|
|
12
11
|
}
|
|
13
12
|
/** JSON equality: seeds are JSON, so two values that serialize alike hydrate alike. */
|
|
14
13
|
export declare const sameJson: (a: unknown, b: unknown) => boolean;
|
|
15
|
-
type Declarations = Readonly<Record<string, PropertyDeclaration>>;
|
|
16
14
|
/**
|
|
17
|
-
* Server side:
|
|
18
|
-
*
|
|
19
|
-
* The SSR renderer escapes attribute values, so the JSON is HTML-safe.
|
|
15
|
+
* Server side: the seed for a component whose props `carried` travel in no attribute
|
|
16
|
+
* (property holes) and whose state is `state`; `initial` is `init(props)`'s state.
|
|
20
17
|
*/
|
|
21
|
-
export declare function
|
|
22
|
-
readonly value: unknown;
|
|
23
|
-
}): void;
|
|
24
|
-
/**
|
|
25
|
-
* Client side, right after hydration (gyral-4k7.12, ADR 0012): Lit SSR writes nothing for
|
|
26
|
-
* an empty primitive (`''`, `null`, `undefined`), so `<!--lit-part--><!--/lit-part-->`
|
|
27
|
-
* has no Text node between the markers. Hydration still records the primitive as the
|
|
28
|
-
* part's committed value, and the next text commit writes `.data` into the
|
|
29
|
-
* `<!--/lit-part-->` comment instead. Restore the empty Text node a client render would
|
|
30
|
-
* have created. Harmless for `nothing` and empty iterables, which clear or insert before
|
|
31
|
-
* the end marker. Shadow roots below `root` are handled by their own hosts.
|
|
32
|
-
*/
|
|
33
|
-
export declare function fillEmptyTextParts(root: Node): void;
|
|
18
|
+
export declare function makeSeed(tag: string, state: unknown, initial: unknown, carried: Readonly<Record<string, unknown>>): Seed;
|
|
34
19
|
/** Client side: reads and removes the seed, if this element was server-rendered by Gyral. */
|
|
35
20
|
export declare function takeSeed(host: Element): Seed | undefined;
|
|
36
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The walk and islands (hydration-client.ts) once loaded; `null` if loading failed (a stale
|
|
23
|
+
* deployment: server-rendered hosts then render fresh); undefined until then.
|
|
24
|
+
*/
|
|
25
|
+
export declare let hydrationCode: typeof import('./hydration-client.js') | null | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Loads the hydration code (once) and runs `next` when it has loaded or failed, in call
|
|
28
|
+
* order. settled() waits meanwhile (view/07-hydration.md "Loading").
|
|
29
|
+
*/
|
|
30
|
+
export declare function whenHydrationLoads(next: () => void): void;
|
package/dist/hydration.js
CHANGED
|
@@ -1,55 +1,24 @@
|
|
|
1
|
+
// Server-render seeds (docs/design-docs/0012-ssr.md, view/06-server.md "Components"). The
|
|
2
|
+
// server writes each component's state, plus the props an attribute can't carry, into one
|
|
3
|
+
// attribute; the client reads it on connect, before its first render, so both sides start from
|
|
4
|
+
// the same state. The walk that then hydrates the server's DOM is hydration-client.ts, which
|
|
5
|
+
// core loads lazily (view/07-hydration.md "Loading").
|
|
1
6
|
import { warnJsonHazard } from './json-safety.js';
|
|
7
|
+
import { hold } from './scheduler.js';
|
|
8
|
+
import { DEV } from './view/index.js';
|
|
2
9
|
/** Host attribute holding the JSON seed. Removed once the client has read it. */
|
|
3
10
|
export const SEED_ATTRIBUTE = 'data-gyral-seed';
|
|
4
11
|
/** JSON equality: seeds are JSON, so two values that serialize alike hydrate alike. */
|
|
5
12
|
export const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
6
|
-
function attributeName(name, decl) {
|
|
7
|
-
if (decl.attribute === false)
|
|
8
|
-
return undefined;
|
|
9
|
-
return typeof decl.attribute === 'string' ? decl.attribute : name.toLowerCase();
|
|
10
|
-
}
|
|
11
13
|
/**
|
|
12
|
-
* Server side:
|
|
13
|
-
*
|
|
14
|
-
* The SSR renderer escapes attribute values, so the JSON is HTML-safe.
|
|
14
|
+
* Server side: the seed for a component whose props `carried` travel in no attribute
|
|
15
|
+
* (property holes) and whose state is `state`; `initial` is `init(props)`'s state.
|
|
15
16
|
*/
|
|
16
|
-
export function
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (value !== undefined && (attr === undefined || !host.hasAttribute(attr))) {
|
|
22
|
-
carried[name] = value;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
const derivable = initialState !== undefined && sameJson(state, initialState.value);
|
|
26
|
-
const seed = derivable ? { props: carried } : { state, props: carried };
|
|
27
|
-
warnJsonHazard(`<${host.localName}>`, seed, 'seed');
|
|
28
|
-
host.setAttribute(SEED_ATTRIBUTE, JSON.stringify(seed));
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Client side, right after hydration (gyral-4k7.12, ADR 0012): Lit SSR writes nothing for
|
|
32
|
-
* an empty primitive (`''`, `null`, `undefined`), so `<!--lit-part--><!--/lit-part-->`
|
|
33
|
-
* has no Text node between the markers. Hydration still records the primitive as the
|
|
34
|
-
* part's committed value, and the next text commit writes `.data` into the
|
|
35
|
-
* `<!--/lit-part-->` comment instead. Restore the empty Text node a client render would
|
|
36
|
-
* have created. Harmless for `nothing` and empty iterables, which clear or insert before
|
|
37
|
-
* the end marker. Shadow roots below `root` are handled by their own hosts.
|
|
38
|
-
*/
|
|
39
|
-
export function fillEmptyTextParts(root) {
|
|
40
|
-
const doc = root.ownerDocument ?? root;
|
|
41
|
-
const walker = doc.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
|
|
42
|
-
const empty = [];
|
|
43
|
-
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
|
|
44
|
-
const next = node.nextSibling;
|
|
45
|
-
if (node.data === 'lit-part' &&
|
|
46
|
-
next instanceof Comment &&
|
|
47
|
-
next.data === '/lit-part') {
|
|
48
|
-
empty.push(node);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
for (const start of empty)
|
|
52
|
-
start.after(doc.createTextNode(''));
|
|
17
|
+
export function makeSeed(tag, state, initial, carried) {
|
|
18
|
+
const seed = sameJson(state, initial) ? { props: carried } : { state, props: carried };
|
|
19
|
+
if (DEV)
|
|
20
|
+
warnJsonHazard(`<${tag}>`, seed, 'seed'); // view/06-server.md "Seed": development
|
|
21
|
+
return seed;
|
|
53
22
|
}
|
|
54
23
|
/** Client side: reads and removes the seed, if this element was server-rendered by Gyral. */
|
|
55
24
|
export function takeSeed(host) {
|
|
@@ -69,4 +38,35 @@ export function takeSeed(host) {
|
|
|
69
38
|
return undefined;
|
|
70
39
|
}
|
|
71
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The walk and islands (hydration-client.ts) once loaded; `null` if loading failed (a stale
|
|
43
|
+
* deployment: server-rendered hosts then render fresh); undefined until then.
|
|
44
|
+
*/
|
|
45
|
+
export let hydrationCode;
|
|
46
|
+
/** Hosts waiting for the code, in connection order; undefined when no load is pending. */
|
|
47
|
+
let waiting;
|
|
48
|
+
/**
|
|
49
|
+
* Loads the hydration code (once) and runs `next` when it has loaded or failed, in call
|
|
50
|
+
* order. settled() waits meanwhile (view/07-hydration.md "Loading").
|
|
51
|
+
*/
|
|
52
|
+
export function whenHydrationLoads(next) {
|
|
53
|
+
if (waiting !== undefined) {
|
|
54
|
+
waiting.push(next);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
waiting = [next];
|
|
58
|
+
const done = (code) => {
|
|
59
|
+
hydrationCode = code;
|
|
60
|
+
const hosts = waiting ?? [];
|
|
61
|
+
waiting = undefined;
|
|
62
|
+
// Each in its own microtask, in order: one host throwing doesn't stop the others. They all
|
|
63
|
+
// run before `hold` counts the load as done.
|
|
64
|
+
for (const host of hosts)
|
|
65
|
+
queueMicrotask(host);
|
|
66
|
+
};
|
|
67
|
+
hold(import('./hydration-client.js').then(done, (error) => {
|
|
68
|
+
console.error('gyral: hydration code failed to load; rendering fresh', error);
|
|
69
|
+
done(null);
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
72
|
//# sourceMappingURL=hydration.js.map
|
package/dist/hydration.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hydration.js","sourceRoot":"","sources":["../src/hydration.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hydration.js","sourceRoot":"","sources":["../src/hydration.ts"],"names":[],"mappings":"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","sourcesContent":["// Server-render seeds (docs/design-docs/0012-ssr.md, view/06-server.md \"Components\"). The\n// server writes each component's state, plus the props an attribute can't carry, into one\n// attribute; the client reads it on connect, before its first render, so both sides start from\n// the same state. The walk that then hydrates the server's DOM is hydration-client.ts, which\n// core loads lazily (view/07-hydration.md \"Loading\").\nimport { warnJsonHazard } from './json-safety.js';\nimport { hold } from './scheduler.js';\nimport { DEV } from './view/index.js';\n\n/** Host attribute holding the JSON seed. Removed once the client has read it. */\nexport const SEED_ATTRIBUTE = 'data-gyral-seed';\n\nexport interface Seed {\n /**\n * The server's state. Omitted when it equals what `init(props)` returns for the seeded\n * props (gyral-czi 4k7.10): the client recomputes it instead of reading a second copy of\n * data the props already carry.\n */\n readonly state?: unknown;\n readonly props: Readonly<Record<string, unknown>>;\n}\n\n/** JSON equality: seeds are JSON, so two values that serialize alike hydrate alike. */\nexport const sameJson = (a: unknown, b: unknown): boolean =>\n JSON.stringify(a) === JSON.stringify(b);\n\n/**\n * Server side: the seed for a component whose props `carried` travel in no attribute\n * (property holes) and whose state is `state`; `initial` is `init(props)`'s state.\n */\nexport function makeSeed(\n tag: string,\n state: unknown,\n initial: unknown,\n carried: Readonly<Record<string, unknown>>,\n): Seed {\n const seed: Seed = sameJson(state, initial) ? { props: carried } : { state, props: carried };\n if (DEV) warnJsonHazard(`<${tag}>`, seed, 'seed'); // view/06-server.md \"Seed\": development\n\n return seed;\n}\n\n/** Client side: reads and removes the seed, if this element was server-rendered by Gyral. */\nexport function takeSeed(host: Element): Seed | undefined {\n const raw = host.getAttribute(SEED_ATTRIBUTE);\n if (raw === null) return undefined;\n host.removeAttribute(SEED_ATTRIBUTE);\n try {\n const parsed = JSON.parse(raw) as Partial<Seed> | null;\n if (parsed === null || typeof parsed !== 'object') return undefined;\n const props = parsed.props ?? {};\n return 'state' in parsed ? { state: parsed.state, props } : { props };\n } catch (error) {\n console.error(`<${host.localName}> has an unreadable ${SEED_ATTRIBUTE}`, error);\n return undefined;\n }\n}\n\n/**\n * The walk and islands (hydration-client.ts) once loaded; `null` if loading failed (a stale\n * deployment: server-rendered hosts then render fresh); undefined until then.\n */\nexport let hydrationCode: typeof import('./hydration-client.js') | null | undefined;\n/** Hosts waiting for the code, in connection order; undefined when no load is pending. */\nlet waiting: (() => void)[] | undefined;\n\n/**\n * Loads the hydration code (once) and runs `next` when it has loaded or failed, in call\n * order. settled() waits meanwhile (view/07-hydration.md \"Loading\").\n */\nexport function whenHydrationLoads(next: () => void): void {\n if (waiting !== undefined) {\n waiting.push(next);\n return;\n }\n waiting = [next];\n const done = (code: typeof hydrationCode): void => {\n hydrationCode = code;\n const hosts = waiting ?? [];\n waiting = undefined;\n // Each in its own microtask, in order: one host throwing doesn't stop the others. They all\n // run before `hold` counts the load as done.\n for (const host of hosts) queueMicrotask(host);\n };\n hold(\n import('./hydration-client.js').then(done, (error: unknown) => {\n console.error('gyral: hydration code failed to load; rendering fresh', error);\n done(null);\n }),\n );\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,34 +9,27 @@ export { ISLAND_ATTRIBUTE } from './islands.js';
|
|
|
9
9
|
export type { HydrateStrategy } from './islands.js';
|
|
10
10
|
export type { ChildSource, OutputSource } from './children.js';
|
|
11
11
|
export { define } from './define.js';
|
|
12
|
+
export { intents } from './intent.js';
|
|
13
|
+
export { settled } from './settled.js';
|
|
12
14
|
export { changed, defineStore, send } from './store.js';
|
|
13
15
|
export type { AnyStore, AnyStoreInstance, Store, StoreChanged, StoreInstance, StoreOverrides, StoreRef, StoreSendInput, SeedCheck, StoreResolver, StoreSpec, StoreUpdate, } from './store.js';
|
|
14
16
|
export { STORE_SEND } from './store.js';
|
|
15
17
|
export { resetDocumentStores, scriptSafeJson, STORE_SEED_ATTRIBUTE, StoreRegistry, STORES_ELEMENT, withStoreScope, } from './store-scope.js';
|
|
16
|
-
export { defineStoresProvider } from './stores-provider.js';
|
|
17
18
|
export { DRIVERS_ELEMENT, provideDrivers } from './drivers-scope.js';
|
|
18
19
|
export { jsonHazard, warnJsonHazard } from './json-safety.js';
|
|
19
20
|
export { random, randomDriver, randomInt, toInt, type RandomInput } from './random.js';
|
|
20
21
|
export { defineForm, field, fieldErrors, form, formDataToObject, formFields, intentRejectedSchema, redirectedTo, validateForm, } from './forms.js';
|
|
21
22
|
export type { FormDefinition, FormRedirected, FormResult, FormValue } from './forms.js';
|
|
22
|
-
export { ElementDirective } from './element-directive.js';
|
|
23
23
|
export { runInit } from './init.js';
|
|
24
|
-
export { invalid } from './invalid.js';
|
|
25
|
-
export { findInScope, labelledBy } from './
|
|
26
|
-
export {
|
|
27
|
-
export {
|
|
24
|
+
export { invalid } from './hooks/invalid.js';
|
|
25
|
+
export { findInScope, labelledBy } from './hooks/labelled-by.js';
|
|
26
|
+
export { prop } from './prop.js';
|
|
27
|
+
export type { Prop, PropKind, PropsOf } from './prop.js';
|
|
28
28
|
export { DEVTOOLS_ENABLED as devtoolsEnabled, devLiveComponents as devtoolsLiveComponents, } from '#devtools';
|
|
29
29
|
export { DEVTOOLS_GLOBAL } from './devtools-events.js';
|
|
30
30
|
export type { CommandPhase, CommandTraceEvent, DevComponentRef, DevEvent, DevtoolsHook, } from './devtools-events.js';
|
|
31
|
-
export
|
|
32
|
-
export type { Styles } from './styles.js';
|
|
33
|
-
export { HIDDEN_MARKER, HYDRATE_KEY, isLightComponent, LIGHT_ATTRIBUTE } from './light-dom.js';
|
|
31
|
+
export { isLightComponent, LIGHT_ATTRIBUTE } from './light-dom.js';
|
|
34
32
|
export type { GyralElement, GyralElementClass } from './define.js';
|
|
35
33
|
export type { ComponentSpec, Ctx, FieldIssue, FormFields, IntentInput, IntentNames, IntentParser, IntentRejected, Intents, PropDeclarations, Hydrated, PropsChanged, Stateless, StoreReader, Tagged, Update, } from './types.js';
|
|
36
|
-
export { css, html,
|
|
37
|
-
export {
|
|
38
|
-
export { classMap } from 'lit/directives/class-map.js';
|
|
39
|
-
export { keyed } from 'lit/directives/keyed.js';
|
|
40
|
-
export { live } from 'lit/directives/live.js';
|
|
41
|
-
export { repeat } from 'lit/directives/repeat.js';
|
|
42
|
-
export { styleMap } from 'lit/directives/style-map.js';
|
|
34
|
+
export { css, defineHook, each, html, HydrationMismatch, nothing, raw } from './view/index.js';
|
|
35
|
+
export type { ChildValue, CssValue, HookAttributes, HookResult, HookSpec, ListResult, RawResult, StyleSource, Styles, TemplateResult, } from './view/index.js';
|