@unoverse-platform/studio 0.1.102 → 0.1.104

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 (79) hide show
  1. package/package.json +4 -2
  2. package/screens/AtomsView.tsx +2 -1
  3. package/screens/ComponentsView.tsx +2 -1
  4. package/screens/NativeAppHost.tsx +1 -2
  5. package/screens/TemplatesLibraryView.tsx +14 -1
  6. package/screens/TemplatesView.tsx +2 -1
  7. package/screens/index.ts +4 -0
  8. package/screens/react/unoverse.tsx +281 -0
  9. package/vendor/base/src/agent-mcp/index.ts +1 -1
  10. package/vendor/base/src/agent-mcp/invoke.ts +6 -2
  11. package/vendor/base/src/agent-mcp/tools.ts +14 -5
  12. package/vendor/base/src/agent-mcp/types.ts +6 -0
  13. package/vendor/base/src/auth/auth.ts +90 -23
  14. package/vendor/base/src/definitions/load/compile/sections.ts +79 -0
  15. package/vendor/base/src/definitions/load/compile/template.ts +23 -2
  16. package/vendor/base/src/definitions/model.ts +9 -0
  17. package/vendor/base/src/definitions/registry/catalog.ts +6 -7
  18. package/vendor/base/src/executor.ts +25 -1
  19. package/vendor/base/src/manifests/executor/toolBridge.ts +25 -19
  20. package/vendor/base/src/manifests/runtime/channels/final.ts +4 -1
  21. package/vendor/base/src/manifests/runtime/duplex/session.ts +3 -9
  22. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +28 -25
  23. package/vendor/base/src/platform/serviceCalls.ts +13 -0
  24. package/vendor/sdk/appEngine.ts +363 -0
  25. package/vendor/sdk/lib/actions.ts +36 -0
  26. package/vendor/sdk/lib/citations.ts +48 -0
  27. package/vendor/sdk/lib/core/apps.ts +18 -0
  28. package/vendor/sdk/lib/core/client.ts +3 -3
  29. package/vendor/sdk/lib/core/store.ts +6 -0
  30. package/vendor/sdk/lib/dom/app.ts +546 -0
  31. package/vendor/sdk/lib/dom/chrome.ts +125 -0
  32. package/vendor/sdk/lib/dom/component.ts +333 -0
  33. package/vendor/sdk/lib/dom/connection.ts +309 -0
  34. package/vendor/sdk/lib/dom/element.ts +69 -0
  35. package/vendor/sdk/lib/dom/isolate.ts +37 -0
  36. package/vendor/sdk/lib/dom/markdown.ts +388 -0
  37. package/vendor/sdk/lib/dom/markdownParse.ts +440 -0
  38. package/vendor/sdk/lib/dom/mounted.ts +29 -0
  39. package/vendor/sdk/lib/dom/primitives.ts +689 -0
  40. package/vendor/sdk/lib/dom/region.ts +195 -0
  41. package/vendor/sdk/lib/dom/render.ts +177 -0
  42. package/vendor/sdk/lib/dom/theme.ts +38 -0
  43. package/vendor/sdk/lib/dom/types.ts +36 -0
  44. package/vendor/sdk/lib/index.ts +26 -17
  45. package/vendor/sdk/lib/orb/canvas.ts +139 -0
  46. package/vendor/sdk/lib/orb/index.ts +8 -18
  47. package/vendor/sdk/lib/orb/presets.ts +1 -1
  48. package/vendor/sdk/lib/orb/{types.ts → states.ts} +0 -24
  49. package/vendor/sdk/lib/realtime/capture.ts +152 -0
  50. package/vendor/sdk/lib/realtime/playback.ts +129 -0
  51. package/vendor/sdk/lib/realtime/socket.ts +117 -0
  52. package/vendor/sdk/lib/style.ts +45 -37
  53. package/vendor/sdk/lib/voiceService.ts +344 -0
  54. package/vendor/sdk/main.ts +107 -0
  55. package/vendor/sdk/mcpAppBridge.ts +125 -102
  56. package/vendor/sdk/pageInterfaces.ts +452 -0
  57. package/vendor/sdk/responseView.ts +193 -0
  58. package/vite.config.ts +3 -3
  59. package/vendor/sdk/appEngine.tsx +0 -393
  60. package/vendor/sdk/lib/UnoverseComponent.tsx +0 -325
  61. package/vendor/sdk/lib/app.tsx +0 -730
  62. package/vendor/sdk/lib/connection.tsx +0 -433
  63. package/vendor/sdk/lib/isolate.tsx +0 -70
  64. package/vendor/sdk/lib/orb/ThinkingOrb.tsx +0 -131
  65. package/vendor/sdk/lib/orb/theme.ts +0 -83
  66. package/vendor/sdk/lib/primitives.tsx +0 -943
  67. package/vendor/sdk/lib/realtime/useAudioCapture.ts +0 -237
  68. package/vendor/sdk/lib/realtime/useAudioPlayback.ts +0 -157
  69. package/vendor/sdk/lib/realtime/useRealtimeWebSocket.ts +0 -164
  70. package/vendor/sdk/lib/render.tsx +0 -244
  71. package/vendor/sdk/lib/streamed.tsx +0 -30
  72. package/vendor/sdk/lib/theme.ts +0 -55
  73. package/vendor/sdk/lib/transition/MountArrangement.tsx +0 -71
  74. package/vendor/sdk/lib/transition/index.ts +0 -9
  75. package/vendor/sdk/lib/transition/viewTransition.ts +0 -29
  76. package/vendor/sdk/lib/voice.tsx +0 -422
  77. package/vendor/sdk/main.tsx +0 -125
  78. package/vendor/sdk/pageInterfaces.tsx +0 -383
  79. package/vendor/sdk/responseView.tsx +0 -82
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The element layer of the plain-DOM renderer: styles, attributes and children, set in place.
3
+ * No virtual DOM: an element is built once and each update writes only what changed.
4
+ */
5
+ import type { CssStyle } from "../style";
6
+
7
+ /** CSS properties whose plain numbers are not lengths (`zIndex: 2`, never `2px`). */
8
+ const UNITLESS = new Set([
9
+ "animationIterationCount", "aspectRatio", "borderImageOutset", "borderImageSlice", "borderImageWidth",
10
+ "boxFlex", "boxFlexGroup", "boxOrdinalGroup", "columnCount", "columns", "flex", "flexGrow", "flexPositive",
11
+ "flexShrink", "flexNegative", "flexOrder", "gridArea", "gridRow", "gridRowEnd", "gridRowSpan", "gridRowStart",
12
+ "gridColumn", "gridColumnEnd", "gridColumnSpan", "gridColumnStart", "fontWeight", "lineClamp", "lineHeight",
13
+ "opacity", "order", "orphans", "scale", "tabSize", "widows", "zIndex", "zoom", "fillOpacity", "floodOpacity",
14
+ "stopOpacity", "strokeDasharray", "strokeDashoffset", "strokeMiterlimit", "strokeOpacity", "strokeWidth",
15
+ ]);
16
+
17
+ const kebab = (name: string) =>
18
+ name.startsWith("--") ? name : name.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase()).replace(/^ms-/, "-ms-");
19
+
20
+ /** A style value as CSS text, or null when the property is not set. */
21
+ function cssValue(name: string, v: unknown): string | null {
22
+ if (v == null || typeof v === "boolean" || v === "") return null;
23
+ if (typeof v === "number" && v !== 0 && !UNITLESS.has(name) && !name.startsWith("--")) return `${v}px`;
24
+ return String(v).trim();
25
+ }
26
+
27
+ const applied = new WeakMap<Element, Record<string, string>>();
28
+
29
+ /**
30
+ * Set an element's inline style to `next`, touching only the properties that changed. A
31
+ * property this layer never set (a behaviour's `opacity`, a stage's `transform`) is left alone.
32
+ */
33
+ export function setStyle(el: HTMLElement | SVGElement, next: CssStyle | undefined): void {
34
+ const prev = applied.get(el) ?? {};
35
+ const now: Record<string, string> = {};
36
+ for (const [k, v] of Object.entries(next ?? {})) {
37
+ const css = cssValue(k, v);
38
+ if (css != null) now[kebab(k)] = css;
39
+ }
40
+ for (const k of Object.keys(prev)) if (!(k in now)) el.style.removeProperty(k);
41
+ for (const [k, v] of Object.entries(now)) if (prev[k] !== v) el.style.setProperty(k, v);
42
+ applied.set(el, now);
43
+ if (!el.getAttribute("style")) el.removeAttribute("style");
44
+ }
45
+
46
+ /** Set or remove an attribute: absent, null or false removes it, true sets it empty. */
47
+ export function setAttr(el: Element, name: string, v: unknown): void {
48
+ if (v == null || v === false) el.removeAttribute(name);
49
+ else {
50
+ const s = v === true ? "" : String(v);
51
+ if (el.getAttribute(name) !== s) el.setAttribute(name, s);
52
+ }
53
+ }
54
+
55
+ /** Put exactly `nodes` in `parent`, in order, moving only what is out of place. */
56
+ export function setChildren(parent: Node, nodes: Node[]): void {
57
+ let i = 0;
58
+ for (const n of nodes) {
59
+ const at = parent.childNodes[i] ?? null;
60
+ if (at !== n) parent.insertBefore(n, at);
61
+ i++;
62
+ }
63
+ while (parent.childNodes.length > i) parent.removeChild(parent.lastChild!);
64
+ }
65
+
66
+ /** A text node whose content follows the value (strings and numbers; anything else is empty). */
67
+ export function textOf(v: unknown): string {
68
+ return typeof v === "string" ? v : typeof v === "number" || typeof v === "bigint" ? String(v) : "";
69
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * THE SHADOW ROOT: every component and app draws inside its own open shadow root, so the
3
+ * page's styles cannot reach in and ours cannot leak out. The SDK owns the boundary once,
4
+ * for every host.
5
+ *
6
+ * Shadow DOM blocks style SELECTORS but NOT inheritance: inherited properties (font, colour,
7
+ * line-height, letter-spacing) still pierce it, and <button>/<input> fall back to the UA
8
+ * font. `:host { all: initial }` neutralizes the inherited cascade; `display: contents`
9
+ * keeps the host out of layout (the consumer owns the outer box). The container inside then
10
+ * takes the served `theme.root`, the sole typographic baseline.
11
+ */
12
+ export const RESET_CSS = `
13
+ :host { all: initial; display: contents; }
14
+ .uno-isolate-root { box-sizing: border-box; }
15
+ .uno-isolate-root *, .uno-isolate-root *::before, .uno-isolate-root *::after { box-sizing: border-box; }
16
+ .uno-isolate-root button, .uno-isolate-root input, .uno-isolate-root textarea, .uno-isolate-root select {
17
+ font: inherit; letter-spacing: inherit; color: inherit; margin: 0;
18
+ }
19
+ /* Scrollbars hidden by default across every scroll container — scrolling still works,
20
+ the bar is just not painted. Generic chrome that design definitions can't author per-node (pseudo-
21
+ elements), so it lives with the structural reset. */
22
+ .uno-isolate-root * { scrollbar-width: none; }
23
+ .uno-isolate-root *::-webkit-scrollbar { width: 0; height: 0; display: none; }
24
+ `;
25
+
26
+ /** A host element carrying a shadow root, and the container everything draws into. */
27
+ export function isolatedRoot(doc: Document): { host: HTMLDivElement; root: HTMLDivElement } {
28
+ const host = doc.createElement("div");
29
+ host.style.display = "contents";
30
+ const shadow = host.shadowRoot ?? host.attachShadow({ mode: "open" });
31
+ const reset = doc.createElement("style");
32
+ reset.textContent = RESET_CSS;
33
+ const root = doc.createElement("div");
34
+ root.className = "uno-isolate-root";
35
+ shadow.append(reset, root);
36
+ return { host, root };
37
+ }
@@ -0,0 +1,388 @@
1
+ /**
2
+ * `Markdown`, in plain DOM: a bound markdown string drawn as elements. The text is parsed
3
+ * (./markdownParse) into a tree of tags, and each element takes its look from the SERVED prose
4
+ * recipe (`theme.prose` ← design/styles/semantic/prose). ⛔ AUTHORS ZERO STYLE VALUES: this
5
+ * file maps element → recipe key and supplies behaviour only (links open in a new tab, a
6
+ * broken image falls back to its alt text, a citation opens its pop-up).
7
+ *
8
+ * Updated in place as an answer streams: an element whose tag is unchanged is kept, so an
9
+ * image keeps its broken state and an open citation stays open.
10
+ */
11
+ import type { ResolvedTheme, UnoverseNode } from "../core/types";
12
+ import { styleToCss, type CssStyle } from "../style";
13
+ import { citationOf, fitPopover, holdOpenCitation, keepCitationsWhole } from "../citations";
14
+ import { setAttr, setChildren, setStyle } from "./element";
15
+ import { svgOf, type Prim } from "./primitives";
16
+ import { parseMarkdown } from "./markdownParse";
17
+ import type { Data, MarkdownTree, RenderCtx } from "./types";
18
+
19
+ interface Env {
20
+ doc: Document;
21
+ theme(): ResolvedTheme;
22
+ inheritLink(): boolean;
23
+ cite(): { action?: string; data: Data };
24
+ onAction: RenderCtx["onAction"];
25
+ /** Re-draw the whole answer (an element changed what it puts on the page). */
26
+ refresh(): void;
27
+ }
28
+
29
+ interface Rec {
30
+ key: string;
31
+ readonly nodes: Node[];
32
+ update(t: MarkdownTree): void;
33
+ destroy(): void;
34
+ }
35
+
36
+ type El = Exclude<MarkdownTree, string>;
37
+
38
+ /** The prose recipe's style for one key. */
39
+ const prose = (theme: ResolvedTheme) => {
40
+ const p = (theme.prose ?? {}) as Record<string, unknown>;
41
+ const s = (k: string) => (p[k] as CssStyle | undefined) ?? {};
42
+ const headings = (p.heading as Record<string, string>) ?? {};
43
+ const head = (tag: string) => (theme.text?.[headings[tag]] as CssStyle | undefined) ?? {};
44
+ return { p, s, head };
45
+ };
46
+
47
+ /** The served style each element takes, by tag. */
48
+ function recipeFor(tag: string, theme: ResolvedTheme): CssStyle | undefined {
49
+ const { p, s, head } = prose(theme);
50
+ switch (tag) {
51
+ case "p": return s("paragraph");
52
+ case "strong": return s("strong");
53
+ case "em": return s("emphasis");
54
+ case "del": return s("strikethrough");
55
+ case "code": return s("code");
56
+ case "pre": return s("codeBlock");
57
+ case "blockquote": return s("blockquote");
58
+ case "hr": return s("rule");
59
+ case "th": return { ...s("cell"), ...s("headerCell") };
60
+ case "td": return s("cell");
61
+ case "ul": return { ...s("list"), listStyleType: p.bulletStyle as string };
62
+ case "ol": return { ...s("list"), listStyleType: p.orderedStyle as string };
63
+ case "li": return s("listItem");
64
+ case "h1": case "h2": case "h3": case "h4": case "h5": case "h6": return head(tag);
65
+ default: return undefined;
66
+ }
67
+ }
68
+
69
+ const PROP_ATTR: Record<string, string> = { className: "class", htmlFor: "for", readOnly: "readonly", tabIndex: "tabindex", colSpan: "colspan", rowSpan: "rowspan" };
70
+
71
+ /** A parsed element's own props as attributes (style handled by the caller). */
72
+ function setProps(el: Element, props: Record<string, unknown>, prev: Set<string>): Set<string> {
73
+ const now = new Set<string>();
74
+ for (const [k, v] of Object.entries(props)) {
75
+ if (k === "key" || k === "children" || k === "style" || typeof v === "function") continue;
76
+ const name = PROP_ATTR[k] ?? k;
77
+ if (v == null || v === false || (k === "className" && v === "")) continue;
78
+ now.add(name);
79
+ setAttr(el, name, v);
80
+ }
81
+ for (const name of prev) if (!now.has(name)) el.removeAttribute(name);
82
+ return now;
83
+ }
84
+
85
+ const keyOf = (t: MarkdownTree): string =>
86
+ typeof t === "string" ? "#text" : t.tag === "a" && typeof t.props.href === "string" && t.props.href.startsWith("cite:") ? "a:cite" : t.tag;
87
+
88
+ function children(env: Env, parent: Node, recs: Rec[], trees: MarkdownTree[]): void {
89
+ trees.forEach((t, i) => {
90
+ const key = keyOf(t);
91
+ if (recs[i]?.key !== key) {
92
+ recs[i]?.destroy();
93
+ recs[i] = create(env, key);
94
+ }
95
+ recs[i].update(t);
96
+ });
97
+ for (const r of recs.splice(trees.length)) r.destroy();
98
+ setChildren(parent, recs.flatMap((r) => r.nodes));
99
+ }
100
+
101
+ function create(env: Env, key: string): Rec {
102
+ if (key === "#text") {
103
+ const n = env.doc.createTextNode("");
104
+ return { key, nodes: [n], update: (t) => void (n.data !== t && (n.data = t as string)), destroy() {} };
105
+ }
106
+ if (key === "a:cite") return citeMark(env);
107
+ if (key === "a") return externalLink(env);
108
+ if (key === "table") return scrollableTable(env);
109
+ if (key === "img") return markdownImage(env);
110
+ return element(env, key);
111
+ }
112
+
113
+ function element(env: Env, tag: string): Rec {
114
+ const el = env.doc.createElement(tag);
115
+ const recs: Rec[] = [];
116
+ let attrs = new Set<string>();
117
+ return {
118
+ key: tag,
119
+ nodes: [el],
120
+ update(t) {
121
+ const { props, children: kids } = t as El;
122
+ attrs = setProps(el, props, attrs);
123
+ // The recipe's style wins over the parser's own (the overrides' merge order).
124
+ setStyle(el, recipeFor(tag, env.theme()) ?? (props.style as CssStyle | undefined));
125
+ children(env, el, recs, kids);
126
+ },
127
+ destroy() {
128
+ for (const r of recs) r.destroy();
129
+ },
130
+ };
131
+ }
132
+
133
+ /** A web link: opens in a new tab, with the served external-link glyph after it. */
134
+ function externalLink(env: Env): Rec {
135
+ const el = env.doc.createElement("a");
136
+ const recs: Rec[] = [];
137
+ let attrs = new Set<string>();
138
+ let arrow: { glyph: unknown; svg: SVGSVGElement } | null = null;
139
+ return {
140
+ key: "a",
141
+ nodes: [el],
142
+ update(t) {
143
+ const { props, children: kids } = t as El;
144
+ const theme = env.theme();
145
+ const { s } = prose(theme);
146
+ attrs = setProps(el, { ...props, target: "_blank", rel: "noopener noreferrer" }, attrs);
147
+ setStyle(el, env.inheritLink() ? { ...s("link"), color: "inherit" } : s("link"));
148
+ const li = (theme.icons?.externalLink ?? {}) as Parameters<typeof svgOf>[1];
149
+ if (!li.children?.length) arrow = null;
150
+ else if (arrow?.glyph !== li) arrow = { glyph: li, svg: svgOf(env.doc, li, s("linkIcon")) };
151
+ children(env, el, recs, kids);
152
+ if (arrow) el.appendChild(arrow.svg);
153
+ },
154
+ destroy() {
155
+ for (const r of recs) r.destroy();
156
+ },
157
+ };
158
+ }
159
+
160
+ /** Wide tables scroll instead of crushing their columns. */
161
+ function scrollableTable(env: Env): Rec {
162
+ const wrap = env.doc.createElement("div");
163
+ const table = env.doc.createElement("table");
164
+ wrap.appendChild(table);
165
+ const recs: Rec[] = [];
166
+ let attrs = new Set<string>();
167
+ return {
168
+ key: "table",
169
+ nodes: [wrap],
170
+ update(t) {
171
+ const { props, children: kids } = t as El;
172
+ const { s } = prose(env.theme());
173
+ setStyle(wrap, s("tableWrap"));
174
+ attrs = setProps(table, props, attrs);
175
+ setStyle(table, s("table"));
176
+ children(env, table, recs, kids);
177
+ },
178
+ destroy() {
179
+ for (const r of recs) r.destroy();
180
+ },
181
+ };
182
+ }
183
+
184
+ /** A broken image URL degrades to its alt text, not a broken-image icon. */
185
+ function markdownImage(env: Env): Rec {
186
+ let broken = false;
187
+ let src: string | undefined;
188
+ const img = env.doc.createElement("img");
189
+ const fallback = env.doc.createElement("span");
190
+ img.addEventListener("error", () => {
191
+ broken = true;
192
+ env.refresh();
193
+ });
194
+ let showing: Node = img;
195
+ return {
196
+ key: "img",
197
+ get nodes() {
198
+ return [showing];
199
+ },
200
+ update(t) {
201
+ const { props } = t as El;
202
+ const { s } = prose(env.theme());
203
+ if (props.src !== src) {
204
+ src = props.src as string | undefined;
205
+ broken = false;
206
+ }
207
+ const alt = props.alt as string | undefined;
208
+ if (!src || broken) {
209
+ setStyle(fallback, s("imageFallback"));
210
+ fallback.textContent = alt || "Image unavailable";
211
+ showing = fallback;
212
+ return;
213
+ }
214
+ setAttr(img, "src", src);
215
+ setAttr(img, "alt", alt || "");
216
+ setAttr(img, "title", props.title);
217
+ setAttr(img, "loading", "lazy");
218
+ setAttr(img, "decoding", "async");
219
+ setStyle(img, s("image"));
220
+ showing = img;
221
+ },
222
+ destroy() {},
223
+ };
224
+ }
225
+
226
+ /**
227
+ * A CITATION (docs/unoverse/data/objects/citations-in-answers.md): a `cite:` link is a marker
228
+ * after the fact it supports. Tapped, a pop-up shows the quoted words, the document and the
229
+ * page, and an Open button that fires the node's `citeAction` with the citation. Every look is
230
+ * a served prose key (citeAnchor, citeMark, citePopover, citeQuote, citeSource, citeOpen).
231
+ */
232
+ function citeMark(env: Env): Rec {
233
+ const doc = env.doc;
234
+ const anchor = doc.createElement("span");
235
+ const mark = doc.createElement("button");
236
+ mark.type = "button";
237
+ anchor.appendChild(mark);
238
+ const recs: Rec[] = [];
239
+ let plain: Node[] | null = null;
240
+ let open = false;
241
+ let c: ReturnType<typeof citationOf> = null;
242
+ let place = { shift: 0, above: false };
243
+ let pop: HTMLSpanElement | null = null;
244
+ let stopOpen: (() => void) | null = null;
245
+
246
+ const drawPop = () => {
247
+ if (!open || !c) {
248
+ pop?.remove();
249
+ pop = null;
250
+ return;
251
+ }
252
+ const { s } = prose(env.theme());
253
+ const { action, data } = env.cite();
254
+ pop ??= doc.createElement("span");
255
+ setAttr(pop, "role", "dialog");
256
+ setStyle(pop, { ...s("citePopover"), transform: `translateX(${place.shift}px)`, ...(place.above ? { top: "auto", bottom: "100%" } : {}) });
257
+ const parts: Node[] = [];
258
+ const source = [c.doc, c.page ? `p. ${c.page}` : ""].filter(Boolean).join(" · ");
259
+ if (c.quote) {
260
+ const q = doc.createElement("span");
261
+ setStyle(q, { display: "block", ...s("citeQuote") });
262
+ q.textContent = `“${c.quote}”`;
263
+ parts.push(q);
264
+ }
265
+ if (source) {
266
+ const src = doc.createElement("span");
267
+ setStyle(src, { display: "block", ...s("citeSource") });
268
+ src.textContent = source;
269
+ parts.push(src);
270
+ }
271
+ if (action && env.onAction) {
272
+ const btn = doc.createElement("button");
273
+ btn.type = "button";
274
+ setStyle(btn, s("citeOpen"));
275
+ btn.textContent = "Open";
276
+ const citation = c;
277
+ btn.addEventListener("click", () => {
278
+ setOpen(false);
279
+ env.onAction?.(action, { ...data, citation });
280
+ });
281
+ parts.push(btn);
282
+ }
283
+ setChildren(pop, parts);
284
+ if (pop.parentNode !== anchor) anchor.appendChild(pop);
285
+ };
286
+
287
+ // ALWAYS ON SCREEN: measured as it opens, and again when the window changes size.
288
+ const fit = () => {
289
+ if (!pop) return;
290
+ const a = anchor.getBoundingClientRect();
291
+ place = fitPopover(a, { width: pop.offsetWidth, height: pop.offsetHeight }, { width: window.innerWidth, height: window.innerHeight });
292
+ drawPop();
293
+ };
294
+
295
+ const setOpen = (next: boolean) => {
296
+ if (next === open) return;
297
+ open = next;
298
+ setAttr(mark, "aria-expanded", String(open));
299
+ stopOpen?.();
300
+ stopOpen = null;
301
+ drawPop();
302
+ if (!open) return;
303
+ fit();
304
+ const away = (e: Event) => {
305
+ if (!e.composedPath().includes(anchor)) setOpen(false);
306
+ };
307
+ const esc = (e: KeyboardEvent) => {
308
+ if (e.key === "Escape") setOpen(false);
309
+ };
310
+ window.addEventListener("resize", fit);
311
+ document.addEventListener("pointerdown", away);
312
+ document.addEventListener("keydown", esc);
313
+ stopOpen = () => {
314
+ window.removeEventListener("resize", fit);
315
+ document.removeEventListener("pointerdown", away);
316
+ document.removeEventListener("keydown", esc);
317
+ };
318
+ };
319
+ mark.addEventListener("click", () => setOpen(!open));
320
+
321
+ return {
322
+ key: "a:cite",
323
+ get nodes() {
324
+ return plain ?? [anchor];
325
+ },
326
+ update(t) {
327
+ const { props, children: kids } = t as El;
328
+ c = citationOf(props.href as string, props.title as string | undefined);
329
+ if (!c) {
330
+ // Not a readable citation: its words only.
331
+ const holder = doc.createDocumentFragment();
332
+ children(env, holder, recs, kids);
333
+ plain = recs.flatMap((r) => r.nodes);
334
+ return;
335
+ }
336
+ plain = null;
337
+ const { s } = prose(env.theme());
338
+ setStyle(anchor, s("citeAnchor"));
339
+ const source = [c.doc, c.page ? `p. ${c.page}` : ""].filter(Boolean).join(" · ");
340
+ setAttr(mark, "aria-expanded", String(open));
341
+ setAttr(mark, "aria-label", source ? `Source: ${source}` : "Source");
342
+ setStyle(mark, s("citeMark"));
343
+ children(env, mark, recs, kids);
344
+ drawPop();
345
+ },
346
+ destroy() {
347
+ stopOpen?.();
348
+ for (const r of recs) r.destroy();
349
+ },
350
+ };
351
+ }
352
+
353
+ /**
354
+ * The Markdown primitive. The container takes `node.style`; its content is the parsed text.
355
+ * `inheritLinkColor: true` makes links take the node's own color (a CTA whose label must
356
+ * match it); otherwise links keep the served link recipe.
357
+ */
358
+ export function markdownView(ctx: RenderCtx): Prim {
359
+ const el = ctx.doc.createElement("div");
360
+ const recs: Rec[] = [];
361
+ let node: UnoverseNode;
362
+ let data: Data;
363
+ let trees: MarkdownTree[] = [];
364
+ const env: Env = {
365
+ doc: ctx.doc,
366
+ theme: () => ctx.theme,
367
+ inheritLink: () => (node as { inheritLinkColor?: boolean }).inheritLinkColor === true,
368
+ cite: () => ({ action: (node as { citeAction?: string }).citeAction, data }),
369
+ onAction: (...a) => ctx.onAction?.(...a),
370
+ refresh: () => children(env, el, recs, trees),
371
+ };
372
+ return {
373
+ nodes: [el],
374
+ update(n, d) {
375
+ node = n;
376
+ data = d;
377
+ setStyle(el, styleToCss(n.style, ctx.theme, d));
378
+ // Bound, or literal as a `Ref`'s `with` leaves it, exactly as Text reads its value.
379
+ const value = (n.bind?.value ? (d[n.bind.value] as string) : (n as { value?: unknown }).value) ?? "";
380
+ const parsed = parseMarkdown(holdOpenCitation(keepCitationsWhole(String(value))));
381
+ trees = parsed == null ? [] : [parsed];
382
+ children(env, el, recs, trees);
383
+ },
384
+ destroy() {
385
+ for (const r of recs) r.destroy();
386
+ },
387
+ };
388
+ }