@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.
Files changed (187) hide show
  1. package/README.md +30 -28
  2. package/components/Link.jsx +43 -0
  3. package/core/errors.js +48 -0
  4. package/core/reactive.js +190 -0
  5. package/core/signals.js +207 -0
  6. package/index.js +13 -0
  7. package/package.json +27 -18
  8. package/runtime/clipboard.js +47 -0
  9. package/runtime/code-block.js +47 -0
  10. package/runtime/context.js +99 -0
  11. package/runtime/dom.js +368 -0
  12. package/runtime/error-boundary.js +109 -0
  13. package/runtime/events.js +17 -0
  14. package/runtime/hydrate.js +121 -0
  15. package/runtime/index.js +22 -0
  16. package/runtime/lifecycle.js +9 -0
  17. package/runtime/mount.js +49 -0
  18. package/runtime/portal.js +59 -0
  19. package/runtime/raw-html.js +33 -0
  20. package/runtime/router.js +392 -0
  21. package/server/builtins.js +16 -0
  22. package/server/head.js +191 -0
  23. package/server/index.js +8 -0
  24. package/server/render.js +98 -0
  25. package/server/ssg-runtime.js +112 -0
  26. package/dist/CHANGELOG.md +0 -49
  27. package/dist/README.md +0 -41
  28. package/dist/babel.config.cjs +0 -6
  29. package/dist/compiler/babel-plugin.cjs +0 -798
  30. package/dist/core/mount.js +0 -32
  31. package/dist/index.js +0 -7
  32. package/dist/node_modules/@babel/core/LICENSE +0 -22
  33. package/dist/node_modules/@babel/core/README.md +0 -19
  34. package/dist/node_modules/@babel/core/lib/config/cache-contexts.js +0 -5
  35. package/dist/node_modules/@babel/core/lib/config/cache-contexts.js.map +0 -1
  36. package/dist/node_modules/@babel/core/lib/config/caching.js +0 -261
  37. package/dist/node_modules/@babel/core/lib/config/caching.js.map +0 -1
  38. package/dist/node_modules/@babel/core/lib/config/config-chain.js +0 -469
  39. package/dist/node_modules/@babel/core/lib/config/config-chain.js.map +0 -1
  40. package/dist/node_modules/@babel/core/lib/config/config-descriptors.js +0 -190
  41. package/dist/node_modules/@babel/core/lib/config/config-descriptors.js.map +0 -1
  42. package/dist/node_modules/@babel/core/lib/config/files/configuration.js +0 -290
  43. package/dist/node_modules/@babel/core/lib/config/files/configuration.js.map +0 -1
  44. package/dist/node_modules/@babel/core/lib/config/files/import.cjs +0 -6
  45. package/dist/node_modules/@babel/core/lib/config/files/import.cjs.map +0 -1
  46. package/dist/node_modules/@babel/core/lib/config/files/index-browser.js +0 -58
  47. package/dist/node_modules/@babel/core/lib/config/files/index-browser.js.map +0 -1
  48. package/dist/node_modules/@babel/core/lib/config/files/index.js +0 -78
  49. package/dist/node_modules/@babel/core/lib/config/files/index.js.map +0 -1
  50. package/dist/node_modules/@babel/core/lib/config/files/module-types.js +0 -203
  51. package/dist/node_modules/@babel/core/lib/config/files/module-types.js.map +0 -1
  52. package/dist/node_modules/@babel/core/lib/config/files/package.js +0 -61
  53. package/dist/node_modules/@babel/core/lib/config/files/package.js.map +0 -1
  54. package/dist/node_modules/@babel/core/lib/config/files/plugins.js +0 -220
  55. package/dist/node_modules/@babel/core/lib/config/files/plugins.js.map +0 -1
  56. package/dist/node_modules/@babel/core/lib/config/files/types.js +0 -5
  57. package/dist/node_modules/@babel/core/lib/config/files/types.js.map +0 -1
  58. package/dist/node_modules/@babel/core/lib/config/files/utils.js +0 -36
  59. package/dist/node_modules/@babel/core/lib/config/files/utils.js.map +0 -1
  60. package/dist/node_modules/@babel/core/lib/config/full.js +0 -312
  61. package/dist/node_modules/@babel/core/lib/config/full.js.map +0 -1
  62. package/dist/node_modules/@babel/core/lib/config/helpers/config-api.js +0 -85
  63. package/dist/node_modules/@babel/core/lib/config/helpers/config-api.js.map +0 -1
  64. package/dist/node_modules/@babel/core/lib/config/helpers/deep-array.js +0 -23
  65. package/dist/node_modules/@babel/core/lib/config/helpers/deep-array.js.map +0 -1
  66. package/dist/node_modules/@babel/core/lib/config/helpers/environment.js +0 -12
  67. package/dist/node_modules/@babel/core/lib/config/helpers/environment.js.map +0 -1
  68. package/dist/node_modules/@babel/core/lib/config/index.js +0 -87
  69. package/dist/node_modules/@babel/core/lib/config/index.js.map +0 -1
  70. package/dist/node_modules/@babel/core/lib/config/item.js +0 -67
  71. package/dist/node_modules/@babel/core/lib/config/item.js.map +0 -1
  72. package/dist/node_modules/@babel/core/lib/config/partial.js +0 -158
  73. package/dist/node_modules/@babel/core/lib/config/partial.js.map +0 -1
  74. package/dist/node_modules/@babel/core/lib/config/pattern-to-regex.js +0 -38
  75. package/dist/node_modules/@babel/core/lib/config/pattern-to-regex.js.map +0 -1
  76. package/dist/node_modules/@babel/core/lib/config/plugin.js +0 -33
  77. package/dist/node_modules/@babel/core/lib/config/plugin.js.map +0 -1
  78. package/dist/node_modules/@babel/core/lib/config/printer.js +0 -113
  79. package/dist/node_modules/@babel/core/lib/config/printer.js.map +0 -1
  80. package/dist/node_modules/@babel/core/lib/config/resolve-targets-browser.js +0 -41
  81. package/dist/node_modules/@babel/core/lib/config/resolve-targets-browser.js.map +0 -1
  82. package/dist/node_modules/@babel/core/lib/config/resolve-targets.js +0 -61
  83. package/dist/node_modules/@babel/core/lib/config/resolve-targets.js.map +0 -1
  84. package/dist/node_modules/@babel/core/lib/config/util.js +0 -31
  85. package/dist/node_modules/@babel/core/lib/config/util.js.map +0 -1
  86. package/dist/node_modules/@babel/core/lib/config/validation/option-assertions.js +0 -277
  87. package/dist/node_modules/@babel/core/lib/config/validation/option-assertions.js.map +0 -1
  88. package/dist/node_modules/@babel/core/lib/config/validation/options.js +0 -187
  89. package/dist/node_modules/@babel/core/lib/config/validation/options.js.map +0 -1
  90. package/dist/node_modules/@babel/core/lib/config/validation/plugins.js +0 -67
  91. package/dist/node_modules/@babel/core/lib/config/validation/plugins.js.map +0 -1
  92. package/dist/node_modules/@babel/core/lib/config/validation/removed.js +0 -68
  93. package/dist/node_modules/@babel/core/lib/config/validation/removed.js.map +0 -1
  94. package/dist/node_modules/@babel/core/lib/errors/config-error.js +0 -18
  95. package/dist/node_modules/@babel/core/lib/errors/config-error.js.map +0 -1
  96. package/dist/node_modules/@babel/core/lib/errors/rewrite-stack-trace.js +0 -98
  97. package/dist/node_modules/@babel/core/lib/errors/rewrite-stack-trace.js.map +0 -1
  98. package/dist/node_modules/@babel/core/lib/gensync-utils/async.js +0 -90
  99. package/dist/node_modules/@babel/core/lib/gensync-utils/async.js.map +0 -1
  100. package/dist/node_modules/@babel/core/lib/gensync-utils/fs.js +0 -31
  101. package/dist/node_modules/@babel/core/lib/gensync-utils/fs.js.map +0 -1
  102. package/dist/node_modules/@babel/core/lib/gensync-utils/functional.js +0 -58
  103. package/dist/node_modules/@babel/core/lib/gensync-utils/functional.js.map +0 -1
  104. package/dist/node_modules/@babel/core/lib/index.js +0 -230
  105. package/dist/node_modules/@babel/core/lib/index.js.map +0 -1
  106. package/dist/node_modules/@babel/core/lib/parse.js +0 -45
  107. package/dist/node_modules/@babel/core/lib/parse.js.map +0 -1
  108. package/dist/node_modules/@babel/core/lib/parser/index.js +0 -85
  109. package/dist/node_modules/@babel/core/lib/parser/index.js.map +0 -1
  110. package/dist/node_modules/@babel/core/lib/parser/util/missing-plugin-helper.js +0 -337
  111. package/dist/node_modules/@babel/core/lib/parser/util/missing-plugin-helper.js.map +0 -1
  112. package/dist/node_modules/@babel/core/lib/tools/build-external-helpers.js +0 -144
  113. package/dist/node_modules/@babel/core/lib/tools/build-external-helpers.js.map +0 -1
  114. package/dist/node_modules/@babel/core/lib/transform-ast.js +0 -48
  115. package/dist/node_modules/@babel/core/lib/transform-ast.js.map +0 -1
  116. package/dist/node_modules/@babel/core/lib/transform-file-browser.js +0 -23
  117. package/dist/node_modules/@babel/core/lib/transform-file-browser.js.map +0 -1
  118. package/dist/node_modules/@babel/core/lib/transform-file.js +0 -40
  119. package/dist/node_modules/@babel/core/lib/transform-file.js.map +0 -1
  120. package/dist/node_modules/@babel/core/lib/transform.js +0 -47
  121. package/dist/node_modules/@babel/core/lib/transform.js.map +0 -1
  122. package/dist/node_modules/@babel/core/lib/transformation/block-hoist-plugin.js +0 -84
  123. package/dist/node_modules/@babel/core/lib/transformation/block-hoist-plugin.js.map +0 -1
  124. package/dist/node_modules/@babel/core/lib/transformation/file/babel-7-helpers.cjs +0 -4
  125. package/dist/node_modules/@babel/core/lib/transformation/file/babel-7-helpers.cjs.map +0 -1
  126. package/dist/node_modules/@babel/core/lib/transformation/file/file.js +0 -204
  127. package/dist/node_modules/@babel/core/lib/transformation/file/file.js.map +0 -1
  128. package/dist/node_modules/@babel/core/lib/transformation/file/generate.js +0 -84
  129. package/dist/node_modules/@babel/core/lib/transformation/file/generate.js.map +0 -1
  130. package/dist/node_modules/@babel/core/lib/transformation/file/merge-map.js +0 -37
  131. package/dist/node_modules/@babel/core/lib/transformation/file/merge-map.js.map +0 -1
  132. package/dist/node_modules/@babel/core/lib/transformation/index.js +0 -90
  133. package/dist/node_modules/@babel/core/lib/transformation/index.js.map +0 -1
  134. package/dist/node_modules/@babel/core/lib/transformation/normalize-file.js +0 -127
  135. package/dist/node_modules/@babel/core/lib/transformation/normalize-file.js.map +0 -1
  136. package/dist/node_modules/@babel/core/lib/transformation/normalize-opts.js +0 -59
  137. package/dist/node_modules/@babel/core/lib/transformation/normalize-opts.js.map +0 -1
  138. package/dist/node_modules/@babel/core/lib/transformation/plugin-pass.js +0 -48
  139. package/dist/node_modules/@babel/core/lib/transformation/plugin-pass.js.map +0 -1
  140. package/dist/node_modules/@babel/core/lib/transformation/util/clone-deep.js +0 -54
  141. package/dist/node_modules/@babel/core/lib/transformation/util/clone-deep.js.map +0 -1
  142. package/dist/node_modules/@babel/core/lib/vendor/import-meta-resolve.js +0 -1042
  143. package/dist/node_modules/@babel/core/lib/vendor/import-meta-resolve.js.map +0 -1
  144. package/dist/node_modules/@babel/core/package.json +0 -82
  145. package/dist/node_modules/@babel/core/src/config/files/index-browser.ts +0 -115
  146. package/dist/node_modules/@babel/core/src/config/files/index.ts +0 -30
  147. package/dist/node_modules/@babel/core/src/config/resolve-targets-browser.ts +0 -42
  148. package/dist/node_modules/@babel/core/src/config/resolve-targets.ts +0 -54
  149. package/dist/node_modules/@babel/core/src/transform-file-browser.ts +0 -33
  150. package/dist/node_modules/@babel/core/src/transform-file.ts +0 -56
  151. package/dist/node_modules/@babel/helper-module-imports/LICENSE +0 -22
  152. package/dist/node_modules/@babel/helper-module-imports/README.md +0 -19
  153. package/dist/node_modules/@babel/helper-module-imports/lib/import-builder.js +0 -122
  154. package/dist/node_modules/@babel/helper-module-imports/lib/import-builder.js.map +0 -1
  155. package/dist/node_modules/@babel/helper-module-imports/lib/import-injector.js +0 -304
  156. package/dist/node_modules/@babel/helper-module-imports/lib/import-injector.js.map +0 -1
  157. package/dist/node_modules/@babel/helper-module-imports/lib/index.js +0 -37
  158. package/dist/node_modules/@babel/helper-module-imports/lib/index.js.map +0 -1
  159. package/dist/node_modules/@babel/helper-module-imports/lib/is-module.js +0 -11
  160. package/dist/node_modules/@babel/helper-module-imports/lib/is-module.js.map +0 -1
  161. package/dist/node_modules/@babel/helper-module-imports/package.json +0 -28
  162. package/dist/node_modules/@babel/plugin-syntax-jsx/LICENSE +0 -22
  163. package/dist/node_modules/@babel/plugin-syntax-jsx/README.md +0 -19
  164. package/dist/node_modules/@babel/plugin-syntax-jsx/lib/index.js +0 -21
  165. package/dist/node_modules/@babel/plugin-syntax-jsx/lib/index.js.map +0 -1
  166. package/dist/node_modules/@babel/plugin-syntax-jsx/package.json +0 -33
  167. package/dist/node_modules/@preact/signals-core/CHANGELOG.md +0 -331
  168. package/dist/node_modules/@preact/signals-core/LICENSE +0 -21
  169. package/dist/node_modules/@preact/signals-core/README.md +0 -258
  170. package/dist/node_modules/@preact/signals-core/dist/signals-core.d.ts +0 -149
  171. package/dist/node_modules/@preact/signals-core/dist/signals-core.js +0 -1
  172. package/dist/node_modules/@preact/signals-core/dist/signals-core.js.map +0 -1
  173. package/dist/node_modules/@preact/signals-core/dist/signals-core.min.js +0 -1
  174. package/dist/node_modules/@preact/signals-core/dist/signals-core.min.js.map +0 -1
  175. package/dist/node_modules/@preact/signals-core/dist/signals-core.mjs +0 -1
  176. package/dist/node_modules/@preact/signals-core/dist/signals-core.mjs.map +0 -1
  177. package/dist/node_modules/@preact/signals-core/dist/signals-core.module.js +0 -1
  178. package/dist/node_modules/@preact/signals-core/dist/signals-core.module.js.map +0 -1
  179. package/dist/node_modules/@preact/signals-core/package.json +0 -48
  180. package/dist/node_modules/@preact/signals-core/src/index.ts +0 -1121
  181. package/dist/package.json +0 -30
  182. package/dist/router/Link.js +0 -73
  183. package/dist/router/index.js +0 -216
  184. package/dist/runtime/For.js +0 -16
  185. package/dist/runtime/dom.js +0 -73
  186. package/dist/runtime/lifecycle.js +0 -25
  187. 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
+ }