@wildwinter/app-shell 0.57.0 → 0.58.1

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 (66) hide show
  1. package/README.md +195 -0
  2. package/dist/chunk-4JQWNC6Z.js +319 -0
  3. package/dist/chunk-4JQWNC6Z.js.map +1 -0
  4. package/dist/chunk-ICYPXUNA.js +8 -0
  5. package/dist/chunk-ICYPXUNA.js.map +1 -0
  6. package/dist/{chunk-XJM733E6.js → chunk-OO6O4LEL.js} +6 -2
  7. package/dist/chunk-OO6O4LEL.js.map +1 -0
  8. package/dist/{chunk-CPNTROWD.js → chunk-OZQ2BYXH.js} +13 -4
  9. package/dist/chunk-OZQ2BYXH.js.map +1 -0
  10. package/dist/{chunk-5BKGDTYD.js → chunk-RMQNBWLZ.js} +3 -3
  11. package/dist/{chunk-5BKGDTYD.js.map → chunk-RMQNBWLZ.js.map} +1 -1
  12. package/dist/index.cjs +334 -3
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +2 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +4 -2
  17. package/dist/index.js.map +1 -1
  18. package/dist/property-list.cjs.map +1 -1
  19. package/dist/property-list.js +2 -2
  20. package/dist/tool-window.cjs +11 -2
  21. package/dist/tool-window.cjs.map +1 -1
  22. package/dist/tool-window.d.cts +10 -4
  23. package/dist/tool-window.d.ts +10 -4
  24. package/dist/tool-window.js +1 -1
  25. package/dist/updater.cjs.map +1 -1
  26. package/dist/updater.js +1 -1
  27. package/dist/value-control.cjs +4 -0
  28. package/dist/value-control.cjs.map +1 -1
  29. package/dist/value-control.d.cts +4 -4
  30. package/dist/value-control.d.ts +4 -4
  31. package/dist/value-control.js +1 -1
  32. package/dist/value-rows.cjs +8 -1
  33. package/dist/value-rows.cjs.map +1 -1
  34. package/dist/value-rows.js +5 -2
  35. package/dist/value-rows.js.map +1 -1
  36. package/dist/view-client.cjs +172 -0
  37. package/dist/view-client.cjs.map +1 -0
  38. package/dist/view-client.d.cts +46 -0
  39. package/dist/view-client.d.ts +46 -0
  40. package/dist/view-client.js +165 -0
  41. package/dist/view-client.js.map +1 -0
  42. package/dist/view-host-web.cjs +327 -0
  43. package/dist/view-host-web.cjs.map +1 -0
  44. package/dist/view-host-web.d.cts +61 -0
  45. package/dist/view-host-web.d.ts +61 -0
  46. package/dist/view-host-web.js +4 -0
  47. package/dist/view-host-web.js.map +1 -0
  48. package/dist/view-host.cjs +532 -0
  49. package/dist/view-host.cjs.map +1 -0
  50. package/dist/view-host.d.cts +178 -0
  51. package/dist/view-host.d.ts +178 -0
  52. package/dist/view-host.js +426 -0
  53. package/dist/view-host.js.map +1 -0
  54. package/dist/view-preload.cjs +16 -0
  55. package/dist/view-preload.cjs.map +1 -0
  56. package/dist/view-preload.js +8 -0
  57. package/dist/view-preload.js.map +1 -0
  58. package/dist/view-protocol.cjs +12 -0
  59. package/dist/view-protocol.cjs.map +1 -0
  60. package/dist/view-protocol.d.cts +257 -0
  61. package/dist/view-protocol.d.ts +257 -0
  62. package/dist/view-protocol.js +3 -0
  63. package/dist/view-protocol.js.map +1 -0
  64. package/package.json +25 -4
  65. package/dist/chunk-CPNTROWD.js.map +0 -1
  66. package/dist/chunk-XJM733E6.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/view-protocol.ts","../src/view-client.ts"],"names":[],"mappings":"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","file":"view-client.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// The view host API (tier 2 view extensions): the messages between a view, a\n// web component an extension's installed package supplies, and the app that\n// hosts it in a sandboxed floating window (patterkit design/view-extensions.md).\n//\n// Both ends import this module: the app through `hostView`, a view through\n// `@wildwinter/app-shell/view-client`. It is types and constants only, so a\n// view's bundle takes nothing else from the shell.\n//\n// The transport is a MessagePort straight from the view to the editor's\n// renderer, which holds what the writer sees in both apps. Nothing is\n// answered before the view's hello names the API it was built for; a host\n// that can't serve that API refuses it with a sentence and closes the port.\n// ---------------------------------------------------------------------------\n\n/** The API this shell's hosts and clients speak. A host serves exactly this version. */\nexport const VIEW_API = 1;\n\n/** A host: the app a view runs in. */\nexport type ViewHostName = \"patter\" | \"storylets\";\n\n/** A JSON value, as extension data and host fields are. */\nexport type ViewJson = string | number | boolean | null | ViewJson[] | { [key: string]: ViewJson };\n\n// --- What a host hands a view ---------------------------------------------\n\n/**\n * A node of the host's project, in one envelope for both hosts.\n *\n * `kind` is the manifest's node kind (`line`, `card`, `project`, ...). `host`\n * holds the host's own fields for that kind, documented per host in the\n * shell's README; `ext` holds extension data by extension id, the view's own\n * and those its manifest `reads`, nothing else.\n */\nexport interface ViewNode {\n readonly id: string;\n readonly kind: string;\n /** The containing node's id; null for the project. */\n readonly parent: string | null;\n /** A short label for the node, in the source language, as the editor names it. */\n readonly label: string;\n readonly host: { readonly [field: string]: ViewJson };\n readonly ext: { readonly [extension: string]: { readonly [field: string]: ViewJson } };\n /** The node's children in order, when they were asked for. */\n readonly children?: readonly ViewNode[];\n /** Set when somebody else holds the view's own extension data on this node: the sentence saying who. A write\n * touching the node is refused `held`; a view can show the node read-only meanwhile. */\n readonly held?: string;\n}\n\n/** The selection, innermost first (a line, then its snippet, block, and scene). Empty when nothing is selected. */\nexport interface ViewSelection {\n readonly path: readonly { readonly id: string; readonly kind: string }[];\n}\n\n/** A line's length: a recording's when there is one (`measured`), else the host's estimate from its text. */\nexport interface ViewMediaLength {\n readonly seconds: number;\n readonly measured: boolean;\n}\n\n/** The app's theme, for the view to draw in: its CSS custom properties and its colour scheme. */\nexport interface ViewTheme {\n readonly scheme: \"light\" | \"dark\";\n /** Custom property names, with their leading `--`, and their computed values. */\n readonly tokens: { readonly [name: string]: string };\n}\n\n/** Whether the view may write, and why not when it may not, as a sentence for the view to show. */\nexport interface ViewWritable {\n readonly readOnly: boolean;\n readonly why?: string;\n}\n\n/**\n * One write to the view's own extension data: a field on a node, set to a value, or cleared when `value` is absent.\n *\n * `field` is a top-level field the manifest declares for that node's kind, never a path into one: changing one\n * entry of a map or record writes the whole field's new value. Read the node again just before writing, so the\n * write builds on any edit made in the editor meanwhile; requests are answered in order, so nothing lands between.\n *\n * One write is one undo step in one history: its edits are all on the project node, or none of them are (the\n * project's data and a scene's sit in different histories in Patterpad). `hostView` refuses a write that mixes them.\n */\nexport interface ViewEdit {\n readonly node: string;\n readonly field: string;\n readonly value?: ViewJson;\n}\n\n/** Why a request was refused: a stable code for the view, and a sentence for the writer. */\nexport interface ViewRefusal {\n readonly code:\n | \"bad-request\" // the request's shape is wrong\n | \"unsupported\" // the host has no such thing (media in Storyletter)\n | \"gone\" // a node the request names doesn't exist\n | \"held\" // somebody else holds the data the write would change\n | \"read-only\" // the project or the view is read-only\n | \"failed\"; // the host tried and couldn't\n readonly message: string;\n}\n\n// --- Requests --------------------------------------------------------------\n\n/** Each request a view can make: what it sends, and what a granted reply carries. */\nexport interface ViewRequests {\n /** The project node: the host's project facts (Patter: cast, qualifiers, pad default; Storylets: boxes, tag groups). */\n project: { args: Record<string, never>; value: ViewNode };\n /** One node by id, and its children in order when `children` is true; null when there is no such node. */\n node: { args: { id: string; children?: boolean }; value: ViewNode | null };\n selection: { args: Record<string, never>; value: ViewSelection };\n /** Lines' lengths by id. Refused `unsupported` by a host with no media. */\n media: { args: { ids: string[] }; value: { [id: string]: ViewMediaLength } };\n /** Writes to the view's own extension, applied as one undo step with the label, or refused whole. */\n write: { args: { label: string; edits: ViewEdit[] }; value: { written: number } };\n /** Reveal and select a node in the editor. */\n select: { args: { id: string }; value: boolean };\n}\n\nexport type ViewOp = keyof ViewRequests;\n\n// --- Messages ----------------------------------------------------------------\n\n/** A view's first message. */\nexport interface ViewHello { readonly t: \"hello\"; readonly api: number }\n\nexport interface ViewRequest<K extends ViewOp = ViewOp> {\n readonly t: \"request\";\n readonly id: number;\n readonly op: K;\n readonly args: ViewRequests[K][\"args\"];\n}\n\n/** A key combination the view didn't handle, in the shell's portable spelling (\"Mod+Z\", \"Mod+Shift+Z\"), for the host to run. */\nexport interface ViewKey { readonly t: \"key\"; readonly combo: string }\n\nexport type ViewToHost = ViewHello | ViewRequest | ViewKey;\n\n/** The host's answer to a hello it can serve. */\nexport interface ViewWelcome {\n readonly t: \"welcome\";\n readonly api: number;\n readonly host: ViewHostName;\n /** The view's extension: its id, the installed version, and the version the project pins, if any. */\n readonly extension: { readonly id: string; readonly version: string; readonly pinned?: string };\n /** The view's own id within its manifest. */\n readonly view: string;\n /** The extension's project settings, by name. */\n readonly settings: { readonly [name: string]: string };\n readonly theme: ViewTheme;\n readonly writable: ViewWritable;\n}\n\n/** The host's answer to a hello it can't serve, before it closes the port. */\nexport interface ViewRefused { readonly t: \"refused\"; readonly message: string }\n\nexport type ViewReply =\n | { readonly t: \"reply\"; readonly id: number; readonly ok: true; readonly value: unknown }\n | { readonly t: \"reply\"; readonly id: number; readonly ok: false; readonly refusal: ViewRefusal };\n\n/** What the host tells a view unasked. */\nexport interface ViewEvents {\n /** The project changed: these nodes, or anything (\"all\"). Debounced; read again what you show. */\n changed: { nodes: readonly string[] | \"all\" };\n selection: ViewSelection;\n theme: ViewTheme;\n writable: ViewWritable;\n /** The window is closing; the port goes next. */\n closing: Record<string, never>;\n}\n\nexport type ViewEventName = keyof ViewEvents;\n\nexport interface ViewEvent<K extends ViewEventName = ViewEventName> {\n readonly t: \"event\";\n readonly name: K;\n readonly value: ViewEvents[K];\n}\n\nexport type HostToView = ViewWelcome | ViewRefused | ViewReply | ViewEvent;\n\n// --- The port's arrival --------------------------------------------------------\n\n/** The IPC channel main sends each end's port on. */\nexport const VIEW_PORT_CHANNEL = \"wildwinter-view:port\";\n\n/** The window message a preload posts a port with, so page code (which can't see ipcRenderer) can take it. */\nexport const VIEW_PORT_MESSAGE = \"wildwinter-view:port\";\n\n/** What main tells the editor about a view whose port it is handing over. */\nexport interface ViewPortMeta {\n /** The view window's id, unique while it's open. */\n readonly window: number;\n readonly extension: string;\n readonly view: string;\n readonly version: string;\n readonly pinned?: string;\n /** Set, with no port, when the view window has closed: the editor drops its handle for `window`. */\n readonly closed?: true;\n}\n\n// --- The host's side: what an app implements, and what hostView gives back ----\n\n/**\n * What an app supplies `hostView` for one open view: its own model, as the\n * writer sees it (unsaved edits included). `hostView` answers the protocol\n * from it, checks every request's shape, keeps `ext` to the view's own\n * extension and its manifest's `reads`, and refuses writes when not writable.\n * Any method may return a promise.\n */\nexport interface ViewAdapter {\n readonly host: ViewHostName;\n /** The project node. */\n project(): ViewNode | Promise<ViewNode>;\n node(id: string, children: boolean): ViewNode | null | Promise<ViewNode | null>;\n selection(): ViewSelection | Promise<ViewSelection>;\n /** Absent in a host with no media; `hostView` then refuses `media` as unsupported. */\n media?(ids: readonly string[]): { [id: string]: ViewMediaLength } | Promise<{ [id: string]: ViewMediaLength }>;\n /** Apply the edits to `extension`'s data as one undo step with the label, or refuse them all. */\n write(extension: string, label: string, edits: readonly ViewEdit[]): void | ViewRefusal | Promise<void | ViewRefusal>;\n select(id: string): boolean | Promise<boolean>;\n writable(extension: string): ViewWritable;\n /** The extension's project settings, by name. */\n settings(extension: string): { [name: string]: string };\n /** Run a key combination the view passed on (undo, redo, the app's shortcuts). */\n key?(combo: string): void;\n}\n\n/** What `hostView` returns, and `hostViews` for every open view: the app calls these as its model, selection,\n * theme, or locks change. */\nexport interface ViewHandle {\n changed(nodes: readonly string[] | \"all\"): void;\n selection(): void;\n theme(): void;\n writable(): void;\n close(): void;\n}\n","// ---------------------------------------------------------------------------\n// The view client (tier 2 view extensions), for VIEW AUTHORS: what a view's\n// web component imports to talk to the app it runs in. Tiny and\n// dependency-free: it imports nothing but the protocol's types and constants.\n//\n// const app = await connect();\n// const sel = await app.selection();\n// app.on(\"changed\", () => redraw());\n//\n// Call `connect()` as the view's module loads (the port arrives once the page\n// has loaded, and a listener added later misses it). It applies the app's\n// theme to the page's root as custom properties (`var(--ink)`, ...), and\n// passes on key combinations the view doesn't handle (Mod+Z runs the\n// editor's undo); plain typing never leaves the view.\n// ---------------------------------------------------------------------------\n\nimport { VIEW_API, VIEW_PORT_MESSAGE } from \"./view-protocol.js\";\nimport type {\n HostToView, ViewEdit, ViewEventName, ViewEvents, ViewMediaLength, ViewNode, ViewOp, ViewRefusal, ViewRequests,\n ViewSelection, ViewTheme, ViewWelcome,\n} from \"./view-protocol.js\";\n\nexport type {\n HostToView, ViewEdit, ViewEventName, ViewEvents, ViewHostName, ViewJson, ViewMediaLength, ViewNode, ViewOp,\n ViewRefusal, ViewRequests, ViewSelection, ViewTheme, ViewWelcome, ViewWritable,\n} from \"./view-protocol.js\";\nexport { VIEW_API } from \"./view-protocol.js\";\n\n/** A refused request: the host's sentence as the message, and its refusal (`code`) for the view to act on. */\nexport class ViewRefusalError extends Error {\n readonly refusal: ViewRefusal;\n constructor(refusal: ViewRefusal) {\n super(refusal.message);\n this.name = \"ViewRefusalError\";\n this.refusal = refusal;\n }\n}\n\nexport interface ConnectOptions {\n /** How long to wait for the port and the app's welcome, in ms (default 10000). */\n timeoutMs?: number;\n /** A port already in hand, instead of waiting for the preload's (tests, or a host of another kind). */\n port?: MessagePort;\n /** The window to listen on and apply the theme and keys to (default `window`). */\n window?: Window;\n /** Pass unhandled key combinations on to the app (default true). */\n keys?: boolean;\n}\n\nexport interface ViewClient {\n /** What the app said on welcoming the view: host, extension, view, settings, theme, writable. */\n readonly welcome: ViewWelcome;\n request<K extends ViewOp>(op: K, args: ViewRequests[K][\"args\"]): Promise<ViewRequests[K][\"value\"]>;\n project(): Promise<ViewNode>;\n node(id: string, opts?: { children?: boolean }): Promise<ViewNode | null>;\n selection(): Promise<ViewSelection>;\n media(ids: string[]): Promise<{ [id: string]: ViewMediaLength }>;\n write(label: string, edits: ViewEdit[]): Promise<{ written: number }>;\n select(id: string): Promise<boolean>;\n /** Listen for an event; returns the unsubscribe. */\n on<K extends ViewEventName>(name: K, fn: (value: ViewEvents[K]) => void): () => void;\n close(): void;\n}\n\n/** Set the theme's tokens as custom properties on the root, and its colour scheme. */\nexport function applyTheme(theme: ViewTheme, root: HTMLElement = document.documentElement): void {\n for (const [name, value] of Object.entries(theme.tokens)) if (name.startsWith(\"--\")) root.style.setProperty(name, value);\n root.style.setProperty(\"color-scheme\", theme.scheme);\n}\n\nconst isMac = (w: Window): boolean => {\n const nav = w.navigator as Navigator & { userAgentData?: { platform?: string } };\n return /mac|iphone|ipad/i.test(nav.userAgentData?.platform ?? nav.platform ?? nav.userAgent ?? \"\");\n};\n\nconst NAMED: Record<string, string> = {\n ArrowUp: \"Up\", ArrowDown: \"Down\", ArrowLeft: \"Left\", ArrowRight: \"Right\", Escape: \"Esc\", \" \": \"Space\",\n};\n\n/** A key event in the shell's portable spelling (\"Mod+Shift+Z\"), or null for a bare modifier. */\nexport function comboOf(e: KeyboardEvent, mac: boolean): string | null {\n if ([\"Meta\", \"Control\", \"Shift\", \"Alt\", \"OS\"].includes(e.key)) return null;\n const parts: string[] = [];\n if (mac ? e.metaKey : e.ctrlKey) parts.push(\"Mod\");\n if (mac && e.ctrlKey) parts.push(\"Ctrl\");\n if (e.altKey) parts.push(\"Alt\");\n if (e.shiftKey) parts.push(\"Shift\");\n let key: string;\n if (/^Key[A-Z]$/.test(e.code)) key = e.code.slice(3);\n else if (/^Digit[0-9]$/.test(e.code)) key = e.code.slice(5);\n else key = NAMED[e.key] ?? (e.key.length === 1 ? e.key.toUpperCase() : e.key);\n parts.push(key);\n return parts.join(\"+\");\n}\n\n/** The combinations whose page default is the page's own undo: the app's runs instead. */\nconst UNDO = new Set([\"Mod+Z\", \"Mod+Shift+Z\", \"Mod+Y\"]);\n\n/** Wait for the app's port, say hello, and resolve once the app welcomes the view (rejected with its sentence). */\nexport function connect(opts: ConnectOptions = {}): Promise<ViewClient> {\n const w = opts.window ?? window;\n const timeoutMs = opts.timeoutMs ?? 10000;\n return new Promise<ViewClient>((resolve, reject) => {\n let settled = false;\n const fail = (message: string, port?: MessagePort): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n w.removeEventListener(\"message\", onWindow);\n port?.close();\n reject(new Error(message));\n };\n const timer = setTimeout(() => fail(\"The app didn't answer this view in time.\"), timeoutMs);\n\n const begin = (port: MessagePort): void => {\n w.removeEventListener(\"message\", onWindow);\n const first = (e: MessageEvent): void => {\n const msg = e.data as HostToView;\n if (!msg || typeof msg !== \"object\") return;\n if (msg.t === \"refused\") { port.removeEventListener(\"message\", first as EventListener); fail(msg.message, port); return; }\n if (msg.t !== \"welcome\" || settled) return;\n settled = true;\n clearTimeout(timer);\n port.removeEventListener(\"message\", first as EventListener);\n resolve(client(port, msg, w, opts.keys ?? true));\n };\n port.addEventListener(\"message\", first as EventListener);\n port.start();\n port.postMessage({ t: \"hello\", api: VIEW_API });\n };\n function onWindow(e: MessageEvent): void {\n if (e.source !== w) return;\n const d = e.data as unknown;\n const ours = d === VIEW_PORT_MESSAGE || (typeof d === \"object\" && d !== null && (d as { type?: unknown }).type === VIEW_PORT_MESSAGE);\n const port = e.ports?.[0];\n if (ours && port) begin(port);\n }\n if (opts.port) begin(opts.port);\n else w.addEventListener(\"message\", onWindow);\n });\n}\n\nfunction client(port: MessagePort, welcome: ViewWelcome, w: Window, keys: boolean): ViewClient {\n let next = 1;\n let closed = false;\n const waiting = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();\n const listeners = new Map<ViewEventName, Set<(value: never) => void>>();\n const root = w.document.documentElement;\n applyTheme(welcome.theme, root);\n\n const onMessage = (e: MessageEvent): void => {\n const msg = e.data as HostToView;\n if (!msg || typeof msg !== \"object\") return;\n if (msg.t === \"reply\") {\n const p = waiting.get(msg.id);\n if (!p) return;\n waiting.delete(msg.id);\n if (msg.ok) p.resolve(msg.value);\n else p.reject(new ViewRefusalError(msg.refusal));\n } else if (msg.t === \"event\") {\n if (msg.name === \"theme\") applyTheme(msg.value as ViewTheme, root);\n for (const fn of listeners.get(msg.name) ?? []) {\n try { (fn as (v: unknown) => void)(msg.value); } catch (err) { console.error(err); }\n }\n }\n };\n port.addEventListener(\"message\", onMessage as EventListener);\n\n const mac = isMac(w);\n const onKey = (e: KeyboardEvent): void => {\n if (e.defaultPrevented || closed || !(e.metaKey || e.ctrlKey)) return;\n const combo = comboOf(e, mac);\n if (!combo) return;\n if (UNDO.has(combo)) e.preventDefault();\n port.postMessage({ t: \"key\", combo });\n };\n if (keys) w.addEventListener(\"keydown\", onKey);\n\n const request = <K extends ViewOp>(op: K, args: ViewRequests[K][\"args\"]): Promise<ViewRequests[K][\"value\"]> => {\n if (closed) return Promise.reject(new Error(\"This view's connection to the app is closed.\"));\n const id = next++;\n return new Promise((resolve, reject) => {\n waiting.set(id, { resolve: resolve as (v: unknown) => void, reject });\n port.postMessage({ t: \"request\", id, op, args });\n });\n };\n\n return {\n welcome,\n request,\n project: () => request(\"project\", {}),\n node: (id, o) => request(\"node\", o?.children !== undefined ? { id, children: o.children } : { id }),\n selection: () => request(\"selection\", {}),\n media: (ids) => request(\"media\", { ids }),\n write: (label, edits) => request(\"write\", { label, edits }),\n select: (id) => request(\"select\", { id }),\n on(name, fn) {\n let set = listeners.get(name);\n if (!set) listeners.set(name, (set = new Set()));\n set.add(fn as (value: never) => void);\n return () => { set.delete(fn as (value: never) => void); };\n },\n close() {\n if (closed) return;\n closed = true;\n w.removeEventListener(\"keydown\", onKey);\n port.removeEventListener(\"message\", onMessage as EventListener);\n port.close();\n for (const p of waiting.values()) p.reject(new Error(\"This view's connection to the app is closed.\"));\n waiting.clear();\n },\n };\n}\n"]}
@@ -0,0 +1,46 @@
1
+ import { ViewWelcome, ViewOp, ViewRequests, ViewNode, ViewSelection, ViewMediaLength, ViewEdit, ViewEventName, ViewEvents, ViewRefusal, ViewTheme } from './view-protocol.cjs';
2
+ export { HostToView, VIEW_API, ViewHostName, ViewJson, ViewWritable } from './view-protocol.cjs';
3
+
4
+ /** A refused request: the host's sentence as the message, and its refusal (`code`) for the view to act on. */
5
+ declare class ViewRefusalError extends Error {
6
+ readonly refusal: ViewRefusal;
7
+ constructor(refusal: ViewRefusal);
8
+ }
9
+ interface ConnectOptions {
10
+ /** How long to wait for the port and the app's welcome, in ms (default 10000). */
11
+ timeoutMs?: number;
12
+ /** A port already in hand, instead of waiting for the preload's (tests, or a host of another kind). */
13
+ port?: MessagePort;
14
+ /** The window to listen on and apply the theme and keys to (default `window`). */
15
+ window?: Window;
16
+ /** Pass unhandled key combinations on to the app (default true). */
17
+ keys?: boolean;
18
+ }
19
+ interface ViewClient {
20
+ /** What the app said on welcoming the view: host, extension, view, settings, theme, writable. */
21
+ readonly welcome: ViewWelcome;
22
+ request<K extends ViewOp>(op: K, args: ViewRequests[K]["args"]): Promise<ViewRequests[K]["value"]>;
23
+ project(): Promise<ViewNode>;
24
+ node(id: string, opts?: {
25
+ children?: boolean;
26
+ }): Promise<ViewNode | null>;
27
+ selection(): Promise<ViewSelection>;
28
+ media(ids: string[]): Promise<{
29
+ [id: string]: ViewMediaLength;
30
+ }>;
31
+ write(label: string, edits: ViewEdit[]): Promise<{
32
+ written: number;
33
+ }>;
34
+ select(id: string): Promise<boolean>;
35
+ /** Listen for an event; returns the unsubscribe. */
36
+ on<K extends ViewEventName>(name: K, fn: (value: ViewEvents[K]) => void): () => void;
37
+ close(): void;
38
+ }
39
+ /** Set the theme's tokens as custom properties on the root, and its colour scheme. */
40
+ declare function applyTheme(theme: ViewTheme, root?: HTMLElement): void;
41
+ /** A key event in the shell's portable spelling ("Mod+Shift+Z"), or null for a bare modifier. */
42
+ declare function comboOf(e: KeyboardEvent, mac: boolean): string | null;
43
+ /** Wait for the app's port, say hello, and resolve once the app welcomes the view (rejected with its sentence). */
44
+ declare function connect(opts?: ConnectOptions): Promise<ViewClient>;
45
+
46
+ export { type ConnectOptions, type ViewClient, ViewEdit, ViewEventName, ViewEvents, ViewMediaLength, ViewNode, ViewOp, ViewRefusal, ViewRefusalError, ViewRequests, ViewSelection, ViewTheme, ViewWelcome, applyTheme, comboOf, connect };
@@ -0,0 +1,46 @@
1
+ import { ViewWelcome, ViewOp, ViewRequests, ViewNode, ViewSelection, ViewMediaLength, ViewEdit, ViewEventName, ViewEvents, ViewRefusal, ViewTheme } from './view-protocol.js';
2
+ export { HostToView, VIEW_API, ViewHostName, ViewJson, ViewWritable } from './view-protocol.js';
3
+
4
+ /** A refused request: the host's sentence as the message, and its refusal (`code`) for the view to act on. */
5
+ declare class ViewRefusalError extends Error {
6
+ readonly refusal: ViewRefusal;
7
+ constructor(refusal: ViewRefusal);
8
+ }
9
+ interface ConnectOptions {
10
+ /** How long to wait for the port and the app's welcome, in ms (default 10000). */
11
+ timeoutMs?: number;
12
+ /** A port already in hand, instead of waiting for the preload's (tests, or a host of another kind). */
13
+ port?: MessagePort;
14
+ /** The window to listen on and apply the theme and keys to (default `window`). */
15
+ window?: Window;
16
+ /** Pass unhandled key combinations on to the app (default true). */
17
+ keys?: boolean;
18
+ }
19
+ interface ViewClient {
20
+ /** What the app said on welcoming the view: host, extension, view, settings, theme, writable. */
21
+ readonly welcome: ViewWelcome;
22
+ request<K extends ViewOp>(op: K, args: ViewRequests[K]["args"]): Promise<ViewRequests[K]["value"]>;
23
+ project(): Promise<ViewNode>;
24
+ node(id: string, opts?: {
25
+ children?: boolean;
26
+ }): Promise<ViewNode | null>;
27
+ selection(): Promise<ViewSelection>;
28
+ media(ids: string[]): Promise<{
29
+ [id: string]: ViewMediaLength;
30
+ }>;
31
+ write(label: string, edits: ViewEdit[]): Promise<{
32
+ written: number;
33
+ }>;
34
+ select(id: string): Promise<boolean>;
35
+ /** Listen for an event; returns the unsubscribe. */
36
+ on<K extends ViewEventName>(name: K, fn: (value: ViewEvents[K]) => void): () => void;
37
+ close(): void;
38
+ }
39
+ /** Set the theme's tokens as custom properties on the root, and its colour scheme. */
40
+ declare function applyTheme(theme: ViewTheme, root?: HTMLElement): void;
41
+ /** A key event in the shell's portable spelling ("Mod+Shift+Z"), or null for a bare modifier. */
42
+ declare function comboOf(e: KeyboardEvent, mac: boolean): string | null;
43
+ /** Wait for the app's port, say hello, and resolve once the app welcomes the view (rejected with its sentence). */
44
+ declare function connect(opts?: ConnectOptions): Promise<ViewClient>;
45
+
46
+ export { type ConnectOptions, type ViewClient, ViewEdit, ViewEventName, ViewEvents, ViewMediaLength, ViewNode, ViewOp, ViewRefusal, ViewRefusalError, ViewRequests, ViewSelection, ViewTheme, ViewWelcome, applyTheme, comboOf, connect };
@@ -0,0 +1,165 @@
1
+ import { VIEW_API, VIEW_PORT_MESSAGE } from './chunk-ICYPXUNA.js';
2
+ export { VIEW_API } from './chunk-ICYPXUNA.js';
3
+
4
+ // src/view-client.ts
5
+ var ViewRefusalError = class extends Error {
6
+ refusal;
7
+ constructor(refusal) {
8
+ super(refusal.message);
9
+ this.name = "ViewRefusalError";
10
+ this.refusal = refusal;
11
+ }
12
+ };
13
+ function applyTheme(theme, root = document.documentElement) {
14
+ for (const [name, value] of Object.entries(theme.tokens)) if (name.startsWith("--")) root.style.setProperty(name, value);
15
+ root.style.setProperty("color-scheme", theme.scheme);
16
+ }
17
+ var isMac = (w) => {
18
+ const nav = w.navigator;
19
+ return /mac|iphone|ipad/i.test(nav.userAgentData?.platform ?? nav.platform ?? nav.userAgent ?? "");
20
+ };
21
+ var NAMED = {
22
+ ArrowUp: "Up",
23
+ ArrowDown: "Down",
24
+ ArrowLeft: "Left",
25
+ ArrowRight: "Right",
26
+ Escape: "Esc",
27
+ " ": "Space"
28
+ };
29
+ function comboOf(e, mac) {
30
+ if (["Meta", "Control", "Shift", "Alt", "OS"].includes(e.key)) return null;
31
+ const parts = [];
32
+ if (mac ? e.metaKey : e.ctrlKey) parts.push("Mod");
33
+ if (mac && e.ctrlKey) parts.push("Ctrl");
34
+ if (e.altKey) parts.push("Alt");
35
+ if (e.shiftKey) parts.push("Shift");
36
+ let key;
37
+ if (/^Key[A-Z]$/.test(e.code)) key = e.code.slice(3);
38
+ else if (/^Digit[0-9]$/.test(e.code)) key = e.code.slice(5);
39
+ else key = NAMED[e.key] ?? (e.key.length === 1 ? e.key.toUpperCase() : e.key);
40
+ parts.push(key);
41
+ return parts.join("+");
42
+ }
43
+ var UNDO = /* @__PURE__ */ new Set(["Mod+Z", "Mod+Shift+Z", "Mod+Y"]);
44
+ function connect(opts = {}) {
45
+ const w = opts.window ?? window;
46
+ const timeoutMs = opts.timeoutMs ?? 1e4;
47
+ return new Promise((resolve, reject) => {
48
+ let settled = false;
49
+ const fail = (message, port) => {
50
+ if (settled) return;
51
+ settled = true;
52
+ clearTimeout(timer);
53
+ w.removeEventListener("message", onWindow);
54
+ port?.close();
55
+ reject(new Error(message));
56
+ };
57
+ const timer = setTimeout(() => fail("The app didn't answer this view in time."), timeoutMs);
58
+ const begin = (port) => {
59
+ w.removeEventListener("message", onWindow);
60
+ const first = (e) => {
61
+ const msg = e.data;
62
+ if (!msg || typeof msg !== "object") return;
63
+ if (msg.t === "refused") {
64
+ port.removeEventListener("message", first);
65
+ fail(msg.message, port);
66
+ return;
67
+ }
68
+ if (msg.t !== "welcome" || settled) return;
69
+ settled = true;
70
+ clearTimeout(timer);
71
+ port.removeEventListener("message", first);
72
+ resolve(client(port, msg, w, opts.keys ?? true));
73
+ };
74
+ port.addEventListener("message", first);
75
+ port.start();
76
+ port.postMessage({ t: "hello", api: VIEW_API });
77
+ };
78
+ function onWindow(e) {
79
+ if (e.source !== w) return;
80
+ const d = e.data;
81
+ const ours = d === VIEW_PORT_MESSAGE || typeof d === "object" && d !== null && d.type === VIEW_PORT_MESSAGE;
82
+ const port = e.ports?.[0];
83
+ if (ours && port) begin(port);
84
+ }
85
+ if (opts.port) begin(opts.port);
86
+ else w.addEventListener("message", onWindow);
87
+ });
88
+ }
89
+ function client(port, welcome, w, keys) {
90
+ let next = 1;
91
+ let closed = false;
92
+ const waiting = /* @__PURE__ */ new Map();
93
+ const listeners = /* @__PURE__ */ new Map();
94
+ const root = w.document.documentElement;
95
+ applyTheme(welcome.theme, root);
96
+ const onMessage = (e) => {
97
+ const msg = e.data;
98
+ if (!msg || typeof msg !== "object") return;
99
+ if (msg.t === "reply") {
100
+ const p = waiting.get(msg.id);
101
+ if (!p) return;
102
+ waiting.delete(msg.id);
103
+ if (msg.ok) p.resolve(msg.value);
104
+ else p.reject(new ViewRefusalError(msg.refusal));
105
+ } else if (msg.t === "event") {
106
+ if (msg.name === "theme") applyTheme(msg.value, root);
107
+ for (const fn of listeners.get(msg.name) ?? []) {
108
+ try {
109
+ fn(msg.value);
110
+ } catch (err) {
111
+ console.error(err);
112
+ }
113
+ }
114
+ }
115
+ };
116
+ port.addEventListener("message", onMessage);
117
+ const mac = isMac(w);
118
+ const onKey = (e) => {
119
+ if (e.defaultPrevented || closed || !(e.metaKey || e.ctrlKey)) return;
120
+ const combo = comboOf(e, mac);
121
+ if (!combo) return;
122
+ if (UNDO.has(combo)) e.preventDefault();
123
+ port.postMessage({ t: "key", combo });
124
+ };
125
+ if (keys) w.addEventListener("keydown", onKey);
126
+ const request = (op, args) => {
127
+ if (closed) return Promise.reject(new Error("This view's connection to the app is closed."));
128
+ const id = next++;
129
+ return new Promise((resolve, reject) => {
130
+ waiting.set(id, { resolve, reject });
131
+ port.postMessage({ t: "request", id, op, args });
132
+ });
133
+ };
134
+ return {
135
+ welcome,
136
+ request,
137
+ project: () => request("project", {}),
138
+ node: (id, o) => request("node", o?.children !== void 0 ? { id, children: o.children } : { id }),
139
+ selection: () => request("selection", {}),
140
+ media: (ids) => request("media", { ids }),
141
+ write: (label, edits) => request("write", { label, edits }),
142
+ select: (id) => request("select", { id }),
143
+ on(name, fn) {
144
+ let set = listeners.get(name);
145
+ if (!set) listeners.set(name, set = /* @__PURE__ */ new Set());
146
+ set.add(fn);
147
+ return () => {
148
+ set.delete(fn);
149
+ };
150
+ },
151
+ close() {
152
+ if (closed) return;
153
+ closed = true;
154
+ w.removeEventListener("keydown", onKey);
155
+ port.removeEventListener("message", onMessage);
156
+ port.close();
157
+ for (const p of waiting.values()) p.reject(new Error("This view's connection to the app is closed."));
158
+ waiting.clear();
159
+ }
160
+ };
161
+ }
162
+
163
+ export { ViewRefusalError, applyTheme, comboOf, connect };
164
+ //# sourceMappingURL=view-client.js.map
165
+ //# sourceMappingURL=view-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/view-client.ts"],"names":[],"mappings":";;;;AA6BO,IAAM,gBAAA,GAAN,cAA+B,KAAA,CAAM;AAAA,EACjC,OAAA;AAAA,EACT,YAAY,OAAA,EAAsB;AAChC,IAAA,KAAA,CAAM,QAAQ,OAAO,CAAA;AACrB,IAAA,IAAA,CAAK,IAAA,GAAO,kBAAA;AACZ,IAAA,IAAA,CAAK,OAAA,GAAU,OAAA;AAAA,EACjB;AACF;AA6BO,SAAS,UAAA,CAAW,KAAA,EAAkB,IAAA,GAAoB,QAAA,CAAS,eAAA,EAAuB;AAC/F,EAAA,KAAA,MAAW,CAAC,IAAA,EAAM,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,MAAM,MAAM,CAAA,EAAG,IAAI,IAAA,CAAK,WAAW,IAAI,CAAA,OAAQ,KAAA,CAAM,WAAA,CAAY,MAAM,KAAK,CAAA;AACvH,EAAA,IAAA,CAAK,KAAA,CAAM,WAAA,CAAY,cAAA,EAAgB,KAAA,CAAM,MAAM,CAAA;AACrD;AAEA,IAAM,KAAA,GAAQ,CAAC,CAAA,KAAuB;AACpC,EAAA,MAAM,MAAM,CAAA,CAAE,SAAA;AACd,EAAA,OAAO,kBAAA,CAAmB,KAAK,GAAA,CAAI,aAAA,EAAe,YAAY,GAAA,CAAI,QAAA,IAAY,GAAA,CAAI,SAAA,IAAa,EAAE,CAAA;AACnG,CAAA;AAEA,IAAM,KAAA,GAAgC;AAAA,EACpC,OAAA,EAAS,IAAA;AAAA,EAAM,SAAA,EAAW,MAAA;AAAA,EAAQ,SAAA,EAAW,MAAA;AAAA,EAAQ,UAAA,EAAY,OAAA;AAAA,EAAS,MAAA,EAAQ,KAAA;AAAA,EAAO,GAAA,EAAK;AAChG,CAAA;AAGO,SAAS,OAAA,CAAQ,GAAkB,GAAA,EAA6B;AACrE,EAAA,IAAI,CAAC,MAAA,EAAQ,SAAA,EAAW,OAAA,EAAS,KAAA,EAAO,IAAI,CAAA,CAAE,QAAA,CAAS,CAAA,CAAE,GAAG,CAAA,EAAG,OAAO,IAAA;AACtE,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,IAAI,MAAM,CAAA,CAAE,OAAA,GAAU,EAAE,OAAA,EAAS,KAAA,CAAM,KAAK,KAAK,CAAA;AACjD,EAAA,IAAI,GAAA,IAAO,CAAA,CAAE,OAAA,EAAS,KAAA,CAAM,KAAK,MAAM,CAAA;AACvC,EAAA,IAAI,CAAA,CAAE,MAAA,EAAQ,KAAA,CAAM,IAAA,CAAK,KAAK,CAAA;AAC9B,EAAA,IAAI,CAAA,CAAE,QAAA,EAAU,KAAA,CAAM,IAAA,CAAK,OAAO,CAAA;AAClC,EAAA,IAAI,GAAA;AACJ,EAAA,IAAI,YAAA,CAAa,KAAK,CAAA,CAAE,IAAI,GAAG,GAAA,GAAM,CAAA,CAAE,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA;AAAA,OAAA,IAC1C,cAAA,CAAe,KAAK,CAAA,CAAE,IAAI,GAAG,GAAA,GAAM,CAAA,CAAE,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA;AAAA,OACrD,GAAA,GAAM,KAAA,CAAM,CAAA,CAAE,GAAG,CAAA,KAAM,CAAA,CAAE,GAAA,CAAI,MAAA,KAAW,CAAA,GAAI,CAAA,CAAE,GAAA,CAAI,WAAA,KAAgB,CAAA,CAAE,GAAA,CAAA;AACzE,EAAA,KAAA,CAAM,KAAK,GAAG,CAAA;AACd,EAAA,OAAO,KAAA,CAAM,KAAK,GAAG,CAAA;AACvB;AAGA,IAAM,uBAAO,IAAI,GAAA,CAAI,CAAC,OAAA,EAAS,aAAA,EAAe,OAAO,CAAC,CAAA;AAG/C,SAAS,OAAA,CAAQ,IAAA,GAAuB,EAAC,EAAwB;AACtE,EAAA,MAAM,CAAA,GAAI,KAAK,MAAA,IAAU,MAAA;AACzB,EAAA,MAAM,SAAA,GAAY,KAAK,SAAA,IAAa,GAAA;AACpC,EAAA,OAAO,IAAI,OAAA,CAAoB,CAAC,OAAA,EAAS,MAAA,KAAW;AAClD,IAAA,IAAI,OAAA,GAAU,KAAA;AACd,IAAA,MAAM,IAAA,GAAO,CAAC,OAAA,EAAiB,IAAA,KAA6B;AAC1D,MAAA,IAAI,OAAA,EAAS;AACb,MAAA,OAAA,GAAU,IAAA;AACV,MAAA,YAAA,CAAa,KAAK,CAAA;AAClB,MAAA,CAAA,CAAE,mBAAA,CAAoB,WAAW,QAAQ,CAAA;AACzC,MAAA,IAAA,EAAM,KAAA,EAAM;AACZ,MAAA,MAAA,CAAO,IAAI,KAAA,CAAM,OAAO,CAAC,CAAA;AAAA,IAC3B,CAAA;AACA,IAAA,MAAM,QAAQ,UAAA,CAAW,MAAM,IAAA,CAAK,0CAA0C,GAAG,SAAS,CAAA;AAE1F,IAAA,MAAM,KAAA,GAAQ,CAAC,IAAA,KAA4B;AACzC,MAAA,CAAA,CAAE,mBAAA,CAAoB,WAAW,QAAQ,CAAA;AACzC,MAAA,MAAM,KAAA,GAAQ,CAAC,CAAA,KAA0B;AACvC,QAAA,MAAM,MAAM,CAAA,CAAE,IAAA;AACd,QAAA,IAAI,CAAC,GAAA,IAAO,OAAO,GAAA,KAAQ,QAAA,EAAU;AACrC,QAAA,IAAI,GAAA,CAAI,MAAM,SAAA,EAAW;AAAE,UAAA,IAAA,CAAK,mBAAA,CAAoB,WAAW,KAAsB,CAAA;AAAG,UAAA,IAAA,CAAK,GAAA,CAAI,SAAS,IAAI,CAAA;AAAG,UAAA;AAAA,QAAQ;AACzH,QAAA,IAAI,GAAA,CAAI,CAAA,KAAM,SAAA,IAAa,OAAA,EAAS;AACpC,QAAA,OAAA,GAAU,IAAA;AACV,QAAA,YAAA,CAAa,KAAK,CAAA;AAClB,QAAA,IAAA,CAAK,mBAAA,CAAoB,WAAW,KAAsB,CAAA;AAC1D,QAAA,OAAA,CAAQ,OAAO,IAAA,EAAM,GAAA,EAAK,GAAG,IAAA,CAAK,IAAA,IAAQ,IAAI,CAAC,CAAA;AAAA,MACjD,CAAA;AACA,MAAA,IAAA,CAAK,gBAAA,CAAiB,WAAW,KAAsB,CAAA;AACvD,MAAA,IAAA,CAAK,KAAA,EAAM;AACX,MAAA,IAAA,CAAK,YAAY,EAAE,CAAA,EAAG,OAAA,EAAS,GAAA,EAAK,UAAU,CAAA;AAAA,IAChD,CAAA;AACA,IAAA,SAAS,SAAS,CAAA,EAAuB;AACvC,MAAA,IAAI,CAAA,CAAE,WAAW,CAAA,EAAG;AACpB,MAAA,MAAM,IAAI,CAAA,CAAE,IAAA;AACZ,MAAA,MAAM,IAAA,GAAO,MAAM,iBAAA,IAAsB,OAAO,MAAM,QAAA,IAAY,CAAA,KAAM,IAAA,IAAS,CAAA,CAAyB,IAAA,KAAS,iBAAA;AACnH,MAAA,MAAM,IAAA,GAAO,CAAA,CAAE,KAAA,GAAQ,CAAC,CAAA;AACxB,MAAA,IAAI,IAAA,IAAQ,IAAA,EAAM,KAAA,CAAM,IAAI,CAAA;AAAA,IAC9B;AACA,IAAA,IAAI,IAAA,CAAK,IAAA,EAAM,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA;AAAA,SACzB,CAAA,CAAE,gBAAA,CAAiB,SAAA,EAAW,QAAQ,CAAA;AAAA,EAC7C,CAAC,CAAA;AACH;AAEA,SAAS,MAAA,CAAO,IAAA,EAAmB,OAAA,EAAsB,CAAA,EAAW,IAAA,EAA2B;AAC7F,EAAA,IAAI,IAAA,GAAO,CAAA;AACX,EAAA,IAAI,MAAA,GAAS,KAAA;AACb,EAAA,MAAM,OAAA,uBAAc,GAAA,EAA2E;AAC/F,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAgD;AACtE,EAAA,MAAM,IAAA,GAAO,EAAE,QAAA,CAAS,eAAA;AACxB,EAAA,UAAA,CAAW,OAAA,CAAQ,OAAO,IAAI,CAAA;AAE9B,EAAA,MAAM,SAAA,GAAY,CAAC,CAAA,KAA0B;AAC3C,IAAA,MAAM,MAAM,CAAA,CAAE,IAAA;AACd,IAAA,IAAI,CAAC,GAAA,IAAO,OAAO,GAAA,KAAQ,QAAA,EAAU;AACrC,IAAA,IAAI,GAAA,CAAI,MAAM,OAAA,EAAS;AACrB,MAAA,MAAM,CAAA,GAAI,OAAA,CAAQ,GAAA,CAAI,GAAA,CAAI,EAAE,CAAA;AAC5B,MAAA,IAAI,CAAC,CAAA,EAAG;AACR,MAAA,OAAA,CAAQ,MAAA,CAAO,IAAI,EAAE,CAAA;AACrB,MAAA,IAAI,GAAA,CAAI,EAAA,EAAI,CAAA,CAAE,OAAA,CAAQ,IAAI,KAAK,CAAA;AAAA,aACxB,MAAA,CAAO,IAAI,gBAAA,CAAiB,GAAA,CAAI,OAAO,CAAC,CAAA;AAAA,IACjD,CAAA,MAAA,IAAW,GAAA,CAAI,CAAA,KAAM,OAAA,EAAS;AAC5B,MAAA,IAAI,IAAI,IAAA,KAAS,OAAA,EAAS,UAAA,CAAW,GAAA,CAAI,OAAoB,IAAI,CAAA;AACjE,MAAA,KAAA,MAAW,MAAM,SAAA,CAAU,GAAA,CAAI,IAAI,IAAI,CAAA,IAAK,EAAC,EAAG;AAC9C,QAAA,IAAI;AAAE,UAAC,EAAA,CAA4B,IAAI,KAAK,CAAA;AAAA,QAAG,SAAS,GAAA,EAAK;AAAE,UAAA,OAAA,CAAQ,MAAM,GAAG,CAAA;AAAA,QAAG;AAAA,MACrF;AAAA,IACF;AAAA,EACF,CAAA;AACA,EAAA,IAAA,CAAK,gBAAA,CAAiB,WAAW,SAA0B,CAAA;AAE3D,EAAA,MAAM,GAAA,GAAM,MAAM,CAAC,CAAA;AACnB,EAAA,MAAM,KAAA,GAAQ,CAAC,CAAA,KAA2B;AACxC,IAAA,IAAI,EAAE,gBAAA,IAAoB,MAAA,IAAU,EAAE,CAAA,CAAE,OAAA,IAAW,EAAE,OAAA,CAAA,EAAU;AAC/D,IAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,CAAA,EAAG,GAAG,CAAA;AAC5B,IAAA,IAAI,CAAC,KAAA,EAAO;AACZ,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,KAAK,CAAA,IAAK,cAAA,EAAe;AACtC,IAAA,IAAA,CAAK,WAAA,CAAY,EAAE,CAAA,EAAG,KAAA,EAAO,OAAO,CAAA;AAAA,EACtC,CAAA;AACA,EAAA,IAAI,IAAA,EAAM,CAAA,CAAE,gBAAA,CAAiB,SAAA,EAAW,KAAK,CAAA;AAE7C,EAAA,MAAM,OAAA,GAAU,CAAmB,EAAA,EAAO,IAAA,KAAqE;AAC7G,IAAA,IAAI,QAAQ,OAAO,OAAA,CAAQ,OAAO,IAAI,KAAA,CAAM,8CAA8C,CAAC,CAAA;AAC3F,IAAA,MAAM,EAAA,GAAK,IAAA,EAAA;AACX,IAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AACtC,MAAA,OAAA,CAAQ,GAAA,CAAI,EAAA,EAAI,EAAE,OAAA,EAA0C,QAAQ,CAAA;AACpE,MAAA,IAAA,CAAK,YAAY,EAAE,CAAA,EAAG,WAAW,EAAA,EAAI,EAAA,EAAI,MAAM,CAAA;AAAA,IACjD,CAAC,CAAA;AAAA,EACH,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,OAAA;AAAA,IACA,OAAA;AAAA,IACA,OAAA,EAAS,MAAM,OAAA,CAAQ,SAAA,EAAW,EAAE,CAAA;AAAA,IACpC,MAAM,CAAC,EAAA,EAAI,CAAA,KAAM,OAAA,CAAQ,QAAQ,CAAA,EAAG,QAAA,KAAa,MAAA,GAAY,EAAE,IAAI,QAAA,EAAU,CAAA,CAAE,UAAS,GAAI,EAAE,IAAI,CAAA;AAAA,IAClG,SAAA,EAAW,MAAM,OAAA,CAAQ,WAAA,EAAa,EAAE,CAAA;AAAA,IACxC,OAAO,CAAC,GAAA,KAAQ,QAAQ,OAAA,EAAS,EAAE,KAAK,CAAA;AAAA,IACxC,KAAA,EAAO,CAAC,KAAA,EAAO,KAAA,KAAU,QAAQ,OAAA,EAAS,EAAE,KAAA,EAAO,KAAA,EAAO,CAAA;AAAA,IAC1D,QAAQ,CAAC,EAAA,KAAO,QAAQ,QAAA,EAAU,EAAE,IAAI,CAAA;AAAA,IACxC,EAAA,CAAG,MAAM,EAAA,EAAI;AACX,MAAA,IAAI,GAAA,GAAM,SAAA,CAAU,GAAA,CAAI,IAAI,CAAA;AAC5B,MAAA,IAAI,CAAC,KAAK,SAAA,CAAU,GAAA,CAAI,MAAO,GAAA,mBAAM,IAAI,KAAM,CAAA;AAC/C,MAAA,GAAA,CAAI,IAAI,EAA4B,CAAA;AACpC,MAAA,OAAO,MAAM;AAAE,QAAA,GAAA,CAAI,OAAO,EAA4B,CAAA;AAAA,MAAG,CAAA;AAAA,IAC3D,CAAA;AAAA,IACA,KAAA,GAAQ;AACN,MAAA,IAAI,MAAA,EAAQ;AACZ,MAAA,MAAA,GAAS,IAAA;AACT,MAAA,CAAA,CAAE,mBAAA,CAAoB,WAAW,KAAK,CAAA;AACtC,MAAA,IAAA,CAAK,mBAAA,CAAoB,WAAW,SAA0B,CAAA;AAC9D,MAAA,IAAA,CAAK,KAAA,EAAM;AACX,MAAA,KAAA,MAAW,CAAA,IAAK,QAAQ,MAAA,EAAO,IAAK,MAAA,CAAO,IAAI,KAAA,CAAM,8CAA8C,CAAC,CAAA;AACpG,MAAA,OAAA,CAAQ,KAAA,EAAM;AAAA,IAChB;AAAA,GACF;AACF","file":"view-client.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The view client (tier 2 view extensions), for VIEW AUTHORS: what a view's\n// web component imports to talk to the app it runs in. Tiny and\n// dependency-free: it imports nothing but the protocol's types and constants.\n//\n// const app = await connect();\n// const sel = await app.selection();\n// app.on(\"changed\", () => redraw());\n//\n// Call `connect()` as the view's module loads (the port arrives once the page\n// has loaded, and a listener added later misses it). It applies the app's\n// theme to the page's root as custom properties (`var(--ink)`, ...), and\n// passes on key combinations the view doesn't handle (Mod+Z runs the\n// editor's undo); plain typing never leaves the view.\n// ---------------------------------------------------------------------------\n\nimport { VIEW_API, VIEW_PORT_MESSAGE } from \"./view-protocol.js\";\nimport type {\n HostToView, ViewEdit, ViewEventName, ViewEvents, ViewMediaLength, ViewNode, ViewOp, ViewRefusal, ViewRequests,\n ViewSelection, ViewTheme, ViewWelcome,\n} from \"./view-protocol.js\";\n\nexport type {\n HostToView, ViewEdit, ViewEventName, ViewEvents, ViewHostName, ViewJson, ViewMediaLength, ViewNode, ViewOp,\n ViewRefusal, ViewRequests, ViewSelection, ViewTheme, ViewWelcome, ViewWritable,\n} from \"./view-protocol.js\";\nexport { VIEW_API } from \"./view-protocol.js\";\n\n/** A refused request: the host's sentence as the message, and its refusal (`code`) for the view to act on. */\nexport class ViewRefusalError extends Error {\n readonly refusal: ViewRefusal;\n constructor(refusal: ViewRefusal) {\n super(refusal.message);\n this.name = \"ViewRefusalError\";\n this.refusal = refusal;\n }\n}\n\nexport interface ConnectOptions {\n /** How long to wait for the port and the app's welcome, in ms (default 10000). */\n timeoutMs?: number;\n /** A port already in hand, instead of waiting for the preload's (tests, or a host of another kind). */\n port?: MessagePort;\n /** The window to listen on and apply the theme and keys to (default `window`). */\n window?: Window;\n /** Pass unhandled key combinations on to the app (default true). */\n keys?: boolean;\n}\n\nexport interface ViewClient {\n /** What the app said on welcoming the view: host, extension, view, settings, theme, writable. */\n readonly welcome: ViewWelcome;\n request<K extends ViewOp>(op: K, args: ViewRequests[K][\"args\"]): Promise<ViewRequests[K][\"value\"]>;\n project(): Promise<ViewNode>;\n node(id: string, opts?: { children?: boolean }): Promise<ViewNode | null>;\n selection(): Promise<ViewSelection>;\n media(ids: string[]): Promise<{ [id: string]: ViewMediaLength }>;\n write(label: string, edits: ViewEdit[]): Promise<{ written: number }>;\n select(id: string): Promise<boolean>;\n /** Listen for an event; returns the unsubscribe. */\n on<K extends ViewEventName>(name: K, fn: (value: ViewEvents[K]) => void): () => void;\n close(): void;\n}\n\n/** Set the theme's tokens as custom properties on the root, and its colour scheme. */\nexport function applyTheme(theme: ViewTheme, root: HTMLElement = document.documentElement): void {\n for (const [name, value] of Object.entries(theme.tokens)) if (name.startsWith(\"--\")) root.style.setProperty(name, value);\n root.style.setProperty(\"color-scheme\", theme.scheme);\n}\n\nconst isMac = (w: Window): boolean => {\n const nav = w.navigator as Navigator & { userAgentData?: { platform?: string } };\n return /mac|iphone|ipad/i.test(nav.userAgentData?.platform ?? nav.platform ?? nav.userAgent ?? \"\");\n};\n\nconst NAMED: Record<string, string> = {\n ArrowUp: \"Up\", ArrowDown: \"Down\", ArrowLeft: \"Left\", ArrowRight: \"Right\", Escape: \"Esc\", \" \": \"Space\",\n};\n\n/** A key event in the shell's portable spelling (\"Mod+Shift+Z\"), or null for a bare modifier. */\nexport function comboOf(e: KeyboardEvent, mac: boolean): string | null {\n if ([\"Meta\", \"Control\", \"Shift\", \"Alt\", \"OS\"].includes(e.key)) return null;\n const parts: string[] = [];\n if (mac ? e.metaKey : e.ctrlKey) parts.push(\"Mod\");\n if (mac && e.ctrlKey) parts.push(\"Ctrl\");\n if (e.altKey) parts.push(\"Alt\");\n if (e.shiftKey) parts.push(\"Shift\");\n let key: string;\n if (/^Key[A-Z]$/.test(e.code)) key = e.code.slice(3);\n else if (/^Digit[0-9]$/.test(e.code)) key = e.code.slice(5);\n else key = NAMED[e.key] ?? (e.key.length === 1 ? e.key.toUpperCase() : e.key);\n parts.push(key);\n return parts.join(\"+\");\n}\n\n/** The combinations whose page default is the page's own undo: the app's runs instead. */\nconst UNDO = new Set([\"Mod+Z\", \"Mod+Shift+Z\", \"Mod+Y\"]);\n\n/** Wait for the app's port, say hello, and resolve once the app welcomes the view (rejected with its sentence). */\nexport function connect(opts: ConnectOptions = {}): Promise<ViewClient> {\n const w = opts.window ?? window;\n const timeoutMs = opts.timeoutMs ?? 10000;\n return new Promise<ViewClient>((resolve, reject) => {\n let settled = false;\n const fail = (message: string, port?: MessagePort): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n w.removeEventListener(\"message\", onWindow);\n port?.close();\n reject(new Error(message));\n };\n const timer = setTimeout(() => fail(\"The app didn't answer this view in time.\"), timeoutMs);\n\n const begin = (port: MessagePort): void => {\n w.removeEventListener(\"message\", onWindow);\n const first = (e: MessageEvent): void => {\n const msg = e.data as HostToView;\n if (!msg || typeof msg !== \"object\") return;\n if (msg.t === \"refused\") { port.removeEventListener(\"message\", first as EventListener); fail(msg.message, port); return; }\n if (msg.t !== \"welcome\" || settled) return;\n settled = true;\n clearTimeout(timer);\n port.removeEventListener(\"message\", first as EventListener);\n resolve(client(port, msg, w, opts.keys ?? true));\n };\n port.addEventListener(\"message\", first as EventListener);\n port.start();\n port.postMessage({ t: \"hello\", api: VIEW_API });\n };\n function onWindow(e: MessageEvent): void {\n if (e.source !== w) return;\n const d = e.data as unknown;\n const ours = d === VIEW_PORT_MESSAGE || (typeof d === \"object\" && d !== null && (d as { type?: unknown }).type === VIEW_PORT_MESSAGE);\n const port = e.ports?.[0];\n if (ours && port) begin(port);\n }\n if (opts.port) begin(opts.port);\n else w.addEventListener(\"message\", onWindow);\n });\n}\n\nfunction client(port: MessagePort, welcome: ViewWelcome, w: Window, keys: boolean): ViewClient {\n let next = 1;\n let closed = false;\n const waiting = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();\n const listeners = new Map<ViewEventName, Set<(value: never) => void>>();\n const root = w.document.documentElement;\n applyTheme(welcome.theme, root);\n\n const onMessage = (e: MessageEvent): void => {\n const msg = e.data as HostToView;\n if (!msg || typeof msg !== \"object\") return;\n if (msg.t === \"reply\") {\n const p = waiting.get(msg.id);\n if (!p) return;\n waiting.delete(msg.id);\n if (msg.ok) p.resolve(msg.value);\n else p.reject(new ViewRefusalError(msg.refusal));\n } else if (msg.t === \"event\") {\n if (msg.name === \"theme\") applyTheme(msg.value as ViewTheme, root);\n for (const fn of listeners.get(msg.name) ?? []) {\n try { (fn as (v: unknown) => void)(msg.value); } catch (err) { console.error(err); }\n }\n }\n };\n port.addEventListener(\"message\", onMessage as EventListener);\n\n const mac = isMac(w);\n const onKey = (e: KeyboardEvent): void => {\n if (e.defaultPrevented || closed || !(e.metaKey || e.ctrlKey)) return;\n const combo = comboOf(e, mac);\n if (!combo) return;\n if (UNDO.has(combo)) e.preventDefault();\n port.postMessage({ t: \"key\", combo });\n };\n if (keys) w.addEventListener(\"keydown\", onKey);\n\n const request = <K extends ViewOp>(op: K, args: ViewRequests[K][\"args\"]): Promise<ViewRequests[K][\"value\"]> => {\n if (closed) return Promise.reject(new Error(\"This view's connection to the app is closed.\"));\n const id = next++;\n return new Promise((resolve, reject) => {\n waiting.set(id, { resolve: resolve as (v: unknown) => void, reject });\n port.postMessage({ t: \"request\", id, op, args });\n });\n };\n\n return {\n welcome,\n request,\n project: () => request(\"project\", {}),\n node: (id, o) => request(\"node\", o?.children !== undefined ? { id, children: o.children } : { id }),\n selection: () => request(\"selection\", {}),\n media: (ids) => request(\"media\", { ids }),\n write: (label, edits) => request(\"write\", { label, edits }),\n select: (id) => request(\"select\", { id }),\n on(name, fn) {\n let set = listeners.get(name);\n if (!set) listeners.set(name, (set = new Set()));\n set.add(fn as (value: never) => void);\n return () => { set.delete(fn as (value: never) => void); };\n },\n close() {\n if (closed) return;\n closed = true;\n w.removeEventListener(\"keydown\", onKey);\n port.removeEventListener(\"message\", onMessage as EventListener);\n port.close();\n for (const p of waiting.values()) p.reject(new Error(\"This view's connection to the app is closed.\"));\n waiting.clear();\n },\n };\n}\n"]}