@opentf/web 0.4.0 → 0.6.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/README.md +30 -28
- package/components/Link.jsx +43 -0
- package/core/errors.js +48 -0
- package/core/reactive.js +190 -0
- package/core/signals.js +207 -0
- package/index.js +13 -0
- package/package.json +27 -18
- package/runtime/clipboard.js +47 -0
- package/runtime/code-block.js +47 -0
- package/runtime/context.js +99 -0
- package/runtime/dom.js +368 -0
- package/runtime/error-boundary.js +109 -0
- package/runtime/events.js +17 -0
- package/runtime/hydrate.js +121 -0
- package/runtime/index.js +22 -0
- package/runtime/lifecycle.js +9 -0
- package/runtime/mount.js +49 -0
- package/runtime/portal.js +59 -0
- package/runtime/raw-html.js +33 -0
- package/runtime/router.js +392 -0
- package/server/builtins.js +16 -0
- package/server/head.js +191 -0
- package/server/index.js +8 -0
- package/server/render.js +98 -0
- package/server/ssg-runtime.js +112 -0
- package/dist/CHANGELOG.md +0 -49
- package/dist/README.md +0 -41
- package/dist/babel.config.cjs +0 -6
- package/dist/compiler/babel-plugin.cjs +0 -798
- package/dist/core/mount.js +0 -32
- package/dist/index.js +0 -7
- package/dist/node_modules/@babel/core/LICENSE +0 -22
- package/dist/node_modules/@babel/core/README.md +0 -19
- package/dist/node_modules/@babel/core/lib/config/cache-contexts.js +0 -5
- package/dist/node_modules/@babel/core/lib/config/cache-contexts.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/caching.js +0 -261
- package/dist/node_modules/@babel/core/lib/config/caching.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/config-chain.js +0 -469
- package/dist/node_modules/@babel/core/lib/config/config-chain.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/config-descriptors.js +0 -190
- package/dist/node_modules/@babel/core/lib/config/config-descriptors.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/files/configuration.js +0 -290
- package/dist/node_modules/@babel/core/lib/config/files/configuration.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/files/import.cjs +0 -6
- package/dist/node_modules/@babel/core/lib/config/files/import.cjs.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/files/index-browser.js +0 -58
- package/dist/node_modules/@babel/core/lib/config/files/index-browser.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/files/index.js +0 -78
- package/dist/node_modules/@babel/core/lib/config/files/index.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/files/module-types.js +0 -203
- package/dist/node_modules/@babel/core/lib/config/files/module-types.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/files/package.js +0 -61
- package/dist/node_modules/@babel/core/lib/config/files/package.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/files/plugins.js +0 -220
- package/dist/node_modules/@babel/core/lib/config/files/plugins.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/files/types.js +0 -5
- package/dist/node_modules/@babel/core/lib/config/files/types.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/files/utils.js +0 -36
- package/dist/node_modules/@babel/core/lib/config/files/utils.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/full.js +0 -312
- package/dist/node_modules/@babel/core/lib/config/full.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/helpers/config-api.js +0 -85
- package/dist/node_modules/@babel/core/lib/config/helpers/config-api.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/helpers/deep-array.js +0 -23
- package/dist/node_modules/@babel/core/lib/config/helpers/deep-array.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/helpers/environment.js +0 -12
- package/dist/node_modules/@babel/core/lib/config/helpers/environment.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/index.js +0 -87
- package/dist/node_modules/@babel/core/lib/config/index.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/item.js +0 -67
- package/dist/node_modules/@babel/core/lib/config/item.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/partial.js +0 -158
- package/dist/node_modules/@babel/core/lib/config/partial.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/pattern-to-regex.js +0 -38
- package/dist/node_modules/@babel/core/lib/config/pattern-to-regex.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/plugin.js +0 -33
- package/dist/node_modules/@babel/core/lib/config/plugin.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/printer.js +0 -113
- package/dist/node_modules/@babel/core/lib/config/printer.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/resolve-targets-browser.js +0 -41
- package/dist/node_modules/@babel/core/lib/config/resolve-targets-browser.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/resolve-targets.js +0 -61
- package/dist/node_modules/@babel/core/lib/config/resolve-targets.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/util.js +0 -31
- package/dist/node_modules/@babel/core/lib/config/util.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/validation/option-assertions.js +0 -277
- package/dist/node_modules/@babel/core/lib/config/validation/option-assertions.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/validation/options.js +0 -187
- package/dist/node_modules/@babel/core/lib/config/validation/options.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/validation/plugins.js +0 -67
- package/dist/node_modules/@babel/core/lib/config/validation/plugins.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/validation/removed.js +0 -68
- package/dist/node_modules/@babel/core/lib/config/validation/removed.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/errors/config-error.js +0 -18
- package/dist/node_modules/@babel/core/lib/errors/config-error.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/errors/rewrite-stack-trace.js +0 -98
- package/dist/node_modules/@babel/core/lib/errors/rewrite-stack-trace.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/gensync-utils/async.js +0 -90
- package/dist/node_modules/@babel/core/lib/gensync-utils/async.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/gensync-utils/fs.js +0 -31
- package/dist/node_modules/@babel/core/lib/gensync-utils/fs.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/gensync-utils/functional.js +0 -58
- package/dist/node_modules/@babel/core/lib/gensync-utils/functional.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/index.js +0 -230
- package/dist/node_modules/@babel/core/lib/index.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/parse.js +0 -45
- package/dist/node_modules/@babel/core/lib/parse.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/parser/index.js +0 -85
- package/dist/node_modules/@babel/core/lib/parser/index.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/parser/util/missing-plugin-helper.js +0 -337
- package/dist/node_modules/@babel/core/lib/parser/util/missing-plugin-helper.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/tools/build-external-helpers.js +0 -144
- package/dist/node_modules/@babel/core/lib/tools/build-external-helpers.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transform-ast.js +0 -48
- package/dist/node_modules/@babel/core/lib/transform-ast.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transform-file-browser.js +0 -23
- package/dist/node_modules/@babel/core/lib/transform-file-browser.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transform-file.js +0 -40
- package/dist/node_modules/@babel/core/lib/transform-file.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transform.js +0 -47
- package/dist/node_modules/@babel/core/lib/transform.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transformation/block-hoist-plugin.js +0 -84
- package/dist/node_modules/@babel/core/lib/transformation/block-hoist-plugin.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transformation/file/babel-7-helpers.cjs +0 -4
- package/dist/node_modules/@babel/core/lib/transformation/file/babel-7-helpers.cjs.map +0 -1
- package/dist/node_modules/@babel/core/lib/transformation/file/file.js +0 -204
- package/dist/node_modules/@babel/core/lib/transformation/file/file.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transformation/file/generate.js +0 -84
- package/dist/node_modules/@babel/core/lib/transformation/file/generate.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transformation/file/merge-map.js +0 -37
- package/dist/node_modules/@babel/core/lib/transformation/file/merge-map.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transformation/index.js +0 -90
- package/dist/node_modules/@babel/core/lib/transformation/index.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transformation/normalize-file.js +0 -127
- package/dist/node_modules/@babel/core/lib/transformation/normalize-file.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transformation/normalize-opts.js +0 -59
- package/dist/node_modules/@babel/core/lib/transformation/normalize-opts.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transformation/plugin-pass.js +0 -48
- package/dist/node_modules/@babel/core/lib/transformation/plugin-pass.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transformation/util/clone-deep.js +0 -54
- package/dist/node_modules/@babel/core/lib/transformation/util/clone-deep.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/vendor/import-meta-resolve.js +0 -1042
- package/dist/node_modules/@babel/core/lib/vendor/import-meta-resolve.js.map +0 -1
- package/dist/node_modules/@babel/core/package.json +0 -82
- package/dist/node_modules/@babel/core/src/config/files/index-browser.ts +0 -115
- package/dist/node_modules/@babel/core/src/config/files/index.ts +0 -30
- package/dist/node_modules/@babel/core/src/config/resolve-targets-browser.ts +0 -42
- package/dist/node_modules/@babel/core/src/config/resolve-targets.ts +0 -54
- package/dist/node_modules/@babel/core/src/transform-file-browser.ts +0 -33
- package/dist/node_modules/@babel/core/src/transform-file.ts +0 -56
- package/dist/node_modules/@babel/helper-module-imports/LICENSE +0 -22
- package/dist/node_modules/@babel/helper-module-imports/README.md +0 -19
- package/dist/node_modules/@babel/helper-module-imports/lib/import-builder.js +0 -122
- package/dist/node_modules/@babel/helper-module-imports/lib/import-builder.js.map +0 -1
- package/dist/node_modules/@babel/helper-module-imports/lib/import-injector.js +0 -304
- package/dist/node_modules/@babel/helper-module-imports/lib/import-injector.js.map +0 -1
- package/dist/node_modules/@babel/helper-module-imports/lib/index.js +0 -37
- package/dist/node_modules/@babel/helper-module-imports/lib/index.js.map +0 -1
- package/dist/node_modules/@babel/helper-module-imports/lib/is-module.js +0 -11
- package/dist/node_modules/@babel/helper-module-imports/lib/is-module.js.map +0 -1
- package/dist/node_modules/@babel/helper-module-imports/package.json +0 -28
- package/dist/node_modules/@babel/plugin-syntax-jsx/LICENSE +0 -22
- package/dist/node_modules/@babel/plugin-syntax-jsx/README.md +0 -19
- package/dist/node_modules/@babel/plugin-syntax-jsx/lib/index.js +0 -21
- package/dist/node_modules/@babel/plugin-syntax-jsx/lib/index.js.map +0 -1
- package/dist/node_modules/@babel/plugin-syntax-jsx/package.json +0 -33
- package/dist/node_modules/@preact/signals-core/CHANGELOG.md +0 -331
- package/dist/node_modules/@preact/signals-core/LICENSE +0 -21
- package/dist/node_modules/@preact/signals-core/README.md +0 -258
- package/dist/node_modules/@preact/signals-core/dist/signals-core.d.ts +0 -149
- package/dist/node_modules/@preact/signals-core/dist/signals-core.js +0 -1
- package/dist/node_modules/@preact/signals-core/dist/signals-core.js.map +0 -1
- package/dist/node_modules/@preact/signals-core/dist/signals-core.min.js +0 -1
- package/dist/node_modules/@preact/signals-core/dist/signals-core.min.js.map +0 -1
- package/dist/node_modules/@preact/signals-core/dist/signals-core.mjs +0 -1
- package/dist/node_modules/@preact/signals-core/dist/signals-core.mjs.map +0 -1
- package/dist/node_modules/@preact/signals-core/dist/signals-core.module.js +0 -1
- package/dist/node_modules/@preact/signals-core/dist/signals-core.module.js.map +0 -1
- package/dist/node_modules/@preact/signals-core/package.json +0 -48
- package/dist/node_modules/@preact/signals-core/src/index.ts +0 -1121
- package/dist/package.json +0 -30
- package/dist/router/Link.js +0 -73
- package/dist/router/index.js +0 -216
- package/dist/runtime/For.js +0 -16
- package/dist/runtime/dom.js +0 -73
- package/dist/runtime/lifecycle.js +0 -25
- package/dist/runtime/props.js +0 -103
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//! Code block — a built-in that renders a (build-time syntax-highlighted) code block
|
|
2
|
+
//! and **owns its copy behavior**.
|
|
3
|
+
//
|
|
4
|
+
// <CodeFence html={"<div class='otfw-code'>…<button class='otfw-copy'>…</button><pre>…</pre></div>"} />
|
|
5
|
+
//
|
|
6
|
+
// The MDX front-end emits this for fenced code (compiles to `web-internal-code-block`,
|
|
7
|
+
// like `<RawHtml>` → `web-internal-raw-html`). The highlighted markup is a trusted
|
|
8
|
+
// string from the compiler, so it's assigned to `innerHTML`; the difference from
|
|
9
|
+
// `RawHtml` is that on connect this element wires its own `.otfw-copy` button to copy
|
|
10
|
+
// the `<pre>` text. That keeps copy working wherever a code block is rendered (docs,
|
|
11
|
+
// blog, anywhere) — no delegated listener in a layout. SSG renders the string inline
|
|
12
|
+
// (server/builtins.js); the behavior wires when the element upgrades in the browser.
|
|
13
|
+
|
|
14
|
+
import { copyWithFeedback } from "./clipboard.js";
|
|
15
|
+
|
|
16
|
+
export class CodeBlockElement extends HTMLElement {
|
|
17
|
+
set html(v) {
|
|
18
|
+
this._html = v;
|
|
19
|
+
if (this.isConnected) this.render();
|
|
20
|
+
}
|
|
21
|
+
get html() {
|
|
22
|
+
return this._html;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
connectedCallback() {
|
|
26
|
+
// The `html` property is set before append during a CSR build; render once.
|
|
27
|
+
if (!this._rendered) this.render();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
render() {
|
|
31
|
+
this.innerHTML = this._html == null ? "" : String(this._html);
|
|
32
|
+
const button = this.querySelector(".otfw-copy");
|
|
33
|
+
const pre = this.querySelector("pre");
|
|
34
|
+
if (button && pre && !button._otfwWired) {
|
|
35
|
+
button._otfwWired = true;
|
|
36
|
+
button.addEventListener("click", () => copyWithFeedback(button, pre.innerText));
|
|
37
|
+
}
|
|
38
|
+
this._rendered = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Exported so the bare import in runtime/index.js retains the registration side
|
|
43
|
+
// effect; JSX addresses it by the `web-internal-code-block` tag (no import needed).
|
|
44
|
+
export const CodeFence = CodeBlockElement;
|
|
45
|
+
if (typeof customElements !== "undefined" && !customElements.get("web-internal-code-block")) {
|
|
46
|
+
customElements.define("web-internal-code-block", CodeBlockElement);
|
|
47
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
//! Context API — scoped dependency injection over the component tree.
|
|
2
|
+
//
|
|
3
|
+
// Unlike a module-level signal (which is global) or a prop (which must be drilled),
|
|
4
|
+
// context provides a value to a *subtree* and lets a nested provider override it.
|
|
5
|
+
// Because components compile to Custom Elements, the component tree *is* the DOM
|
|
6
|
+
// tree, so we resolve a consumer's provider with `closest()` — no special graph.
|
|
7
|
+
//
|
|
8
|
+
// const ThemeContext = createContext("dark");
|
|
9
|
+
// <ContextProvider context={ThemeContext} value={theme}> … </ContextProvider>
|
|
10
|
+
// const theme = $context(ThemeContext); // compiler macro; use the bare name
|
|
11
|
+
//
|
|
12
|
+
// `value` flows through the provider's element as a signal, so a reactive `value`
|
|
13
|
+
// (e.g. backed by `$state`) updates every consumer fine-grained.
|
|
14
|
+
|
|
15
|
+
import { signal } from "../core/signals.js";
|
|
16
|
+
|
|
17
|
+
let nextId = 0;
|
|
18
|
+
|
|
19
|
+
// Stack of the elements whose component bodies are currently executing. A stack
|
|
20
|
+
// (not a single slot) because filling a component's children slot connects nested
|
|
21
|
+
// components synchronously inside the parent's connect — each push is balanced by
|
|
22
|
+
// a pop in the generated `finally`, so the parent's host is restored after.
|
|
23
|
+
const hostStack = [];
|
|
24
|
+
|
|
25
|
+
/** Push the current component host (compiler-emitted, paired with `exitHost`). */
|
|
26
|
+
export function enterHost(el) {
|
|
27
|
+
hostStack.push(el);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Pop the current component host (compiler-emitted, in a `finally`). */
|
|
31
|
+
export function exitHost() {
|
|
32
|
+
hostStack.pop();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The component host element currently executing its body, or `null` outside a
|
|
37
|
+
* component (e.g. a form created in module scope or a test). Libraries use this
|
|
38
|
+
* as a per-instance identity to scope state to the mounting component — the
|
|
39
|
+
* compiler emits `enterHost(this)` around every component body (csr.rs).
|
|
40
|
+
*/
|
|
41
|
+
export function getCurrentInstance() {
|
|
42
|
+
return hostStack[hostStack.length - 1] ?? null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create a context with a default value. The returned token is passed to
|
|
47
|
+
* `<ContextProvider context={…}>` and `$context(…)`.
|
|
48
|
+
*/
|
|
49
|
+
export function createContext(defaultValue) {
|
|
50
|
+
return { id: `otfw-ctx-${nextId++}`, fallback: signal(defaultValue) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the value provided by the nearest ancestor `ContextProvider` for
|
|
55
|
+
* `context`, or the context's default if there is none. Returns a **signal**. This
|
|
56
|
+
* is what the `$context(Ctx)` compiler macro lowers to; the macro registers the
|
|
57
|
+
* binding as a signal so the bare name reads `.value` reactively.
|
|
58
|
+
*/
|
|
59
|
+
export function readContext(context) {
|
|
60
|
+
let el = hostStack[hostStack.length - 1];
|
|
61
|
+
const sel = `[data-otfw-ctx~="${context.id}"]`;
|
|
62
|
+
// Walk up the DOM; when the search reaches a portal boundary without finding a
|
|
63
|
+
// provider, hop to the portal's logical location (`__portalHost`) and continue,
|
|
64
|
+
// so context resolves across a <Portal> as if the content were still in place.
|
|
65
|
+
while (el) {
|
|
66
|
+
const provider = el.closest(sel);
|
|
67
|
+
if (provider) return provider._signal;
|
|
68
|
+
const boundary = el.closest("[data-otfw-portal]");
|
|
69
|
+
el = boundary ? boundary.__portalHost : null;
|
|
70
|
+
}
|
|
71
|
+
return context.fallback;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// `<ContextProvider context={Ctx} value={v}>`: a host element that publishes `v`
|
|
75
|
+
// (as a signal) for `Ctx` to its subtree. `context`/`value` arrive as element
|
|
76
|
+
// properties (the compiler sets component props reactively), so a changing `value`
|
|
77
|
+
// propagates to consumers automatically.
|
|
78
|
+
export class ContextProviderElement extends HTMLElement {
|
|
79
|
+
constructor() {
|
|
80
|
+
super();
|
|
81
|
+
this._signal = signal(undefined);
|
|
82
|
+
}
|
|
83
|
+
set context(ctx) {
|
|
84
|
+
if (ctx && ctx.id) this.setAttribute("data-otfw-ctx", ctx.id);
|
|
85
|
+
}
|
|
86
|
+
set value(v) {
|
|
87
|
+
this._signal.value = v;
|
|
88
|
+
}
|
|
89
|
+
get value() {
|
|
90
|
+
return this._signal.value;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Exported so `import { ContextProvider } from "@opentf/web"` resolves; the import
|
|
95
|
+
// side effect registers the element (JSX uses the `web-internal-context-provider` tag).
|
|
96
|
+
export const ContextProvider = ContextProviderElement;
|
|
97
|
+
if (typeof customElements !== "undefined" && !customElements.get("web-internal-context-provider")) {
|
|
98
|
+
customElements.define("web-internal-context-provider", ContextProviderElement);
|
|
99
|
+
}
|
package/runtime/dom.js
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// Zero-VDOM DOM operations. CSR codegen emits calls into these helpers; they
|
|
2
|
+
// also back runtime-driven updates (reactive text/attributes). No virtual DOM,
|
|
3
|
+
// no diffing — just direct, fine-grained DOM writes wired to signals.
|
|
4
|
+
|
|
5
|
+
import { effect, signal } from "../core/signals.js";
|
|
6
|
+
|
|
7
|
+
// Attribute names that must be assigned as JS properties (not setAttribute) for
|
|
8
|
+
// correct behavior. Mirrors the set the old runtime relied on (core/constants).
|
|
9
|
+
const AS_PROPERTY = new Set([
|
|
10
|
+
"value",
|
|
11
|
+
"checked",
|
|
12
|
+
"selected",
|
|
13
|
+
"disabled",
|
|
14
|
+
"readOnly",
|
|
15
|
+
"className",
|
|
16
|
+
"htmlFor",
|
|
17
|
+
"indeterminate",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
// Enumerated attributes: their value is a keyword string ("true"/"false"), not a
|
|
21
|
+
// boolean-attribute presence toggle. `draggable={true}` must become
|
|
22
|
+
// draggable="true" — draggable="" reads back as the default (false). ARIA states
|
|
23
|
+
// behave the same (aria-*="true"/"false").
|
|
24
|
+
const ENUMERATED = new Set(["draggable", "spellcheck", "contenteditable"]);
|
|
25
|
+
|
|
26
|
+
// CSS properties whose numeric values are unitless; every other numeric value
|
|
27
|
+
// gets "px" appended (matching React/JSX conventions, so `style={{ width: 8 }}`
|
|
28
|
+
// means 8px). Mirrors React's CSSProperty unitless list.
|
|
29
|
+
const UNITLESS = new Set([
|
|
30
|
+
"animationIterationCount", "aspectRatio", "borderImageOutset", "borderImageSlice",
|
|
31
|
+
"borderImageWidth", "boxFlex", "boxFlexGroup", "boxOrdinalGroup", "columnCount",
|
|
32
|
+
"columns", "flex", "flexGrow", "flexShrink", "flexOrder", "gridArea", "gridRow",
|
|
33
|
+
"gridRowEnd", "gridRowStart", "gridColumn", "gridColumnEnd", "gridColumnStart",
|
|
34
|
+
"fontWeight", "lineClamp", "lineHeight", "opacity", "order", "orphans", "tabSize",
|
|
35
|
+
"widows", "zIndex", "zoom", "fillOpacity", "floodOpacity", "stopOpacity",
|
|
36
|
+
"strokeDasharray", "strokeDashoffset", "strokeMiterlimit", "strokeOpacity",
|
|
37
|
+
"strokeWidth",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Apply a `style` object to an element. Numbers get "px" appended unless the
|
|
42
|
+
* property is unitless; CSS custom properties (`--foo`) go through setProperty;
|
|
43
|
+
* nullish values clear the property. A string value is treated as raw cssText.
|
|
44
|
+
*/
|
|
45
|
+
export function applyStyle(el, value) {
|
|
46
|
+
if (typeof value === "string") {
|
|
47
|
+
el.style.cssText = value;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
for (const key in value) {
|
|
51
|
+
const v = value[key];
|
|
52
|
+
if (key.startsWith("--")) {
|
|
53
|
+
el.style.setProperty(key, v == null ? "" : String(v));
|
|
54
|
+
} else if (v == null) {
|
|
55
|
+
el.style[key] = "";
|
|
56
|
+
} else {
|
|
57
|
+
el.style[key] = typeof v === "number" && !UNITLESS.has(key) ? `${v}px` : v;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Render a reactive value to its text form: null/undefined/false → "". */
|
|
63
|
+
export function toText(value) {
|
|
64
|
+
return value == null || value === false ? "" : String(value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Sentinel for "nothing written yet" — distinct from any real attribute value. */
|
|
68
|
+
const UNSET = Symbol("unset");
|
|
69
|
+
|
|
70
|
+
/** A value safe to compare by identity for write elision (no internal mutation). */
|
|
71
|
+
const isPrimitive = (v) =>
|
|
72
|
+
v === null || (typeof v !== "object" && typeof v !== "function");
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* clsx-style class normalization. Falsy entries are skipped; strings/numbers are
|
|
76
|
+
* kept; arrays recurse; objects contribute the keys whose values are truthy. Lets
|
|
77
|
+
* JSX write `class={["btn", active && "on", { lg: size === "lg" }]}`.
|
|
78
|
+
*/
|
|
79
|
+
export function clsx(value) {
|
|
80
|
+
if (!value) return "";
|
|
81
|
+
if (typeof value === "string") return value;
|
|
82
|
+
if (typeof value === "number") return String(value);
|
|
83
|
+
if (Array.isArray(value)) {
|
|
84
|
+
const parts = [];
|
|
85
|
+
for (const v of value) {
|
|
86
|
+
const s = clsx(v);
|
|
87
|
+
if (s) parts.push(s);
|
|
88
|
+
}
|
|
89
|
+
return parts.join(" ");
|
|
90
|
+
}
|
|
91
|
+
if (typeof value === "object") {
|
|
92
|
+
const parts = [];
|
|
93
|
+
for (const k in value) if (value[k]) parts.push(k);
|
|
94
|
+
return parts.join(" ");
|
|
95
|
+
}
|
|
96
|
+
return "";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Apply a single static prop/attribute. Handles `style` objects, `on*` event
|
|
101
|
+
* handlers, property-backed attributes, and boolean/nullish removal; everything
|
|
102
|
+
* else falls back to `setAttribute`.
|
|
103
|
+
*/
|
|
104
|
+
export function setAttr(el, name, value) {
|
|
105
|
+
if (name === "style" && value && (typeof value === "object" || typeof value === "string")) {
|
|
106
|
+
applyStyle(el, value);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (name === "class" && value && typeof value === "object") {
|
|
110
|
+
const c = clsx(value); // array/object form → a normalized class string
|
|
111
|
+
if (c) el.setAttribute("class", c);
|
|
112
|
+
else el.removeAttribute("class");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (name.startsWith("on") && typeof value === "function") {
|
|
116
|
+
el[name.toLowerCase()] = value;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (AS_PROPERTY.has(name)) {
|
|
120
|
+
el[name] = value;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (ENUMERATED.has(name) || name.startsWith("aria-")) {
|
|
124
|
+
if (value == null) el.removeAttribute(name);
|
|
125
|
+
else el.setAttribute(name, value === true ? "true" : value === false ? "false" : String(value));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (value == null || value === false) {
|
|
129
|
+
el.removeAttribute(name);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (value === true) {
|
|
133
|
+
el.setAttribute(name, "");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
el.setAttribute(name, String(value));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Apply a named prop to a *child component* element. Set it as a property when the
|
|
141
|
+
* element exposes one (custom-element prop setters, `value`, `checked`, …) so rich
|
|
142
|
+
* values (objects, signals) reach the component's setter; otherwise fall back to an
|
|
143
|
+
* attribute so plain ones (`class`, `data-*`, `aria-*`) still land correctly. This
|
|
144
|
+
* mirrors how JSX libraries target custom elements. If the element is not yet
|
|
145
|
+
* upgraded `name in el` is false, so the value lands as an attribute and the
|
|
146
|
+
* component picks it up from `getAttribute` when it upgrades.
|
|
147
|
+
*/
|
|
148
|
+
export function setProp(el, name, value) {
|
|
149
|
+
if (name in el) el[name] = value;
|
|
150
|
+
else setAttr(el, name, value);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Apply every own enumerable key of `obj` to `el` (`<div {...obj}/>`, SPEC §5.5).
|
|
155
|
+
* `asProps` sets element properties (for components) instead of attributes (for
|
|
156
|
+
* host elements). Keys are applied in object order; the caller wraps this in an
|
|
157
|
+
* effect so a reactive source re-applies.
|
|
158
|
+
*/
|
|
159
|
+
export function spread(el, obj, asProps) {
|
|
160
|
+
if (!obj) return;
|
|
161
|
+
for (const k in obj) {
|
|
162
|
+
if (asProps) el[k] = obj[k];
|
|
163
|
+
else setAttr(el, k, obj[k]);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Wire a text node to a reactive expression. Primitive values update the text in
|
|
169
|
+
* place; a DOM node (or array of them) — e.g. JSX stored as a value, `{iconMap[k]}`
|
|
170
|
+
* — is inserted before the text node, which stays as a stable anchor. Returns the
|
|
171
|
+
* effect disposer so the caller (component lifecycle) can stop updates.
|
|
172
|
+
*/
|
|
173
|
+
export function bindText(node, fn) {
|
|
174
|
+
let inserted = [];
|
|
175
|
+
let lastText; // undefined sentinel — toText() never returns undefined
|
|
176
|
+
return effect(() => {
|
|
177
|
+
const value = fn();
|
|
178
|
+
|
|
179
|
+
// Primitive text: the common case. Skip the write when the rendered string
|
|
180
|
+
// is unchanged (a shared dependency can re-run this effect with the same
|
|
181
|
+
// result), so unchanged text nodes don't churn.
|
|
182
|
+
if (!(value instanceof Node) && !Array.isArray(value)) {
|
|
183
|
+
const text = toText(value);
|
|
184
|
+
if (text === lastText) return;
|
|
185
|
+
lastText = text;
|
|
186
|
+
for (const n of inserted) {
|
|
187
|
+
if (n.parentNode) n.parentNode.removeChild(n);
|
|
188
|
+
}
|
|
189
|
+
inserted = [];
|
|
190
|
+
node.data = text;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Node / array value (e.g. JSX stored as a value): insert before the stable
|
|
195
|
+
// text anchor. Reset the text memo so a later primitive value re-applies.
|
|
196
|
+
lastText = undefined;
|
|
197
|
+
for (const n of inserted) {
|
|
198
|
+
if (n.parentNode) n.parentNode.removeChild(n);
|
|
199
|
+
}
|
|
200
|
+
inserted = [];
|
|
201
|
+
node.data = "";
|
|
202
|
+
const parent = node.parentNode;
|
|
203
|
+
if (parent) {
|
|
204
|
+
for (const n of toNodes(value)) {
|
|
205
|
+
parent.insertBefore(n, node);
|
|
206
|
+
inserted.push(n);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Wire an element attribute/prop to a reactive expression. Returns the disposer.
|
|
214
|
+
*
|
|
215
|
+
* No-op writes are elided for primitive values: when the expression recomputes
|
|
216
|
+
* to the same primitive it last wrote, the DOM write is skipped. This matters
|
|
217
|
+
* when many elements share one dependency — e.g. a keyed list where every row's
|
|
218
|
+
* `class={selected === row.id ? "danger" : ""}` subscribes to a single
|
|
219
|
+
* `selected` signal. Changing it re-runs every row's effect, but only the two
|
|
220
|
+
* rows whose class actually changes should touch the DOM; the rest would
|
|
221
|
+
* otherwise call `setAttribute` with an unchanged value and trigger needless
|
|
222
|
+
* style invalidation. Object values (style/class objects, spreads) may have
|
|
223
|
+
* mutated internally, so they are always re-applied.
|
|
224
|
+
*/
|
|
225
|
+
export function bindAttr(el, name, fn) {
|
|
226
|
+
let last = UNSET;
|
|
227
|
+
return effect(() => {
|
|
228
|
+
const value = fn();
|
|
229
|
+
if (isPrimitive(value) && Object.is(value, last)) return;
|
|
230
|
+
last = value;
|
|
231
|
+
setAttr(el, name, value);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Flatten a reactive child value into DOM nodes: nodes pass through, arrays
|
|
236
|
+
* recurse, nullish/booleans render nothing, primitives become text nodes. */
|
|
237
|
+
function toNodes(value) {
|
|
238
|
+
if (value == null || typeof value === "boolean") return [];
|
|
239
|
+
if (Array.isArray(value)) {
|
|
240
|
+
const out = [];
|
|
241
|
+
for (const v of value) for (const n of toNodes(v)) out.push(n);
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
if (value instanceof Node) return [value];
|
|
245
|
+
return [document.createTextNode(String(value))];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Wire a dynamic child region (conditional/element-valued holes like
|
|
250
|
+
* `{cond && <p/>}` or `{cond ? <a/> : <b/>}`) to a reactive expression. The
|
|
251
|
+
* region is delimited by `anchor` (a comment); on every change the previous
|
|
252
|
+
* nodes are replaced with the new ones, inserted just before the anchor.
|
|
253
|
+
* Returns the effect disposer.
|
|
254
|
+
*/
|
|
255
|
+
export function bindChild(anchor, fn) {
|
|
256
|
+
let current = [];
|
|
257
|
+
return effect(() => {
|
|
258
|
+
const next = toNodes(fn());
|
|
259
|
+
const host = anchor.parentNode;
|
|
260
|
+
for (const n of current) {
|
|
261
|
+
if (n.parentNode === host) host.removeChild(n);
|
|
262
|
+
}
|
|
263
|
+
if (host) for (const n of next) host.insertBefore(n, anchor);
|
|
264
|
+
current = next;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Render a keyed list (`array.map(...)`, SPEC §5.4.4) into `parent`, reconciling
|
|
270
|
+
* by key on every change to `sourceFn`'s dependencies.
|
|
271
|
+
*
|
|
272
|
+
* - `sourceFn()` returns the current array (plain data).
|
|
273
|
+
* - `renderItem(itemSignal, index)` builds a node for a new item; the item's
|
|
274
|
+
* value is a signal so per-item bindings update fine-grained on data changes.
|
|
275
|
+
* - `keyFn(item, index)` returns a stable key; falls back to `index` when omitted.
|
|
276
|
+
*
|
|
277
|
+
* Returns the effect disposer (stops reconciliation; used by component cleanup).
|
|
278
|
+
*/
|
|
279
|
+
export function bindList(parent, sourceFn, renderItem, keyFn) {
|
|
280
|
+
const anchor = document.createComment("");
|
|
281
|
+
parent.appendChild(anchor);
|
|
282
|
+
let cache = new Map(); // key -> { sig, node }
|
|
283
|
+
let prevKeys = []; // keys in current DOM order, for minimal-move reconciliation
|
|
284
|
+
|
|
285
|
+
return effect(() => {
|
|
286
|
+
const data = sourceFn();
|
|
287
|
+
const items = Array.isArray(data) ? data : [];
|
|
288
|
+
const next = new Map();
|
|
289
|
+
const nodes = new Array(items.length);
|
|
290
|
+
const newKeys = new Array(items.length);
|
|
291
|
+
// For each new position, the node's index in the *previous* order, or -1 if
|
|
292
|
+
// it is newly created. Drives the LIS-based minimal move below.
|
|
293
|
+
const prevIndex = new Array(items.length);
|
|
294
|
+
|
|
295
|
+
const prevPos = new Map();
|
|
296
|
+
for (let i = 0; i < prevKeys.length; i++) prevPos.set(prevKeys[i], i);
|
|
297
|
+
|
|
298
|
+
for (let index = 0; index < items.length; index++) {
|
|
299
|
+
const item = items[index];
|
|
300
|
+
const key = keyFn ? keyFn(item, index) : index;
|
|
301
|
+
let entry = cache.get(key);
|
|
302
|
+
if (entry) {
|
|
303
|
+
cache.delete(key);
|
|
304
|
+
entry.sig.value = item; // fine-grained per-item update (no-op if unchanged)
|
|
305
|
+
prevIndex[index] = prevPos.has(key) ? prevPos.get(key) : -1;
|
|
306
|
+
} else {
|
|
307
|
+
const sig = signal(item);
|
|
308
|
+
entry = { sig, node: renderItem(sig, index) };
|
|
309
|
+
prevIndex[index] = -1;
|
|
310
|
+
}
|
|
311
|
+
next.set(key, entry);
|
|
312
|
+
newKeys[index] = key;
|
|
313
|
+
nodes[index] = entry.node;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Remove nodes whose keys disappeared.
|
|
317
|
+
for (const entry of cache.values()) {
|
|
318
|
+
if (entry.node.parentNode) entry.node.parentNode.removeChild(entry.node);
|
|
319
|
+
}
|
|
320
|
+
cache = next;
|
|
321
|
+
prevKeys = newKeys;
|
|
322
|
+
|
|
323
|
+
// Minimal-move placement. Nodes whose previous indices form the longest
|
|
324
|
+
// increasing subsequence are already in relative order and stay put; only
|
|
325
|
+
// the rest (and newly created nodes) are inserted. A 2-row swap moves 2
|
|
326
|
+
// nodes, not the ~n nodes a naive "fix every wrong nextSibling" pass would
|
|
327
|
+
// cascade into. Walk back to front so each insertion's reference node is
|
|
328
|
+
// already in its final position.
|
|
329
|
+
const keep = longestIncreasingRun(prevIndex);
|
|
330
|
+
const host = anchor.parentNode || parent;
|
|
331
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
332
|
+
if (keep.has(i)) continue;
|
|
333
|
+
const ref = i + 1 < nodes.length ? nodes[i + 1] : anchor;
|
|
334
|
+
if (nodes[i].nextSibling !== ref) host.insertBefore(nodes[i], ref);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Indices `i` of `seq` whose values form a longest strictly-increasing
|
|
341
|
+
* subsequence (patience sorting, O(n log n)). Entries equal to -1 (new nodes)
|
|
342
|
+
* are excluded — they always need insertion. Returned as a Set for O(1) lookup
|
|
343
|
+
* during placement; these are the nodes that can stay where they are.
|
|
344
|
+
*/
|
|
345
|
+
function longestIncreasingRun(seq) {
|
|
346
|
+
const piles = []; // piles[k] = index of the smallest tail of a length-(k+1) run
|
|
347
|
+
const prev = new Array(seq.length).fill(-1);
|
|
348
|
+
for (let i = 0; i < seq.length; i++) {
|
|
349
|
+
const v = seq[i];
|
|
350
|
+
if (v < 0) continue; // new node — never part of the stable run
|
|
351
|
+
let lo = 0;
|
|
352
|
+
let hi = piles.length;
|
|
353
|
+
while (lo < hi) {
|
|
354
|
+
const mid = (lo + hi) >> 1;
|
|
355
|
+
if (seq[piles[mid]] < v) lo = mid + 1;
|
|
356
|
+
else hi = mid;
|
|
357
|
+
}
|
|
358
|
+
if (lo > 0) prev[i] = piles[lo - 1];
|
|
359
|
+
piles[lo] = i;
|
|
360
|
+
}
|
|
361
|
+
const keep = new Set();
|
|
362
|
+
let k = piles.length ? piles[piles.length - 1] : -1;
|
|
363
|
+
while (k >= 0) {
|
|
364
|
+
keep.add(k);
|
|
365
|
+
k = prev[k];
|
|
366
|
+
}
|
|
367
|
+
return keep;
|
|
368
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
//! ErrorBoundary — catch render/mount errors from descendant components and show
|
|
2
|
+
//! a fallback instead of letting the broken subtree (or its siblings) take the
|
|
3
|
+
//! page down.
|
|
4
|
+
//
|
|
5
|
+
// <ErrorBoundary fallback={(error, reset) => `Oops: ${error.message}`}>
|
|
6
|
+
// <Risky/>
|
|
7
|
+
// </ErrorBoundary>
|
|
8
|
+
//
|
|
9
|
+
// A descendant component's connectedCallback funnels a thrown error through
|
|
10
|
+
// `handleError(this, …)` (emitted by the compiler): it reports the error (dev
|
|
11
|
+
// overlay / logging) and walks up the DOM — hopping across portals, like
|
|
12
|
+
// `readContext` — to the nearest `web-internal-error-boundary`, which swaps its children
|
|
13
|
+
// for the fallback. `reset()` rebuilds the original subtree (snapshotted before it
|
|
14
|
+
// ran) to retry. Only synchronous render/mount errors are caught; later
|
|
15
|
+
// effect/event errors still go to global reporting (as with React boundaries).
|
|
16
|
+
|
|
17
|
+
import { reportError } from "../core/errors.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Report a caught render/mount error and route it to the nearest enclosing
|
|
21
|
+
* `<ErrorBoundary>`. Called from a throwing component's connectedCallback catch.
|
|
22
|
+
*/
|
|
23
|
+
export function handleError(el, error, context = {}) {
|
|
24
|
+
reportError(error, context); // always log / surface in the dev overlay
|
|
25
|
+
let node = el;
|
|
26
|
+
while (node) {
|
|
27
|
+
const boundary = node.closest ? node.closest("web-internal-error-boundary") : null;
|
|
28
|
+
if (boundary) {
|
|
29
|
+
boundary.catch(error, context);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const portal = node.closest ? node.closest("[data-otfw-portal]") : null;
|
|
33
|
+
node = portal ? portal.__portalHost : null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// A self-contained fallback (inline-styled so it reads fine without any app CSS).
|
|
38
|
+
function defaultFallback(message, reset) {
|
|
39
|
+
const box = document.createElement("div");
|
|
40
|
+
box.setAttribute("role", "alert");
|
|
41
|
+
box.setAttribute("data-otfw-fallback", "");
|
|
42
|
+
box.style.cssText =
|
|
43
|
+
"display:flex;align-items:center;gap:.75rem;padding:.75rem 1rem;border:1px solid #ef4444;" +
|
|
44
|
+
"border-radius:.5rem;background:rgba(239,68,68,.1);color:#fca5a5;font:14px system-ui,sans-serif";
|
|
45
|
+
const text = document.createElement("span");
|
|
46
|
+
text.textContent = message;
|
|
47
|
+
box.appendChild(text);
|
|
48
|
+
const retry = document.createElement("button");
|
|
49
|
+
retry.textContent = "Retry";
|
|
50
|
+
retry.style.cssText =
|
|
51
|
+
"margin-left:auto;padding:.25rem .75rem;border-radius:.375rem;border:none;cursor:pointer;" +
|
|
52
|
+
"background:#ef4444;color:#fff;font:inherit";
|
|
53
|
+
retry.addEventListener("click", reset);
|
|
54
|
+
box.appendChild(retry);
|
|
55
|
+
return box;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class ErrorBoundaryElement extends HTMLElement {
|
|
59
|
+
set fallback(fn) {
|
|
60
|
+
this._fallback = fn;
|
|
61
|
+
}
|
|
62
|
+
get fallback() {
|
|
63
|
+
return this._fallback;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
connectedCallback() {
|
|
67
|
+
if (this._mounted) return;
|
|
68
|
+
this._mounted = true;
|
|
69
|
+
// Snapshot the children before they connect (and possibly throw) so reset()
|
|
70
|
+
// can rebuild a pristine subtree.
|
|
71
|
+
this._blueprint = Array.from(this.childNodes).map((n) => n.cloneNode(true));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Invoked by handleError when a descendant throws during render/mount. Deferred
|
|
75
|
+
// to a microtask so we never mutate the tree mid-connectedCallback (re-entrancy).
|
|
76
|
+
catch(error) {
|
|
77
|
+
if (this._failed) return; // first error wins; ignore the cascade
|
|
78
|
+
this._failed = true;
|
|
79
|
+
queueMicrotask(() => this.#renderFallback(error));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#renderFallback(error) {
|
|
83
|
+
this.replaceChildren(); // tear the broken subtree down (runs disconnect cleanups)
|
|
84
|
+
const reset = () => this.reset();
|
|
85
|
+
let view = null;
|
|
86
|
+
try {
|
|
87
|
+
view = this._fallback ? this._fallback(error, reset) : null;
|
|
88
|
+
} catch (e) {
|
|
89
|
+
reportError(e, { phase: "fallback" });
|
|
90
|
+
}
|
|
91
|
+
const message = typeof view === "string" ? view : (error && error.message) || "Something went wrong";
|
|
92
|
+
this.appendChild(view instanceof Node ? view : defaultFallback(message, reset));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
reset() {
|
|
96
|
+
if (!this._failed) return;
|
|
97
|
+
this._failed = false;
|
|
98
|
+
// Re-clone the blueprint: the rebuilt subtree connects fresh and may throw
|
|
99
|
+
// again (→ catch), or render cleanly if the cause is gone.
|
|
100
|
+
this.replaceChildren(...this._blueprint.map((n) => n.cloneNode(true)));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Exported so `import { ErrorBoundary } from "@opentf/web"` resolves; the import
|
|
105
|
+
// side effect registers the element (JSX uses the `web-internal-error-boundary` tag).
|
|
106
|
+
export const ErrorBoundary = ErrorBoundaryElement;
|
|
107
|
+
if (typeof customElements !== "undefined" && !customElements.get("web-internal-error-boundary")) {
|
|
108
|
+
customElements.define("web-internal-error-boundary", ErrorBoundaryElement);
|
|
109
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//! Events — the dispatch half of the hybrid event model.
|
|
2
|
+
//
|
|
3
|
+
// Two channels (SPEC §5.4):
|
|
4
|
+
// • callback props — `<Picker onSelect={fn}>` sets a property; the component
|
|
5
|
+
// calls `this.onSelect?.(detail)`. Direct parent→child, no bubbling.
|
|
6
|
+
// • event listeners — `<Picker onSelect:listen={fn}>` compiles to
|
|
7
|
+
// `addEventListener("select", fn, opts)` (with `:capture`/`:passive`/`:once`),
|
|
8
|
+
// for bubbling/interop/multiple subscribers. `emit` is the matching dispatch.
|
|
9
|
+
//
|
|
10
|
+
// `emit(el, "select", detail)` fires a CustomEvent that bubbles and crosses shadow
|
|
11
|
+
// boundaries by default, so a `web-*` element is a first-class event source for any
|
|
12
|
+
// framework consuming it. Returns false if a listener called preventDefault().
|
|
13
|
+
|
|
14
|
+
export function emit(el, name, detail, options = {}) {
|
|
15
|
+
const { bubbles = true, composed = true, cancelable = false } = options;
|
|
16
|
+
return el.dispatchEvent(new CustomEvent(name, { detail, bubbles, composed, cancelable }));
|
|
17
|
+
}
|