@gyral/core 0.0.0 → 0.2.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 +32 -0
- package/LICENSE +1 -1
- package/NOTICE +11 -0
- package/README.md +45 -5
- package/dist/accessible-name.d.ts +19 -0
- package/dist/accessible-name.js +56 -0
- package/dist/accessible-name.js.map +1 -0
- package/dist/children.d.ts +37 -0
- package/dist/children.js +49 -0
- package/dist/children.js.map +1 -0
- package/dist/command.d.ts +63 -0
- package/dist/command.js +26 -0
- package/dist/command.js.map +1 -0
- package/dist/define.d.ts +9 -0
- package/dist/define.js +338 -0
- package/dist/define.js.map +1 -0
- package/dist/devtools-events.d.ts +51 -0
- package/dist/devtools-events.js +3 -0
- package/dist/devtools-events.js.map +1 -0
- package/dist/devtools-off.d.ts +9 -0
- package/dist/devtools-off.js +10 -0
- package/dist/devtools-off.js.map +1 -0
- package/dist/devtools.d.ts +13 -0
- package/dist/devtools.js +68 -0
- package/dist/devtools.js.map +1 -0
- package/dist/drivers-scope.d.ts +15 -0
- package/dist/drivers-scope.js +48 -0
- package/dist/drivers-scope.js.map +1 -0
- package/dist/element-directive.d.ts +20 -0
- package/dist/element-directive.js +31 -0
- package/dist/element-directive.js.map +1 -0
- package/dist/element-types.d.ts +34 -0
- package/dist/element-types.js +2 -0
- package/dist/element-types.js.map +1 -0
- package/dist/focus.d.ts +39 -0
- package/dist/focus.js +42 -0
- package/dist/focus.js.map +1 -0
- package/dist/forms.d.ts +59 -0
- package/dist/forms.js +138 -0
- package/dist/forms.js.map +1 -0
- package/dist/hydration.d.ts +36 -0
- package/dist/hydration.js +72 -0
- package/dist/hydration.js.map +1 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +7 -0
- package/dist/init.js +10 -0
- package/dist/init.js.map +1 -0
- package/dist/intent.d.ts +25 -0
- package/dist/intent.js +157 -0
- package/dist/intent.js.map +1 -0
- package/dist/internal/interpreter.d.ts +8 -0
- package/dist/internal/interpreter.js +166 -0
- package/dist/internal/interpreter.js.map +1 -0
- package/dist/invalid.d.ts +21 -0
- package/dist/invalid.js +70 -0
- package/dist/invalid.js.map +1 -0
- package/dist/invokers.d.ts +15 -0
- package/dist/invokers.js +64 -0
- package/dist/invokers.js.map +1 -0
- package/dist/islands.d.ts +11 -0
- package/dist/islands.js +63 -0
- package/dist/islands.js.map +1 -0
- package/dist/json-safety.d.ts +8 -0
- package/dist/json-safety.js +68 -0
- package/dist/json-safety.js.map +1 -0
- package/dist/light-dom.d.ts +61 -0
- package/dist/light-dom.js +109 -0
- package/dist/light-dom.js.map +1 -0
- package/dist/lit-html-check.d.ts +6 -0
- package/dist/lit-html-check.js +48 -0
- package/dist/lit-html-check.js.map +1 -0
- package/dist/live-boolean.d.ts +22 -0
- package/dist/live-boolean.js +34 -0
- package/dist/live-boolean.js.map +1 -0
- package/dist/props.d.ts +26 -0
- package/dist/props.js +44 -0
- package/dist/props.js.map +1 -0
- package/dist/random.d.ts +13 -0
- package/dist/random.js +19 -0
- package/dist/random.js.map +1 -0
- package/dist/states.d.ts +7 -0
- package/dist/states.js +23 -0
- package/dist/states.js.map +1 -0
- package/dist/store-binding.d.ts +14 -0
- package/dist/store-binding.js +63 -0
- package/dist/store-binding.js.map +1 -0
- package/dist/store-scope.d.ts +40 -0
- package/dist/store-scope.js +146 -0
- package/dist/store-scope.js.map +1 -0
- package/dist/store.d.ts +114 -0
- package/dist/store.js +129 -0
- package/dist/store.js.map +1 -0
- package/dist/stores-provider.d.ts +6 -0
- package/dist/stores-provider.js +57 -0
- package/dist/stores-provider.js.map +1 -0
- package/dist/styles.d.ts +12 -0
- package/dist/styles.js +11 -0
- package/dist/styles.js.map +1 -0
- package/dist/template-whitespace.d.ts +7 -0
- package/dist/template-whitespace.js +192 -0
- package/dist/template-whitespace.js.map +1 -0
- package/dist/templates.d.ts +5 -0
- package/dist/templates.js +15 -0
- package/dist/templates.js.map +1 -0
- package/dist/textarea.d.ts +26 -0
- package/dist/textarea.js +85 -0
- package/dist/textarea.js.map +1 -0
- package/dist/transitions.d.ts +9 -0
- package/dist/transitions.js +26 -0
- package/dist/transitions.js.map +1 -0
- package/dist/types.d.ts +186 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/vite.d.ts +25 -0
- package/dist/vite.js +37 -0
- package/dist/vite.js.map +1 -0
- package/package.json +41 -13
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invalid.js","sourceRoot":"","sources":["../src/invalid.ts"],"names":[],"mappings":"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","sourcesContent":["import { directive } from 'lit/directive.js';\nimport { ElementDirective } from './element-directive.js';\n\ntype Errors = readonly string[] | string | undefined;\n\ninterface Validatable extends Element {\n setCustomValidity(message: string): void;\n}\n\nconst isValidatable = (el: Element): el is Validatable => 'setCustomValidity' in el;\n\nconst valueOf = (el: Element): unknown => ('value' in el ? el.value : undefined);\n\nconst formOf = (el: Element): HTMLFormElement | undefined => {\n const form = 'form' in el ? el.form : undefined;\n return form instanceof HTMLFormElement ? form : undefined;\n};\n\n// Forms with a resubmit already queued: several stale fields in one validation pass resubmit once.\nconst resubmitting = new WeakSet<HTMLFormElement>();\n\n/**\n * Mirrors model errors to native validity (ADR 0008): sets `setCustomValidity` and\n * `aria-invalid`, so `:user-invalid` and native bubbles agree with the model.\n *\n * The custom error clears when:\n * - the control fires `input` or `change` (the user edited it);\n * - the bound errors become `undefined` or empty (the model cleared them);\n * - a submit finds the error stale: the value changed without an event (set from code, e.g.\n * by a test or a script). Then the error is dropped and the form is submitted again once.\n *\n * An error is applied again only when a new value arrives (keep `fieldErrors()` results in\n * state). Element parts don't render on the server, so this only runs in the browser.\n */\nclass InvalidDirective extends ElementDirective<[errors?: Errors]> {\n #last: Errors;\n #listening: AbortController | undefined;\n\n apply(el: Element, [errors]: [errors?: Errors]): void {\n if (errors === this.#last) return;\n this.#last = errors;\n this.#listening?.abort();\n this.#listening = undefined;\n const message = typeof errors === 'string' ? errors : (errors ?? []).join(' ');\n if (isValidatable(el)) el.setCustomValidity(message);\n if (message === '') {\n el.removeAttribute('aria-invalid');\n return;\n }\n el.setAttribute('aria-invalid', 'true');\n\n const listening = new AbortController();\n this.#listening = listening;\n const { signal } = listening;\n const rejectedValue = valueOf(el);\n const clear = (): void => {\n listening.abort();\n if (isValidatable(el)) el.setCustomValidity('');\n el.removeAttribute('aria-invalid');\n };\n el.addEventListener('input', clear, { signal });\n el.addEventListener('change', clear, { signal });\n el.addEventListener(\n 'invalid',\n (event) => {\n if (Object.is(valueOf(el), rejectedValue)) return; // still the rejected value\n event.preventDefault(); // no bubble for an error that no longer applies\n clear();\n const form = formOf(el);\n if (form === undefined || resubmitting.has(form)) return;\n resubmitting.add(form);\n queueMicrotask(() => {\n resubmitting.delete(form);\n form.requestSubmit();\n });\n },\n { signal },\n );\n }\n}\n\nexport const invalid = directive(InvalidDirective);\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface CommandInfo {
|
|
2
|
+
/** The command, e.g. `--clear`. */
|
|
3
|
+
readonly command: string;
|
|
4
|
+
/** The invoker (the button) that sent it. */
|
|
5
|
+
readonly source: Element | undefined;
|
|
6
|
+
}
|
|
7
|
+
/** Native invoker commands available? */
|
|
8
|
+
export declare const invokersSupported: () => boolean;
|
|
9
|
+
/** The command carried by a native `CommandEvent` or by Gyral's fallback event. */
|
|
10
|
+
export declare function commandOf(event: Event): CommandInfo | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Fallback for browsers without invoker commands: a click on a `command="--…"` invoker inside
|
|
13
|
+
* `root` dispatches a `command` CustomEvent on its `commandfor` target (same tree scope).
|
|
14
|
+
*/
|
|
15
|
+
export declare function shimInvokers(root: Node): void;
|
package/dist/invokers.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Invoker commands as an intent source (gyral-czi.6, ADR 0001 addendum, ADR 0003).
|
|
2
|
+
//
|
|
3
|
+
// <button type="button" commandfor="cart" command="--clear">Clear</button>
|
|
4
|
+
// <section id="cart" data-intent=${i.Clear} data-intent-on="command">…</section>
|
|
5
|
+
//
|
|
6
|
+
// Browsers with invoker commands dispatch a `CommandEvent` on the target. Elsewhere (invokers
|
|
7
|
+
// are Baseline newly available, not widely) Gyral dispatches an equivalent `command` event when
|
|
8
|
+
// a custom-command (`--…`) invoker inside a component is clicked, so the same markup works.
|
|
9
|
+
// Built-in commands (`show-modal`, `toggle-popover`, …) are left to the browser.
|
|
10
|
+
/** Native invoker commands available? */
|
|
11
|
+
export const invokersSupported = () => 'CommandEvent' in globalThis;
|
|
12
|
+
/** The command carried by a native `CommandEvent` or by Gyral's fallback event. */
|
|
13
|
+
export function commandOf(event) {
|
|
14
|
+
if (event.type !== 'command')
|
|
15
|
+
return undefined;
|
|
16
|
+
if (event instanceof CustomEvent) {
|
|
17
|
+
const detail = event.detail;
|
|
18
|
+
return typeof detail?.command === 'string'
|
|
19
|
+
? { command: detail.command, source: detail.source }
|
|
20
|
+
: undefined;
|
|
21
|
+
}
|
|
22
|
+
const native = event;
|
|
23
|
+
return typeof native.command === 'string'
|
|
24
|
+
? {
|
|
25
|
+
command: native.command,
|
|
26
|
+
source: native.source instanceof Element ? native.source : undefined,
|
|
27
|
+
}
|
|
28
|
+
: undefined;
|
|
29
|
+
}
|
|
30
|
+
/** The custom-command invoker an event passed through, if any. */
|
|
31
|
+
function invokerIn(event) {
|
|
32
|
+
for (const node of event.composedPath()) {
|
|
33
|
+
if (node instanceof Element &&
|
|
34
|
+
node.getAttribute('command')?.startsWith('--') === true &&
|
|
35
|
+
node.hasAttribute('commandfor')) {
|
|
36
|
+
return node;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Fallback for browsers without invoker commands: a click on a `command="--…"` invoker inside
|
|
43
|
+
* `root` dispatches a `command` CustomEvent on its `commandfor` target (same tree scope).
|
|
44
|
+
*/
|
|
45
|
+
export function shimInvokers(root) {
|
|
46
|
+
root.addEventListener('click', (event) => {
|
|
47
|
+
if (invokersSupported())
|
|
48
|
+
return;
|
|
49
|
+
const invoker = invokerIn(event);
|
|
50
|
+
const id = invoker?.getAttribute('commandfor');
|
|
51
|
+
const command = invoker?.getAttribute('command');
|
|
52
|
+
if (invoker === undefined || id == null || command == null)
|
|
53
|
+
return;
|
|
54
|
+
const scope = invoker.getRootNode();
|
|
55
|
+
const target = scope.getElementById(id);
|
|
56
|
+
if (target === null)
|
|
57
|
+
return;
|
|
58
|
+
target.dispatchEvent(new CustomEvent('command', {
|
|
59
|
+
detail: { command, source: invoker },
|
|
60
|
+
cancelable: true,
|
|
61
|
+
}));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=invokers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invokers.js","sourceRoot":"","sources":["../src/invokers.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,EAAE;AACF,6EAA6E;AAC7E,mFAAmF;AACnF,EAAE;AACF,8FAA8F;AAC9F,gGAAgG;AAChG,4FAA4F;AAC5F,iFAAiF;AASjF,yCAAyC;AACzC,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAY,EAAE,CAAC,cAAc,IAAI,UAAU,CAAC;AAE7E,mFAAmF;AACnF,MAAM,UAAU,SAAS,CAAC,KAAY;IACpC,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC/C,IAAI,KAAK,YAAY,WAAW,EAAE,CAAC;QACjC,MAAM,MAAM,GAAG,KAAK,CAAC,MAAqC,CAAC;QAC3D,OAAO,OAAO,MAAM,EAAE,OAAO,KAAK,QAAQ;YACxC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;YACpD,CAAC,CAAC,SAAS,CAAC;IAChB,CAAC;IACD,MAAM,MAAM,GAAG,KAAwD,CAAC;IACxE,OAAO,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ;QACvC,CAAC,CAAC;YACE,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,MAAM,EAAE,MAAM,CAAC,MAAM,YAAY,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS;SACrE;QACH,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC;AAED,kEAAkE;AAClE,SAAS,SAAS,CAAC,KAAY;IAC7B,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,YAAY,EAAE,EAAE,CAAC;QACxC,IACE,IAAI,YAAY,OAAO;YACvB,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,EAAE,UAAU,CAAC,IAAI,CAAC,KAAK,IAAI;YACvD,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,EAC/B,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,IAAU;IACrC,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;QACvC,IAAI,iBAAiB,EAAE;YAAE,OAAO;QAChC,MAAM,OAAO,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;QACjC,MAAM,EAAE,GAAG,OAAO,EAAE,YAAY,CAAC,YAAY,CAAC,CAAC;QAC/C,MAAM,OAAO,GAAG,OAAO,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC;QACjD,IAAI,OAAO,KAAK,SAAS,IAAI,EAAE,IAAI,IAAI,IAAI,OAAO,IAAI,IAAI;YAAE,OAAO;QACnE,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,EAA2B,CAAC;QAC7D,MAAM,MAAM,GAAG,KAAK,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC;QACxC,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO;QAC5B,MAAM,CAAC,aAAa,CAClB,IAAI,WAAW,CAAc,SAAS,EAAE;YACtC,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE;YACpC,UAAU,EAAE,IAAI;SACjB,CAAC,CACH,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["// Invoker commands as an intent source (gyral-czi.6, ADR 0001 addendum, ADR 0003).\n//\n// <button type=\"button\" commandfor=\"cart\" command=\"--clear\">Clear</button>\n// <section id=\"cart\" data-intent=${i.Clear} data-intent-on=\"command\">…</section>\n//\n// Browsers with invoker commands dispatch a `CommandEvent` on the target. Elsewhere (invokers\n// are Baseline newly available, not widely) Gyral dispatches an equivalent `command` event when\n// a custom-command (`--…`) invoker inside a component is clicked, so the same markup works.\n// Built-in commands (`show-modal`, `toggle-popover`, …) are left to the browser.\n\nexport interface CommandInfo {\n /** The command, e.g. `--clear`. */\n readonly command: string;\n /** The invoker (the button) that sent it. */\n readonly source: Element | undefined;\n}\n\n/** Native invoker commands available? */\nexport const invokersSupported = (): boolean => 'CommandEvent' in globalThis;\n\n/** The command carried by a native `CommandEvent` or by Gyral's fallback event. */\nexport function commandOf(event: Event): CommandInfo | undefined {\n if (event.type !== 'command') return undefined;\n if (event instanceof CustomEvent) {\n const detail = event.detail as Partial<CommandInfo> | null;\n return typeof detail?.command === 'string'\n ? { command: detail.command, source: detail.source }\n : undefined;\n }\n const native = event as Event & { command?: unknown; source?: unknown };\n return typeof native.command === 'string'\n ? {\n command: native.command,\n source: native.source instanceof Element ? native.source : undefined,\n }\n : undefined;\n}\n\n/** The custom-command invoker an event passed through, if any. */\nfunction invokerIn(event: Event): Element | undefined {\n for (const node of event.composedPath()) {\n if (\n node instanceof Element &&\n node.getAttribute('command')?.startsWith('--') === true &&\n node.hasAttribute('commandfor')\n ) {\n return node;\n }\n }\n return undefined;\n}\n\n/**\n * Fallback for browsers without invoker commands: a click on a `command=\"--…\"` invoker inside\n * `root` dispatches a `command` CustomEvent on its `commandfor` target (same tree scope).\n */\nexport function shimInvokers(root: Node): void {\n root.addEventListener('click', (event) => {\n if (invokersSupported()) return;\n const invoker = invokerIn(event);\n const id = invoker?.getAttribute('commandfor');\n const command = invoker?.getAttribute('command');\n if (invoker === undefined || id == null || command == null) return;\n const scope = invoker.getRootNode() as Document | ShadowRoot;\n const target = scope.getElementById(id);\n if (target === null) return;\n target.dispatchEvent(\n new CustomEvent<CommandInfo>('command', {\n detail: { command, source: invoker },\n cancelable: true,\n }),\n );\n });\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** When a server-rendered component hydrates. `load` (the default) means right away. */
|
|
2
|
+
export type HydrateStrategy = 'load' | 'idle' | 'visible' | 'interaction';
|
|
3
|
+
/** Server-written strategy for a deferred island; removed when the island is released. */
|
|
4
|
+
export declare const ISLAND_ATTRIBUTE = "data-gyral-hydrate";
|
|
5
|
+
/** Server side: marks the host as an island unless it hydrates on load. */
|
|
6
|
+
export declare function markIsland(host: Element, strategy: HydrateStrategy | undefined): void;
|
|
7
|
+
/**
|
|
8
|
+
* Client side: if `host` is a deferred island, releases it when its strategy fires. Called on
|
|
9
|
+
* connect; scheduling happens once per element.
|
|
10
|
+
*/
|
|
11
|
+
export declare function scheduleIsland(host: HTMLElement): void;
|
package/dist/islands.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Lazy hydration islands (gyral-4k7.4, ADR 0012 addendum). A server-rendered component may
|
|
2
|
+
// hydrate later than page load: when the browser is idle, when it scrolls into view, or when
|
|
3
|
+
// the user first reaches for it. The server marks it with Lit's `defer-hydration` plus the
|
|
4
|
+
// strategy; the client removes `defer-hydration` when the strategy fires, and Lit's hydrate
|
|
5
|
+
// support hydrates it in place (define() finishes connecting from attributeChangedCallback).
|
|
6
|
+
/** Server-written strategy for a deferred island; removed when the island is released. */
|
|
7
|
+
export const ISLAND_ATTRIBUTE = 'data-gyral-hydrate';
|
|
8
|
+
const DEFER = 'defer-hydration';
|
|
9
|
+
/** Events that mean the user is about to interact: they arrive before the click itself. */
|
|
10
|
+
const INTERACTION_EVENTS = ['pointerover', 'pointerdown', 'focusin', 'touchstart'];
|
|
11
|
+
/** Server side: marks the host as an island unless it hydrates on load. */
|
|
12
|
+
export function markIsland(host, strategy) {
|
|
13
|
+
if (strategy === undefined || strategy === 'load')
|
|
14
|
+
return;
|
|
15
|
+
host.setAttribute(DEFER, '');
|
|
16
|
+
host.setAttribute(ISLAND_ATTRIBUTE, strategy);
|
|
17
|
+
}
|
|
18
|
+
const scheduled = new WeakSet();
|
|
19
|
+
/** requestIdleCallback is not Baseline widely available; fall back to a short timeout. */
|
|
20
|
+
function whenIdle(run) {
|
|
21
|
+
const idle = globalThis.requestIdleCallback;
|
|
22
|
+
if (idle === undefined)
|
|
23
|
+
setTimeout(run, 200);
|
|
24
|
+
else
|
|
25
|
+
idle(run, { timeout: 2000 });
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Client side: if `host` is a deferred island, releases it when its strategy fires. Called on
|
|
29
|
+
* connect; scheduling happens once per element.
|
|
30
|
+
*/
|
|
31
|
+
export function scheduleIsland(host) {
|
|
32
|
+
const strategy = host.getAttribute(ISLAND_ATTRIBUTE);
|
|
33
|
+
if (strategy === null || !host.hasAttribute(DEFER) || scheduled.has(host))
|
|
34
|
+
return;
|
|
35
|
+
scheduled.add(host);
|
|
36
|
+
const release = () => {
|
|
37
|
+
host.removeAttribute(ISLAND_ATTRIBUTE);
|
|
38
|
+
host.removeAttribute(DEFER); // Lit's hydrate support hydrates it now
|
|
39
|
+
};
|
|
40
|
+
if (strategy === 'idle') {
|
|
41
|
+
whenIdle(release);
|
|
42
|
+
}
|
|
43
|
+
else if (strategy === 'visible') {
|
|
44
|
+
const observer = new IntersectionObserver((entries) => {
|
|
45
|
+
if (entries.some((entry) => entry.isIntersecting)) {
|
|
46
|
+
observer.disconnect();
|
|
47
|
+
release();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
observer.observe(host);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
const once = () => {
|
|
55
|
+
controller.abort();
|
|
56
|
+
release();
|
|
57
|
+
};
|
|
58
|
+
for (const type of INTERACTION_EVENTS) {
|
|
59
|
+
host.addEventListener(type, once, { capture: true, signal: controller.signal });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=islands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"islands.js","sourceRoot":"","sources":["../src/islands.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,6FAA6F;AAC7F,2FAA2F;AAC3F,4FAA4F;AAC5F,6FAA6F;AAK7F,0FAA0F;AAC1F,MAAM,CAAC,MAAM,gBAAgB,GAAG,oBAAoB,CAAC;AACrD,MAAM,KAAK,GAAG,iBAAiB,CAAC;AAEhC,2FAA2F;AAC3F,MAAM,kBAAkB,GAAG,CAAC,aAAa,EAAE,aAAa,EAAE,SAAS,EAAE,YAAY,CAAU,CAAC;AAE5F,2EAA2E;AAC3E,MAAM,UAAU,UAAU,CAAC,IAAa,EAAE,QAAqC;IAC7E,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,MAAM;QAAE,OAAO;IAC1D,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC7B,IAAI,CAAC,YAAY,CAAC,gBAAgB,EAAE,QAAQ,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,SAAS,GAAG,IAAI,OAAO,EAAW,CAAC;AAIzC,0FAA0F;AAC1F,SAAS,QAAQ,CAAC,GAAe;IAC/B,MAAM,IAAI,GAAI,UAAqD,CAAC,mBAAmB,CAAC;IACxF,IAAI,IAAI,KAAK,SAAS;QAAE,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;;QACxC,IAAI,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;AACpC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,IAAiB;IAC9C,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,gBAAgB,CAAC,CAAC;IACrD,IAAI,QAAQ,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO;IAClF,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACpB,MAAM,OAAO,GAAG,GAAS,EAAE;QACzB,IAAI,CAAC,eAAe,CAAC,gBAAgB,CAAC,CAAC;QACvC,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,wCAAwC;IACvE,CAAC,CAAC;IACF,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;QACxB,QAAQ,CAAC,OAAO,CAAC,CAAC;IACpB,CAAC;SAAM,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAClC,MAAM,QAAQ,GAAG,IAAI,oBAAoB,CAAC,CAAC,OAAO,EAAE,EAAE;YACpD,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,EAAE,CAAC;gBAClD,QAAQ,CAAC,UAAU,EAAE,CAAC;gBACtB,OAAO,EAAE,CAAC;YACZ,CAAC;QACH,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC;SAAM,CAAC;QACN,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,IAAI,GAAG,GAAS,EAAE;YACtB,UAAU,CAAC,KAAK,EAAE,CAAC;YACnB,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC;QACF,KAAK,MAAM,IAAI,IAAI,kBAAkB,EAAE,CAAC;YACtC,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;QAClF,CAAC;IACH,CAAC;AACH,CAAC","sourcesContent":["// Lazy hydration islands (gyral-4k7.4, ADR 0012 addendum). A server-rendered component may\n// hydrate later than page load: when the browser is idle, when it scrolls into view, or when\n// the user first reaches for it. The server marks it with Lit's `defer-hydration` plus the\n// strategy; the client removes `defer-hydration` when the strategy fires, and Lit's hydrate\n// support hydrates it in place (define() finishes connecting from attributeChangedCallback).\n\n/** When a server-rendered component hydrates. `load` (the default) means right away. */\nexport type HydrateStrategy = 'load' | 'idle' | 'visible' | 'interaction';\n\n/** Server-written strategy for a deferred island; removed when the island is released. */\nexport const ISLAND_ATTRIBUTE = 'data-gyral-hydrate';\nconst DEFER = 'defer-hydration';\n\n/** Events that mean the user is about to interact: they arrive before the click itself. */\nconst INTERACTION_EVENTS = ['pointerover', 'pointerdown', 'focusin', 'touchstart'] as const;\n\n/** Server side: marks the host as an island unless it hydrates on load. */\nexport function markIsland(host: Element, strategy: HydrateStrategy | undefined): void {\n if (strategy === undefined || strategy === 'load') return;\n host.setAttribute(DEFER, '');\n host.setAttribute(ISLAND_ATTRIBUTE, strategy);\n}\n\nconst scheduled = new WeakSet<Element>();\n\ntype IdleCallback = (callback: () => void, options?: { timeout: number }) => number;\n\n/** requestIdleCallback is not Baseline widely available; fall back to a short timeout. */\nfunction whenIdle(run: () => void): void {\n const idle = (globalThis as { requestIdleCallback?: IdleCallback }).requestIdleCallback;\n if (idle === undefined) setTimeout(run, 200);\n else idle(run, { timeout: 2000 });\n}\n\n/**\n * Client side: if `host` is a deferred island, releases it when its strategy fires. Called on\n * connect; scheduling happens once per element.\n */\nexport function scheduleIsland(host: HTMLElement): void {\n const strategy = host.getAttribute(ISLAND_ATTRIBUTE);\n if (strategy === null || !host.hasAttribute(DEFER) || scheduled.has(host)) return;\n scheduled.add(host);\n const release = (): void => {\n host.removeAttribute(ISLAND_ATTRIBUTE);\n host.removeAttribute(DEFER); // Lit's hydrate support hydrates it now\n };\n if (strategy === 'idle') {\n whenIdle(release);\n } else if (strategy === 'visible') {\n const observer = new IntersectionObserver((entries) => {\n if (entries.some((entry) => entry.isIntersecting)) {\n observer.disconnect();\n release();\n }\n });\n observer.observe(host);\n } else {\n const controller = new AbortController();\n const once = (): void => {\n controller.abort();\n release();\n };\n for (const type of INTERACTION_EVENTS) {\n host.addEventListener(type, once, { capture: true, signal: controller.signal });\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first place where `value` would change across a JSON round trip, as `path: reason`, or
|
|
3
|
+
* `undefined` when it is JSON-safe. `undefined` object properties are allowed (they vanish, and
|
|
4
|
+
* reading them still gives `undefined`).
|
|
5
|
+
*/
|
|
6
|
+
export declare function jsonHazard(value: unknown, path: string, seen?: Set<object>): string | undefined;
|
|
7
|
+
/** Warns (server side) when a seed would hydrate differently from what was rendered. */
|
|
8
|
+
export declare function warnJsonHazard(owner: string, value: unknown, path: string): void;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Seeds are JSON (ADR 0012). A value that doesn't survive JSON.stringify → JSON.parse unchanged
|
|
2
|
+
// makes the client hydrate from different state than the server rendered (gyral-4k7.5).
|
|
3
|
+
const plain = (value) => {
|
|
4
|
+
const proto = Object.getPrototypeOf(value);
|
|
5
|
+
return proto === Object.prototype || proto === null;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* The first place where `value` would change across a JSON round trip, as `path: reason`, or
|
|
9
|
+
* `undefined` when it is JSON-safe. `undefined` object properties are allowed (they vanish, and
|
|
10
|
+
* reading them still gives `undefined`).
|
|
11
|
+
*/
|
|
12
|
+
export function jsonHazard(value, path, seen = new Set()) {
|
|
13
|
+
switch (typeof value) {
|
|
14
|
+
case 'string':
|
|
15
|
+
case 'boolean':
|
|
16
|
+
return undefined;
|
|
17
|
+
case 'number':
|
|
18
|
+
return Number.isFinite(value) ? undefined : `${path} is ${String(value)} (becomes null)`;
|
|
19
|
+
case 'undefined':
|
|
20
|
+
return `${path} is undefined (dropped, or null in an array)`;
|
|
21
|
+
case 'function':
|
|
22
|
+
case 'symbol':
|
|
23
|
+
return `${path} is a ${typeof value} (dropped)`;
|
|
24
|
+
case 'bigint':
|
|
25
|
+
return `${path} is a bigint (JSON can't encode it)`;
|
|
26
|
+
case 'object':
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
if (value === null)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (seen.has(value))
|
|
32
|
+
return `${path} is a circular reference`;
|
|
33
|
+
if (value instanceof Date)
|
|
34
|
+
return `${path} is a Date (becomes a string)`;
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
seen.add(value);
|
|
37
|
+
for (const [index, item] of value.entries()) {
|
|
38
|
+
const found = jsonHazard(item, `${path}[${String(index)}]`, seen);
|
|
39
|
+
if (found !== undefined)
|
|
40
|
+
return found;
|
|
41
|
+
}
|
|
42
|
+
seen.delete(value);
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
if (!plain(value)) {
|
|
46
|
+
const name = value.constructor?.name ?? 'object';
|
|
47
|
+
return `${path} is a ${name} (becomes a plain object)`;
|
|
48
|
+
}
|
|
49
|
+
seen.add(value);
|
|
50
|
+
for (const [key, item] of Object.entries(value)) {
|
|
51
|
+
if (item === undefined)
|
|
52
|
+
continue;
|
|
53
|
+
const found = jsonHazard(item, `${path}.${key}`, seen);
|
|
54
|
+
if (found !== undefined)
|
|
55
|
+
return found;
|
|
56
|
+
}
|
|
57
|
+
seen.delete(value);
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
/** Warns (server side) when a seed would hydrate differently from what was rendered. */
|
|
61
|
+
export function warnJsonHazard(owner, value, path) {
|
|
62
|
+
const found = jsonHazard(value, path);
|
|
63
|
+
if (found !== undefined) {
|
|
64
|
+
console.warn(`${owner}: ${found}, so the client would hydrate from different data than the ` +
|
|
65
|
+
'server rendered. Keep seeded state and props JSON-safe (ADR 0012).');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=json-safety.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-safety.js","sourceRoot":"","sources":["../src/json-safety.ts"],"names":[],"mappings":"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","sourcesContent":["// Seeds are JSON (ADR 0012). A value that doesn't survive JSON.stringify → JSON.parse unchanged\n// makes the client hydrate from different state than the server rendered (gyral-4k7.5).\n\nconst plain = (value: object): boolean => {\n const proto: unknown = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n};\n\n/**\n * The first place where `value` would change across a JSON round trip, as `path: reason`, or\n * `undefined` when it is JSON-safe. `undefined` object properties are allowed (they vanish, and\n * reading them still gives `undefined`).\n */\nexport function jsonHazard(\n value: unknown,\n path: string,\n seen = new Set<object>(),\n): string | undefined {\n switch (typeof value) {\n case 'string':\n case 'boolean':\n return undefined;\n case 'number':\n return Number.isFinite(value) ? undefined : `${path} is ${String(value)} (becomes null)`;\n case 'undefined':\n return `${path} is undefined (dropped, or null in an array)`;\n case 'function':\n case 'symbol':\n return `${path} is a ${typeof value} (dropped)`;\n case 'bigint':\n return `${path} is a bigint (JSON can't encode it)`;\n case 'object':\n break;\n }\n if (value === null) return undefined;\n if (seen.has(value)) return `${path} is a circular reference`;\n if (value instanceof Date) return `${path} is a Date (becomes a string)`;\n if (Array.isArray(value)) {\n seen.add(value);\n for (const [index, item] of value.entries()) {\n const found = jsonHazard(item, `${path}[${String(index)}]`, seen);\n if (found !== undefined) return found;\n }\n seen.delete(value);\n return undefined;\n }\n if (!plain(value)) {\n const name = (value.constructor as { name?: string } | undefined)?.name ?? 'object';\n return `${path} is a ${name} (becomes a plain object)`;\n }\n seen.add(value);\n for (const [key, item] of Object.entries(value)) {\n if (item === undefined) continue;\n const found = jsonHazard(item, `${path}.${key}`, seen);\n if (found !== undefined) return found;\n }\n seen.delete(value);\n return undefined;\n}\n\n/** Warns (server side) when a seed would hydrate differently from what was rendered. */\nexport function warnJsonHazard(owner: string, value: unknown, path: string): void {\n const found = jsonHazard(value, path);\n if (found !== undefined) {\n console.warn(\n `${owner}: ${found}, so the client would hydrate from different data than the ` +\n 'server rendered. Keep seeded state and props JSON-safe (ADR 0012).',\n );\n }\n}\n"]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type CSSResultGroup, type CSSResultOrNative } from 'lit';
|
|
2
|
+
import { type Styles } from './styles.js';
|
|
3
|
+
interface SpecLike {
|
|
4
|
+
readonly shadow?: boolean;
|
|
5
|
+
readonly styles?: Styles;
|
|
6
|
+
}
|
|
7
|
+
export declare const isLight: (spec: SpecLike) => boolean;
|
|
8
|
+
/** Shadow components keep their styles; light ones use the document's (and warn if given). */
|
|
9
|
+
export declare function componentStyles(spec: SpecLike, tag: string): CSSResultGroup;
|
|
10
|
+
/**
|
|
11
|
+
* Is this class a light-DOM Gyral component? Used by @gyral/ssr to render its view as plain
|
|
12
|
+
* children instead of Declarative Shadow DOM.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isLightComponent(ctor: unknown): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Server-rendered light hosts carry this attribute (ADR 0014 addendum). Hydration uses it to
|
|
17
|
+
* tell a host's own markers from those of light hosts nested inside it.
|
|
18
|
+
*/
|
|
19
|
+
export declare const LIGHT_ATTRIBUTE = "data-gyral-light";
|
|
20
|
+
/** Prefix @gyral/ssr puts on Lit's markers inside light views so no hydrate() walk sees them. */
|
|
21
|
+
export declare const HIDDEN_MARKER = "gyral:";
|
|
22
|
+
/** Server side: marks a light host so the client can scope its markers. */
|
|
23
|
+
export declare function markLightHost(host: Element): void;
|
|
24
|
+
/**
|
|
25
|
+
* Client side, on a server-rendered light host's first update: turns the host's own hidden
|
|
26
|
+
* markers back into Lit's, leaving nested light hosts' markers hidden until they hydrate.
|
|
27
|
+
* Returns whether the host's root part marker was found (so Lit can hydrate in place).
|
|
28
|
+
*/
|
|
29
|
+
export declare function revealLightMarkers(host: Element): boolean;
|
|
30
|
+
/** Lit's public `hydrate()` from `@lit-labs/ssr-client`. */
|
|
31
|
+
export type Hydrate = (value: unknown, container: HTMLElement | DocumentFragment, options?: object) => void;
|
|
32
|
+
/**
|
|
33
|
+
* Where `@gyral/ssr/hydrate` registers Lit's public `hydrate()` (gyral-czi.38). A global
|
|
34
|
+
* symbol, not an import, so core doesn't depend on `@lit-labs/ssr-client` and apps without
|
|
35
|
+
* SSR don't ship it.
|
|
36
|
+
*/
|
|
37
|
+
export declare const HYDRATE_KEY: unique symbol;
|
|
38
|
+
/**
|
|
39
|
+
* Lit's `hydrate()`, if a server-rendered app loaded `@gyral/ssr/hydrate`. Used for light hosts
|
|
40
|
+
* and, since gyral-czi.41, for server-rendered shadow roots too: Gyral hydrates both itself, so
|
|
41
|
+
* it never depends on Lit's hydrate support having patched LitElement before Lit evaluated.
|
|
42
|
+
*/
|
|
43
|
+
export declare const hydrator: () => Hydrate | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* First update of a server-rendered (declarative) shadow root: hydrate the server's view in
|
|
46
|
+
* place with Lit's public hydrate(), which leaves the root part on the shadow root so the
|
|
47
|
+
* render() inside LitElement's update() updates it instead of appending a second copy
|
|
48
|
+
* (gyral-czi.41). Without `@gyral/ssr/hydrate` loaded, clear the server's view and let Lit
|
|
49
|
+
* render fresh, adopting the styles Lit's own createRenderRoot would have.
|
|
50
|
+
*/
|
|
51
|
+
export declare function hydrateShadow(root: ShadowRoot, view: unknown, options: object, styles: CSSResultOrNative[]): void;
|
|
52
|
+
/**
|
|
53
|
+
* First update of a server-rendered light host: reveal this host's own hidden markers now (not
|
|
54
|
+
* on connect: a deferred child connects mid-way through its parent's hydrate walk), then call
|
|
55
|
+
* Lit's public hydrate(). It leaves the root part on the host, so the render() inside
|
|
56
|
+
* LitElement's update() updates it in place instead of appending a second copy. No Lit private
|
|
57
|
+
* fields: their names are mangled in Lit's production build (ADR 0014, gyral-czi.38). Without
|
|
58
|
+
* `@gyral/ssr/hydrate` loaded, clear the server's view and let Lit render fresh.
|
|
59
|
+
*/
|
|
60
|
+
export declare function hydrateLight(host: HTMLElement, view: unknown, options: object): void;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Light-DOM render mode (docs/design-docs/0014-light-dom.md).
|
|
2
|
+
import { adoptStyles } from 'lit';
|
|
3
|
+
import { toCssResultGroup } from './styles.js';
|
|
4
|
+
export const isLight = (spec) => spec.shadow === false;
|
|
5
|
+
/** Shadow components keep their styles; light ones use the document's (and warn if given). */
|
|
6
|
+
export function componentStyles(spec, tag) {
|
|
7
|
+
if (!isLight(spec))
|
|
8
|
+
return spec.styles === undefined ? [] : toCssResultGroup(spec.styles);
|
|
9
|
+
if (spec.styles !== undefined) {
|
|
10
|
+
console.warn(`<${tag}> has shadow: false, so its styles are ignored; use document CSS.`);
|
|
11
|
+
}
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Is this class a light-DOM Gyral component? Used by @gyral/ssr to render its view as plain
|
|
16
|
+
* children instead of Declarative Shadow DOM.
|
|
17
|
+
*/
|
|
18
|
+
export function isLightComponent(ctor) {
|
|
19
|
+
const spec = ctor?.spec;
|
|
20
|
+
return spec !== undefined && isLight(spec);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Server-rendered light hosts carry this attribute (ADR 0014 addendum). Hydration uses it to
|
|
24
|
+
* tell a host's own markers from those of light hosts nested inside it.
|
|
25
|
+
*/
|
|
26
|
+
export const LIGHT_ATTRIBUTE = 'data-gyral-light';
|
|
27
|
+
/** Prefix @gyral/ssr puts on Lit's markers inside light views so no hydrate() walk sees them. */
|
|
28
|
+
export const HIDDEN_MARKER = 'gyral:';
|
|
29
|
+
const LIT_MARKER = /^(?:lit-part|\/lit-part|lit-node)\b/;
|
|
30
|
+
/** Server side: marks a light host so the client can scope its markers. */
|
|
31
|
+
export function markLightHost(host) {
|
|
32
|
+
host.setAttribute(LIGHT_ATTRIBUTE, '');
|
|
33
|
+
}
|
|
34
|
+
/** Is the comment inside `host`'s own light view, not a nested light host's? */
|
|
35
|
+
function ownedBy(comment, host) {
|
|
36
|
+
for (let el = comment.parentElement; el !== null; el = el.parentElement) {
|
|
37
|
+
if (el === host)
|
|
38
|
+
return true;
|
|
39
|
+
if (el.hasAttribute(LIGHT_ATTRIBUTE))
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Client side, on a server-rendered light host's first update: turns the host's own hidden
|
|
46
|
+
* markers back into Lit's, leaving nested light hosts' markers hidden until they hydrate.
|
|
47
|
+
* Returns whether the host's root part marker was found (so Lit can hydrate in place).
|
|
48
|
+
*/
|
|
49
|
+
export function revealLightMarkers(host) {
|
|
50
|
+
const walker = document.createTreeWalker(host, NodeFilter.SHOW_COMMENT);
|
|
51
|
+
const own = [];
|
|
52
|
+
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
|
|
53
|
+
const comment = node;
|
|
54
|
+
const data = comment.data.slice(HIDDEN_MARKER.length);
|
|
55
|
+
if (comment.data.startsWith(HIDDEN_MARKER) && LIT_MARKER.test(data) && ownedBy(comment, host)) {
|
|
56
|
+
own.push(comment);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const comment of own)
|
|
60
|
+
comment.data = comment.data.slice(HIDDEN_MARKER.length);
|
|
61
|
+
return own[0]?.data.startsWith('lit-part') === true;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Where `@gyral/ssr/hydrate` registers Lit's public `hydrate()` (gyral-czi.38). A global
|
|
65
|
+
* symbol, not an import, so core doesn't depend on `@lit-labs/ssr-client` and apps without
|
|
66
|
+
* SSR don't ship it.
|
|
67
|
+
*/
|
|
68
|
+
export const HYDRATE_KEY = Symbol.for('gyral.hydrate');
|
|
69
|
+
/**
|
|
70
|
+
* Lit's `hydrate()`, if a server-rendered app loaded `@gyral/ssr/hydrate`. Used for light hosts
|
|
71
|
+
* and, since gyral-czi.41, for server-rendered shadow roots too: Gyral hydrates both itself, so
|
|
72
|
+
* it never depends on Lit's hydrate support having patched LitElement before Lit evaluated.
|
|
73
|
+
*/
|
|
74
|
+
export const hydrator = () => {
|
|
75
|
+
const fn = globalThis[HYDRATE_KEY];
|
|
76
|
+
return typeof fn === 'function' ? fn : undefined;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* First update of a server-rendered (declarative) shadow root: hydrate the server's view in
|
|
80
|
+
* place with Lit's public hydrate(), which leaves the root part on the shadow root so the
|
|
81
|
+
* render() inside LitElement's update() updates it instead of appending a second copy
|
|
82
|
+
* (gyral-czi.41). Without `@gyral/ssr/hydrate` loaded, clear the server's view and let Lit
|
|
83
|
+
* render fresh, adopting the styles Lit's own createRenderRoot would have.
|
|
84
|
+
*/
|
|
85
|
+
export function hydrateShadow(root, view, options, styles) {
|
|
86
|
+
const hydrate = hydrator();
|
|
87
|
+
if (hydrate !== undefined) {
|
|
88
|
+
hydrate(view, root, options);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
root.replaceChildren();
|
|
92
|
+
adoptStyles(root, styles);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* First update of a server-rendered light host: reveal this host's own hidden markers now (not
|
|
96
|
+
* on connect: a deferred child connects mid-way through its parent's hydrate walk), then call
|
|
97
|
+
* Lit's public hydrate(). It leaves the root part on the host, so the render() inside
|
|
98
|
+
* LitElement's update() updates it in place instead of appending a second copy. No Lit private
|
|
99
|
+
* fields: their names are mangled in Lit's production build (ADR 0014, gyral-czi.38). Without
|
|
100
|
+
* `@gyral/ssr/hydrate` loaded, clear the server's view and let Lit render fresh.
|
|
101
|
+
*/
|
|
102
|
+
export function hydrateLight(host, view, options) {
|
|
103
|
+
const hydrate = hydrator();
|
|
104
|
+
if (hydrate !== undefined && revealLightMarkers(host))
|
|
105
|
+
hydrate(view, host, options);
|
|
106
|
+
else
|
|
107
|
+
host.replaceChildren();
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=light-dom.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"light-dom.js","sourceRoot":"","sources":["../src/light-dom.ts"],"names":[],"mappings":"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","sourcesContent":["// Light-DOM render mode (docs/design-docs/0014-light-dom.md).\nimport { adoptStyles, type CSSResultGroup, type CSSResultOrNative } from 'lit';\nimport { toCssResultGroup, type Styles } from './styles.js';\n\ninterface SpecLike {\n readonly shadow?: boolean;\n readonly styles?: Styles;\n}\n\nexport const isLight = (spec: SpecLike): boolean => spec.shadow === false;\n\n/** Shadow components keep their styles; light ones use the document's (and warn if given). */\nexport function componentStyles(spec: SpecLike, tag: string): CSSResultGroup {\n if (!isLight(spec)) return spec.styles === undefined ? [] : toCssResultGroup(spec.styles);\n if (spec.styles !== undefined) {\n console.warn(`<${tag}> has shadow: false, so its styles are ignored; use document CSS.`);\n }\n return [];\n}\n\n/**\n * Is this class a light-DOM Gyral component? Used by @gyral/ssr to render its view as plain\n * children instead of Declarative Shadow DOM.\n */\nexport function isLightComponent(ctor: unknown): boolean {\n const spec = (ctor as { readonly spec?: SpecLike } | null)?.spec;\n return spec !== undefined && isLight(spec);\n}\n\n/**\n * Server-rendered light hosts carry this attribute (ADR 0014 addendum). Hydration uses it to\n * tell a host's own markers from those of light hosts nested inside it.\n */\nexport const LIGHT_ATTRIBUTE = 'data-gyral-light';\n\n/** Prefix @gyral/ssr puts on Lit's markers inside light views so no hydrate() walk sees them. */\nexport const HIDDEN_MARKER = 'gyral:';\n\nconst LIT_MARKER = /^(?:lit-part|\\/lit-part|lit-node)\\b/;\n\n/** Server side: marks a light host so the client can scope its markers. */\nexport function markLightHost(host: Element): void {\n host.setAttribute(LIGHT_ATTRIBUTE, '');\n}\n\n/** Is the comment inside `host`'s own light view, not a nested light host's? */\nfunction ownedBy(comment: Comment, host: Element): boolean {\n for (let el = comment.parentElement; el !== null; el = el.parentElement) {\n if (el === host) return true;\n if (el.hasAttribute(LIGHT_ATTRIBUTE)) return false;\n }\n return false;\n}\n\n/**\n * Client side, on a server-rendered light host's first update: turns the host's own hidden\n * markers back into Lit's, leaving nested light hosts' markers hidden until they hydrate.\n * Returns whether the host's root part marker was found (so Lit can hydrate in place).\n */\nexport function revealLightMarkers(host: Element): boolean {\n const walker = document.createTreeWalker(host, NodeFilter.SHOW_COMMENT);\n const own: Comment[] = [];\n for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {\n const comment = node as Comment;\n const data = comment.data.slice(HIDDEN_MARKER.length);\n if (comment.data.startsWith(HIDDEN_MARKER) && LIT_MARKER.test(data) && ownedBy(comment, host)) {\n own.push(comment);\n }\n }\n for (const comment of own) comment.data = comment.data.slice(HIDDEN_MARKER.length);\n return own[0]?.data.startsWith('lit-part') === true;\n}\n\n/** Lit's public `hydrate()` from `@lit-labs/ssr-client`. */\nexport type Hydrate = (\n value: unknown,\n container: HTMLElement | DocumentFragment,\n options?: object,\n) => void;\n\n/**\n * Where `@gyral/ssr/hydrate` registers Lit's public `hydrate()` (gyral-czi.38). A global\n * symbol, not an import, so core doesn't depend on `@lit-labs/ssr-client` and apps without\n * SSR don't ship it.\n */\nexport const HYDRATE_KEY: unique symbol = Symbol.for('gyral.hydrate') as never;\n\n/**\n * Lit's `hydrate()`, if a server-rendered app loaded `@gyral/ssr/hydrate`. Used for light hosts\n * and, since gyral-czi.41, for server-rendered shadow roots too: Gyral hydrates both itself, so\n * it never depends on Lit's hydrate support having patched LitElement before Lit evaluated.\n */\nexport const hydrator = (): Hydrate | undefined => {\n const fn = (globalThis as { [HYDRATE_KEY]?: unknown })[HYDRATE_KEY];\n return typeof fn === 'function' ? (fn as Hydrate) : undefined;\n};\n\n/**\n * First update of a server-rendered (declarative) shadow root: hydrate the server's view in\n * place with Lit's public hydrate(), which leaves the root part on the shadow root so the\n * render() inside LitElement's update() updates it instead of appending a second copy\n * (gyral-czi.41). Without `@gyral/ssr/hydrate` loaded, clear the server's view and let Lit\n * render fresh, adopting the styles Lit's own createRenderRoot would have.\n */\nexport function hydrateShadow(\n root: ShadowRoot,\n view: unknown,\n options: object,\n styles: CSSResultOrNative[],\n): void {\n const hydrate = hydrator();\n if (hydrate !== undefined) {\n hydrate(view, root, options);\n return;\n }\n root.replaceChildren();\n adoptStyles(root, styles);\n}\n\n/**\n * First update of a server-rendered light host: reveal this host's own hidden markers now (not\n * on connect: a deferred child connects mid-way through its parent's hydrate walk), then call\n * Lit's public hydrate(). It leaves the root part on the host, so the render() inside\n * LitElement's update() updates it in place instead of appending a second copy. No Lit private\n * fields: their names are mangled in Lit's production build (ADR 0014, gyral-czi.38). Without\n * `@gyral/ssr/hydrate` loaded, clear the server's view and let Lit render fresh.\n */\nexport function hydrateLight(host: HTMLElement, view: unknown, options: object): void {\n const hydrate = hydrator();\n if (hydrate !== undefined && revealLightMarkers(host)) hydrate(view, host, options);\n else host.replaceChildren();\n}\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The loaded lit-html versions that have the repeat() leak. */
|
|
2
|
+
export declare function leakingLitHtml(versions: readonly string[]): string[];
|
|
3
|
+
/** Warns once per page when a leaking lit-html is loaded. */
|
|
4
|
+
export declare function warnLeakingLitHtmlOnce(): void;
|
|
5
|
+
/** Test hook: allow the warning again. */
|
|
6
|
+
export declare function resetLitHtmlWarningForTests(): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Development-only warning for the lit-html >= 3.3.1 repeat() leak (gyral-9y6, ADR 0015,
|
|
2
|
+
// lit/lit#5298): removing repeat() items leaves one comment node per item in the DOM.
|
|
3
|
+
// lit-html records every loaded copy's version in `globalThis.litHtmlVersions`. Called from
|
|
4
|
+
// devtools.ts, which production builds replace, so this costs nothing in production.
|
|
5
|
+
/** First lit-html release with the leak. Raise the upper bound once upstream ships a fix. */
|
|
6
|
+
const FIRST_LEAKING = [3, 3, 1];
|
|
7
|
+
function parse(version) {
|
|
8
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
|
|
9
|
+
return match === null ? undefined : [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
10
|
+
}
|
|
11
|
+
function atLeast(version, floor) {
|
|
12
|
+
for (let i = 0; i < floor.length; i++) {
|
|
13
|
+
const a = version[i] ?? 0;
|
|
14
|
+
const b = floor[i] ?? 0;
|
|
15
|
+
if (a !== b)
|
|
16
|
+
return a > b;
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
/** The loaded lit-html versions that have the repeat() leak. */
|
|
21
|
+
export function leakingLitHtml(versions) {
|
|
22
|
+
return versions.filter((v) => {
|
|
23
|
+
const parsed = parse(v);
|
|
24
|
+
return parsed !== undefined && parsed[0] === 3 && atLeast(parsed, FIRST_LEAKING);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
let warned = false;
|
|
28
|
+
/** Warns once per page when a leaking lit-html is loaded. */
|
|
29
|
+
export function warnLeakingLitHtmlOnce() {
|
|
30
|
+
if (warned)
|
|
31
|
+
return;
|
|
32
|
+
warned = true;
|
|
33
|
+
const versions = globalThis.litHtmlVersions;
|
|
34
|
+
if (!Array.isArray(versions))
|
|
35
|
+
return;
|
|
36
|
+
const leaking = leakingLitHtml(versions.filter((v) => typeof v === 'string'));
|
|
37
|
+
if (leaking.length === 0)
|
|
38
|
+
return;
|
|
39
|
+
console.warn(`gyral: lit-html ${leaking.join(', ')} leaks one DOM comment per removed repeat() item ` +
|
|
40
|
+
'(https://github.com/lit/lit/issues/5298). Pin lit-html to 3.3.0 with an override: ' +
|
|
41
|
+
'https://github.com/gyraljs/gyral/blob/main/docs/references/consumer-setup.md' +
|
|
42
|
+
'#known-issue-lit-html-331-list-leak (development builds only).');
|
|
43
|
+
}
|
|
44
|
+
/** Test hook: allow the warning again. */
|
|
45
|
+
export function resetLitHtmlWarningForTests() {
|
|
46
|
+
warned = false;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=lit-html-check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lit-html-check.js","sourceRoot":"","sources":["../src/lit-html-check.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,sFAAsF;AACtF,4FAA4F;AAC5F,qFAAqF;AAErF,6FAA6F;AAC7F,MAAM,aAAa,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAU,CAAC;AAEzC,SAAS,KAAK,CAAC,OAAe;IAC5B,MAAM,KAAK,GAAG,sBAAsB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACnD,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC7F,CAAC;AAED,SAAS,OAAO,CAAC,OAA0B,EAAE,KAAwB;IACnE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC1B,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACxB,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,cAAc,CAAC,QAA2B;IACxD,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;QAC3B,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACxB,OAAO,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACnF,CAAC,CAAC,CAAC;AACL,CAAC;AAED,IAAI,MAAM,GAAG,KAAK,CAAC;AAEnB,6DAA6D;AAC7D,MAAM,UAAU,sBAAsB;IACpC,IAAI,MAAM;QAAE,OAAO;IACnB,MAAM,GAAG,IAAI,CAAC;IACd,MAAM,QAAQ,GAAI,UAA4C,CAAC,eAAe,CAAC;IAC/E,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC;QAAE,OAAO;IACrC,MAAM,OAAO,GAAG,cAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC;IAC3F,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IACjC,OAAO,CAAC,IAAI,CACV,mBAAmB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,mDAAmD;QACtF,oFAAoF;QACpF,8EAA8E;QAC9E,gEAAgE,CACnE,CAAC;AACJ,CAAC;AAED,0CAA0C;AAC1C,MAAM,UAAU,2BAA2B;IACzC,MAAM,GAAG,KAAK,CAAC;AACjB,CAAC","sourcesContent":["// Development-only warning for the lit-html >= 3.3.1 repeat() leak (gyral-9y6, ADR 0015,\n// lit/lit#5298): removing repeat() items leaves one comment node per item in the DOM.\n// lit-html records every loaded copy's version in `globalThis.litHtmlVersions`. Called from\n// devtools.ts, which production builds replace, so this costs nothing in production.\n\n/** First lit-html release with the leak. Raise the upper bound once upstream ships a fix. */\nconst FIRST_LEAKING = [3, 3, 1] as const;\n\nfunction parse(version: string): readonly number[] | undefined {\n const match = /^(\\d+)\\.(\\d+)\\.(\\d+)/.exec(version);\n return match === null ? undefined : [Number(match[1]), Number(match[2]), Number(match[3])];\n}\n\nfunction atLeast(version: readonly number[], floor: readonly number[]): boolean {\n for (let i = 0; i < floor.length; i++) {\n const a = version[i] ?? 0;\n const b = floor[i] ?? 0;\n if (a !== b) return a > b;\n }\n return true;\n}\n\n/** The loaded lit-html versions that have the repeat() leak. */\nexport function leakingLitHtml(versions: readonly string[]): string[] {\n return versions.filter((v) => {\n const parsed = parse(v);\n return parsed !== undefined && parsed[0] === 3 && atLeast(parsed, FIRST_LEAKING);\n });\n}\n\nlet warned = false;\n\n/** Warns once per page when a leaking lit-html is loaded. */\nexport function warnLeakingLitHtmlOnce(): void {\n if (warned) return;\n warned = true;\n const versions = (globalThis as { litHtmlVersions?: unknown }).litHtmlVersions;\n if (!Array.isArray(versions)) return;\n const leaking = leakingLitHtml(versions.filter((v): v is string => typeof v === 'string'));\n if (leaking.length === 0) return;\n console.warn(\n `gyral: lit-html ${leaking.join(', ')} leaks one DOM comment per removed repeat() item ` +\n '(https://github.com/lit/lit/issues/5298). Pin lit-html to 3.3.0 with an override: ' +\n 'https://github.com/gyraljs/gyral/blob/main/docs/references/consumer-setup.md' +\n '#known-issue-lit-html-331-list-leak (development builds only).',\n );\n}\n\n/** Test hook: allow the warning again. */\nexport function resetLitHtmlWarningForTests(): void {\n warned = false;\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Directive, type AttributePart, type PartInfo } from 'lit/directive.js';
|
|
2
|
+
/**
|
|
3
|
+
* A boolean that is right both in server-rendered markup and in the live DOM (ADR 0012).
|
|
4
|
+
* Use it in a boolean attribute binding:
|
|
5
|
+
*
|
|
6
|
+
* <input type="checkbox" ?checked=${liveBoolean(s.on)} />
|
|
7
|
+
* <option ?selected=${liveBoolean(s.sort === 'price')}>Price</option>
|
|
8
|
+
*
|
|
9
|
+
* - On the server it renders the attribute when true and omits it when false.
|
|
10
|
+
* (A property binding, `.checked=${false}`, is serialized by Lit SSR as `checked="false"`,
|
|
11
|
+
* which *checks* the box. Never bind boolean form state as a property in SSR'd views.)
|
|
12
|
+
* - In the browser it also sets the element's property of the same name (`checked`,
|
|
13
|
+
* `selected`, `open`, …), so the control follows model state even after the user has
|
|
14
|
+
* changed it. The attribute alone stops controlling `checked` once a box has been clicked.
|
|
15
|
+
*/
|
|
16
|
+
declare class LiveBoolean extends Directive {
|
|
17
|
+
constructor(info: PartInfo);
|
|
18
|
+
render(value: boolean): boolean;
|
|
19
|
+
update(part: AttributePart, [value]: [boolean]): boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare const liveBoolean: (value: boolean) => import("lit/directive.js").DirectiveResult<typeof LiveBoolean>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Directive, directive, PartType, } from 'lit/directive.js';
|
|
2
|
+
/**
|
|
3
|
+
* A boolean that is right both in server-rendered markup and in the live DOM (ADR 0012).
|
|
4
|
+
* Use it in a boolean attribute binding:
|
|
5
|
+
*
|
|
6
|
+
* <input type="checkbox" ?checked=${liveBoolean(s.on)} />
|
|
7
|
+
* <option ?selected=${liveBoolean(s.sort === 'price')}>Price</option>
|
|
8
|
+
*
|
|
9
|
+
* - On the server it renders the attribute when true and omits it when false.
|
|
10
|
+
* (A property binding, `.checked=${false}`, is serialized by Lit SSR as `checked="false"`,
|
|
11
|
+
* which *checks* the box. Never bind boolean form state as a property in SSR'd views.)
|
|
12
|
+
* - In the browser it also sets the element's property of the same name (`checked`,
|
|
13
|
+
* `selected`, `open`, …), so the control follows model state even after the user has
|
|
14
|
+
* changed it. The attribute alone stops controlling `checked` once a box has been clicked.
|
|
15
|
+
*/
|
|
16
|
+
class LiveBoolean extends Directive {
|
|
17
|
+
constructor(info) {
|
|
18
|
+
super(info);
|
|
19
|
+
if (info.type !== PartType.BOOLEAN_ATTRIBUTE) {
|
|
20
|
+
throw new Error('liveBoolean() must be used in a boolean attribute: ?checked=${…}');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
render(value) {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
update(part, [value]) {
|
|
27
|
+
const element = part.element;
|
|
28
|
+
if (part.name in part.element && element[part.name] !== value)
|
|
29
|
+
element[part.name] = value;
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export const liveBoolean = directive(LiveBoolean);
|
|
34
|
+
//# sourceMappingURL=live-boolean.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live-boolean.js","sourceRoot":"","sources":["../src/live-boolean.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,SAAS,EACT,QAAQ,GAGT,MAAM,kBAAkB,CAAC;AAE1B;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAY,SAAQ,SAAS;IACjC,YAAY,IAAc;QACxB,KAAK,CAAC,IAAI,CAAC,CAAC;QACZ,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,iBAAiB,EAAE,CAAC;YAC7C,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;QACtF,CAAC;IACH,CAAC;IAEQ,MAAM,CAAC,KAAc;QAC5B,OAAO,KAAK,CAAC;IACf,CAAC;IAEQ,MAAM,CAAC,IAAmB,EAAE,CAAC,KAAK,CAAY;QACrD,MAAM,OAAO,GAAG,IAAI,CAAC,OAA6C,CAAC;QACnE,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,KAAK;YAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QAC1F,OAAO,KAAK,CAAC;IACf,CAAC;CACF;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,SAAS,CAAC,WAAW,CAAC,CAAC","sourcesContent":["import {\n Directive,\n directive,\n PartType,\n type AttributePart,\n type PartInfo,\n} from 'lit/directive.js';\n\n/**\n * A boolean that is right both in server-rendered markup and in the live DOM (ADR 0012).\n * Use it in a boolean attribute binding:\n *\n * <input type=\"checkbox\" ?checked=${liveBoolean(s.on)} />\n * <option ?selected=${liveBoolean(s.sort === 'price')}>Price</option>\n *\n * - On the server it renders the attribute when true and omits it when false.\n * (A property binding, `.checked=${false}`, is serialized by Lit SSR as `checked=\"false\"`,\n * which *checks* the box. Never bind boolean form state as a property in SSR'd views.)\n * - In the browser it also sets the element's property of the same name (`checked`,\n * `selected`, `open`, …), so the control follows model state even after the user has\n * changed it. The attribute alone stops controlling `checked` once a box has been clicked.\n */\nclass LiveBoolean extends Directive {\n constructor(info: PartInfo) {\n super(info);\n if (info.type !== PartType.BOOLEAN_ATTRIBUTE) {\n throw new Error('liveBoolean() must be used in a boolean attribute: ?checked=${…}');\n }\n }\n\n override render(value: boolean): boolean {\n return value;\n }\n\n override update(part: AttributePart, [value]: [boolean]): boolean {\n const element = part.element as unknown as Record<string, unknown>;\n if (part.name in part.element && element[part.name] !== value) element[part.name] = value;\n return value;\n }\n}\n\nexport const liveBoolean = directive(LiveBoolean);\n"]}
|