@wildwinter/app-shell 0.56.0 → 0.58.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 (58) hide show
  1. package/README.md +215 -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-CPNTROWD.js → chunk-OZQ2BYXH.js} +13 -4
  7. package/dist/chunk-OZQ2BYXH.js.map +1 -0
  8. package/dist/controls.css +52 -0
  9. package/dist/controls.css.map +1 -1
  10. package/dist/index.cjs +352 -3
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +17 -1
  13. package/dist/index.d.ts +17 -1
  14. package/dist/index.js +24 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/tool-window.cjs +11 -2
  17. package/dist/tool-window.cjs.map +1 -1
  18. package/dist/tool-window.d.cts +10 -4
  19. package/dist/tool-window.d.ts +10 -4
  20. package/dist/tool-window.js +1 -1
  21. package/dist/updater.cjs.map +1 -1
  22. package/dist/updater.js +1 -1
  23. package/dist/value-rows.cjs +200 -10
  24. package/dist/value-rows.cjs.map +1 -1
  25. package/dist/value-rows.d.cts +36 -1
  26. package/dist/value-rows.d.ts +36 -1
  27. package/dist/value-rows.js +197 -12
  28. package/dist/value-rows.js.map +1 -1
  29. package/dist/view-client.cjs +172 -0
  30. package/dist/view-client.cjs.map +1 -0
  31. package/dist/view-client.d.cts +46 -0
  32. package/dist/view-client.d.ts +46 -0
  33. package/dist/view-client.js +165 -0
  34. package/dist/view-client.js.map +1 -0
  35. package/dist/view-host-web.cjs +327 -0
  36. package/dist/view-host-web.cjs.map +1 -0
  37. package/dist/view-host-web.d.cts +61 -0
  38. package/dist/view-host-web.d.ts +61 -0
  39. package/dist/view-host-web.js +4 -0
  40. package/dist/view-host-web.js.map +1 -0
  41. package/dist/view-host.cjs +532 -0
  42. package/dist/view-host.cjs.map +1 -0
  43. package/dist/view-host.d.cts +178 -0
  44. package/dist/view-host.d.ts +178 -0
  45. package/dist/view-host.js +426 -0
  46. package/dist/view-host.js.map +1 -0
  47. package/dist/view-preload.cjs +16 -0
  48. package/dist/view-preload.cjs.map +1 -0
  49. package/dist/view-preload.js +8 -0
  50. package/dist/view-preload.js.map +1 -0
  51. package/dist/view-protocol.cjs +12 -0
  52. package/dist/view-protocol.cjs.map +1 -0
  53. package/dist/view-protocol.d.cts +257 -0
  54. package/dist/view-protocol.d.ts +257 -0
  55. package/dist/view-protocol.js +3 -0
  56. package/dist/view-protocol.js.map +1 -0
  57. package/package.json +25 -4
  58. package/dist/chunk-CPNTROWD.js.map +0 -1
@@ -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":"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","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"]}
@@ -0,0 +1,327 @@
1
+ 'use strict';
2
+
3
+ // src/view-protocol.ts
4
+ var VIEW_API = 1;
5
+ var VIEW_PORT_MESSAGE = "wildwinter-view:port";
6
+
7
+ // src/view-host-web.ts
8
+ var PALETTE_TOKENS = [
9
+ "--bg",
10
+ "--surface",
11
+ "--card",
12
+ "--ink",
13
+ "--muted",
14
+ "--line",
15
+ "--line-soft",
16
+ "--accent",
17
+ "--accent-soft",
18
+ "--on-accent",
19
+ "--danger",
20
+ "--warn",
21
+ "--ok",
22
+ "--font-ui",
23
+ "--font-read",
24
+ "--font-mono",
25
+ ...Array.from({ length: 12 }, (_, i) => `--char-${i}`)
26
+ ];
27
+ var GRAMMAR_TOKENS = [
28
+ "--color-scheme",
29
+ "--chrome-size",
30
+ "--scrim",
31
+ "--shadow-1",
32
+ "--shadow-2",
33
+ "--shadow-3",
34
+ "--ease-standard",
35
+ "--dur-quick",
36
+ "--dur-base",
37
+ "--dur-settle",
38
+ "--dur-slow",
39
+ "--dur-panel",
40
+ "--dur-exit",
41
+ "--radius-sm",
42
+ "--radius-md",
43
+ "--radius-lg",
44
+ "--radius-pill",
45
+ "--scroll-thumb",
46
+ "--scroll-thumb-hover"
47
+ ];
48
+ var VIEW_THEME_TOKENS = [...PALETTE_TOKENS, ...GRAMMAR_TOKENS];
49
+ function readTheme(root = document.documentElement, tokens = VIEW_THEME_TOKENS) {
50
+ const style = getComputedStyle(root);
51
+ const out = {};
52
+ for (const name of tokens) {
53
+ const value = style.getPropertyValue(name).trim();
54
+ if (value !== "") out[name] = value;
55
+ }
56
+ const declared = (style.getPropertyValue("color-scheme") || style.colorScheme || "").trim();
57
+ const words = declared.split(/\s+/);
58
+ let scheme = "light";
59
+ if (declared === "dark") scheme = "dark";
60
+ else if (words.includes("light") && words.includes("dark")) {
61
+ scheme = typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
62
+ }
63
+ return { scheme, tokens: out };
64
+ }
65
+ function onViewPort(fn, target = window, closed) {
66
+ const listener = (e) => {
67
+ if (e.source !== target) return;
68
+ const data = e.data;
69
+ if (!data || typeof data !== "object" || data.type !== VIEW_PORT_MESSAGE) return;
70
+ const port = e.ports?.[0];
71
+ const meta = data.meta;
72
+ if (!meta || typeof meta.window !== "number" || typeof meta.extension !== "string" || typeof meta.view !== "string") return;
73
+ if (meta.closed === true) {
74
+ closed?.(meta);
75
+ return;
76
+ }
77
+ if (!port) return;
78
+ fn(port, meta);
79
+ };
80
+ target.addEventListener("message", listener);
81
+ return () => target.removeEventListener("message", listener);
82
+ }
83
+ var NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
84
+ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
85
+ var isId = (v) => typeof v === "string" && v !== "";
86
+ function isJson(v, depth = 0) {
87
+ if (depth > 64) return false;
88
+ if (v === null || typeof v === "string" || typeof v === "boolean") return true;
89
+ if (typeof v === "number") return Number.isFinite(v);
90
+ if (Array.isArray(v)) return v.every((x) => isJson(x, depth + 1));
91
+ if (isObj(v)) return Object.getPrototypeOf(v) === Object.prototype && Object.values(v).every((x) => isJson(x, depth + 1));
92
+ return false;
93
+ }
94
+ var bad = (message) => ({ code: "bad-request", message });
95
+ function argsProblem(op, args) {
96
+ switch (op) {
97
+ case "project":
98
+ case "selection":
99
+ return null;
100
+ case "node":
101
+ if (!isId(args["id"])) return "A node request names the node by its id.";
102
+ if (args["children"] !== void 0 && typeof args["children"] !== "boolean") return "A node request's children is true or false.";
103
+ return null;
104
+ case "media":
105
+ if (!Array.isArray(args["ids"]) || !args["ids"].every(isId)) return "A media request lists the lines' ids.";
106
+ return null;
107
+ case "select":
108
+ return isId(args["id"]) ? null : "A select request names the node by its id.";
109
+ case "write": {
110
+ if (typeof args["label"] !== "string" || args["label"].trim() === "") return "A write has a label, for the undo step.";
111
+ const edits = args["edits"];
112
+ if (!Array.isArray(edits)) return "A write lists its edits.";
113
+ for (const e of edits) {
114
+ if (!isObj(e) || !isId(e["node"])) return "Each edit names its node by id.";
115
+ if (typeof e["field"] !== "string" || !NAME.test(e["field"])) return "Each edit names one of the extension's fields, never a path inside one.";
116
+ if ("value" in e && e["value"] !== void 0 && !isJson(e["value"])) return `The value for ${e["field"]} isn't plain JSON.`;
117
+ }
118
+ return null;
119
+ }
120
+ }
121
+ }
122
+ var OPS = ["project", "node", "selection", "media", "write", "select"];
123
+ function hostView(port, adapter, opts) {
124
+ const { meta } = opts;
125
+ const ext = meta.extension;
126
+ const allowed = /* @__PURE__ */ new Set([ext, ...opts.reads]);
127
+ const theme = opts.theme ?? (() => readTheme());
128
+ const debounceMs = opts.debounceMs ?? 100;
129
+ let welcomed = false;
130
+ let closed = false;
131
+ let pending = null;
132
+ let timer;
133
+ const post = (msg) => {
134
+ if (!closed) port.postMessage(msg);
135
+ };
136
+ const emit = (name, value) => {
137
+ if (welcomed) post({ t: "event", name, value });
138
+ };
139
+ const filterNode = (node) => {
140
+ const keptExt = {};
141
+ for (const [id, data] of Object.entries(node.ext ?? {})) if (allowed.has(id)) keptExt[id] = data;
142
+ const base = { id: node.id, kind: node.kind, parent: node.parent, label: node.label, host: node.host ?? {}, ext: keptExt };
143
+ const out = typeof node.held === "string" ? { ...base, held: node.held } : base;
144
+ return node.children ? { ...out, children: node.children.map(filterNode) } : out;
145
+ };
146
+ const answer = async (op, args) => {
147
+ switch (op) {
148
+ case "project":
149
+ return { value: filterNode(await adapter.project()) };
150
+ case "node": {
151
+ const node = await adapter.node(args["id"], args["children"] === true);
152
+ return { value: node ? filterNode(node) : null };
153
+ }
154
+ case "selection":
155
+ return { value: await adapter.selection() };
156
+ case "media":
157
+ if (!adapter.media) return { refusal: { code: "unsupported", message: "This app has no recordings or timings for lines." } };
158
+ return { value: await adapter.media(args["ids"]) };
159
+ case "select":
160
+ return { value: await adapter.select(args["id"]) };
161
+ case "write": {
162
+ const writable = adapter.writable(ext);
163
+ if (writable.readOnly) return { refusal: { code: "read-only", message: writable.why ?? "This project is read-only just now." } };
164
+ const edits = args["edits"].map((e) => "value" in e && e.value !== void 0 ? { node: e.node, field: e.field, value: e.value } : { node: e.node, field: e.field });
165
+ if (edits.length === 0) return { value: { written: 0 } };
166
+ if (edits.length > 1) {
167
+ const projectId = (await adapter.project()).id;
168
+ const onProject = edits.filter((e) => e.node === projectId).length;
169
+ if (onProject > 0 && onProject < edits.length) {
170
+ return { refusal: bad("A write changes the project or other nodes, not both: they undo separately. Make two writes.") };
171
+ }
172
+ }
173
+ const refused = await adapter.write(ext, args["label"], edits);
174
+ if (refused) return { refusal: refused };
175
+ return { value: { written: edits.length } };
176
+ }
177
+ }
178
+ };
179
+ const request = async (msg) => {
180
+ const id = msg["id"];
181
+ if (typeof id !== "number" || !Number.isInteger(id)) return;
182
+ const reply = (r) => post("refusal" in r ? { t: "reply", id, ok: false, refusal: r.refusal } : { t: "reply", id, ok: true, value: r.value });
183
+ const op = msg["op"];
184
+ if (typeof op !== "string" || !OPS.includes(op)) {
185
+ reply({ refusal: bad(`There's no request called ${JSON.stringify(op)}.`) });
186
+ return;
187
+ }
188
+ const args = msg["args"] ?? {};
189
+ if (!isObj(args)) {
190
+ reply({ refusal: bad("A request's args are an object.") });
191
+ return;
192
+ }
193
+ const problem = argsProblem(op, args);
194
+ if (problem) {
195
+ reply({ refusal: bad(problem) });
196
+ return;
197
+ }
198
+ try {
199
+ reply(await answer(op, args));
200
+ } catch (e) {
201
+ reply({ refusal: { code: "failed", message: `The app couldn't do that: ${e instanceof Error ? e.message : String(e)}` } });
202
+ }
203
+ };
204
+ const hello = (msg) => {
205
+ const api = msg["api"];
206
+ if (api !== VIEW_API) {
207
+ const who = `${opts.names?.extension ?? ext}'s ${opts.names?.view ?? meta.view}`;
208
+ const message = typeof api === "number" ? `${who} was built for view API ${api}; this app serves ${VIEW_API}.` : `${who} didn't say which view API it was built for; this app serves ${VIEW_API}.`;
209
+ post({ t: "refused", message });
210
+ close(false);
211
+ return;
212
+ }
213
+ const welcome = {
214
+ t: "welcome",
215
+ api: VIEW_API,
216
+ host: adapter.host,
217
+ extension: { id: ext, version: meta.version, ...meta.pinned !== void 0 ? { pinned: meta.pinned } : {} },
218
+ view: meta.view,
219
+ settings: adapter.settings(ext),
220
+ theme: theme(),
221
+ writable: adapter.writable(ext)
222
+ };
223
+ welcomed = true;
224
+ post(welcome);
225
+ };
226
+ const onMessage = (e) => {
227
+ const msg = e.data;
228
+ if (closed || !isObj(msg)) return;
229
+ if (!welcomed) {
230
+ if (msg["t"] === "hello") hello(msg);
231
+ return;
232
+ }
233
+ if (msg["t"] === "request") void request(msg);
234
+ else if (msg["t"] === "key" && typeof msg["combo"] === "string") adapter.key?.(msg["combo"]);
235
+ };
236
+ port.addEventListener("message", onMessage);
237
+ port.start();
238
+ const flush = () => {
239
+ timer = void 0;
240
+ const nodes = pending;
241
+ pending = null;
242
+ if (nodes === null) return;
243
+ emit("changed", { nodes: nodes === "all" ? "all" : [...nodes] });
244
+ };
245
+ function close(sayClosing = true) {
246
+ if (closed) return;
247
+ if (sayClosing) emit("closing", {});
248
+ clearTimeout(timer);
249
+ closed = true;
250
+ port.removeEventListener("message", onMessage);
251
+ port.close();
252
+ }
253
+ return {
254
+ changed(nodes) {
255
+ if (closed || !welcomed) return;
256
+ if (nodes === "all" || pending === "all") pending = "all";
257
+ else {
258
+ pending ??= /* @__PURE__ */ new Set();
259
+ for (const n of nodes) pending.add(n);
260
+ }
261
+ if (timer === void 0) timer = setTimeout(flush, debounceMs);
262
+ },
263
+ selection() {
264
+ if (closed || !welcomed) return;
265
+ void Promise.resolve().then(() => adapter.selection()).then((s) => emit("selection", s), () => {
266
+ });
267
+ },
268
+ theme() {
269
+ if (!closed) emit("theme", theme());
270
+ },
271
+ writable() {
272
+ if (!closed) emit("writable", adapter.writable(ext));
273
+ },
274
+ close: () => close(true)
275
+ };
276
+ }
277
+ function hostViews(setup, opts = {}) {
278
+ const handles = /* @__PURE__ */ new Map();
279
+ const drop = (meta) => {
280
+ handles.get(meta.window)?.close();
281
+ handles.delete(meta.window);
282
+ };
283
+ const stop = onViewPort((port, meta) => {
284
+ drop(meta);
285
+ const s = setup(meta);
286
+ if (!s) {
287
+ port.close();
288
+ return;
289
+ }
290
+ handles.set(meta.window, hostView(port, s.adapter, {
291
+ meta,
292
+ reads: s.reads,
293
+ ...s.names !== void 0 ? { names: s.names } : {},
294
+ ...opts.theme !== void 0 ? { theme: opts.theme } : {},
295
+ ...opts.debounceMs !== void 0 ? { debounceMs: opts.debounceMs } : {}
296
+ }));
297
+ }, opts.target ?? window, drop);
298
+ const each = (f) => {
299
+ for (const h of handles.values()) f(h);
300
+ };
301
+ return {
302
+ get size() {
303
+ return handles.size;
304
+ },
305
+ changed: (nodes) => each((h) => h.changed(nodes)),
306
+ selection: () => each((h) => h.selection()),
307
+ theme: () => each((h) => h.theme()),
308
+ writable: () => each((h) => h.writable()),
309
+ close: () => {
310
+ each((h) => h.close());
311
+ handles.clear();
312
+ },
313
+ dispose: () => {
314
+ stop();
315
+ each((h) => h.close());
316
+ handles.clear();
317
+ }
318
+ };
319
+ }
320
+
321
+ exports.VIEW_THEME_TOKENS = VIEW_THEME_TOKENS;
322
+ exports.hostView = hostView;
323
+ exports.hostViews = hostViews;
324
+ exports.onViewPort = onViewPort;
325
+ exports.readTheme = readTheme;
326
+ //# sourceMappingURL=view-host-web.cjs.map
327
+ //# sourceMappingURL=view-host-web.cjs.map