@zseven-w/dsh-openpencil 0.1.0-rc.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.
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/THIRD_PARTY_NOTICES.md +69 -0
- package/cordis.patch.yml +6 -0
- package/docs/images/dsh-openpencil-overview.png +0 -0
- package/lib/client/details-compat.d.ts +28 -0
- package/lib/client/details-compat.js +10 -0
- package/lib/client/editor-bridge.d.ts +84 -0
- package/lib/client/editor-bridge.js +127 -0
- package/lib/client/editor-dock-layout.d.ts +15 -0
- package/lib/client/editor-dock-layout.js +43 -0
- package/lib/client/editor-recovery.d.ts +27 -0
- package/lib/client/editor-recovery.js +83 -0
- package/lib/client/editor-successor.d.ts +28 -0
- package/lib/client/editor-successor.js +109 -0
- package/lib/client/presentation-hydration.d.ts +36 -0
- package/lib/client/presentation-hydration.js +168 -0
- package/lib/client/selection-polling.d.ts +24 -0
- package/lib/client/selection-polling.js +79 -0
- package/lib/client/selection-store.d.ts +26 -0
- package/lib/client/selection-store.js +85 -0
- package/lib/client.js +4033 -0
- package/lib/design-tools.d.ts +21 -0
- package/lib/design-tools.js +214 -0
- package/lib/editor-host.d.ts +62 -0
- package/lib/editor-host.js +1147 -0
- package/lib/editor-recovery.d.ts +38 -0
- package/lib/editor-recovery.js +319 -0
- package/lib/index.d.ts +24 -0
- package/lib/index.js +132 -0
- package/lib/mcp-client.d.ts +47 -0
- package/lib/mcp-client.js +148 -0
- package/lib/new-tool.d.ts +23 -0
- package/lib/new-tool.js +136 -0
- package/lib/presentation-hydration.d.ts +55 -0
- package/lib/presentation-hydration.js +561 -0
- package/lib/renderer.d.ts +203 -0
- package/lib/renderer.js +772 -0
- package/lib/tool-names.d.ts +13 -0
- package/lib/tool-names.js +19 -0
- package/lib/tool.d.ts +27 -0
- package/lib/tool.js +221 -0
- package/lib/types/client/details-compat.d.ts +28 -0
- package/lib/types/client/editor-bridge.d.ts +84 -0
- package/lib/types/client/editor-dock-layout.d.ts +15 -0
- package/lib/types/client/editor-modal.d.ts +54 -0
- package/lib/types/client/editor-panel.d.ts +127 -0
- package/lib/types/client/editor-recovery.d.ts +27 -0
- package/lib/types/client/editor-successor.d.ts +28 -0
- package/lib/types/client/editor-workbench-host.d.ts +43 -0
- package/lib/types/client/frame-gallery.d.ts +98 -0
- package/lib/types/client/index.d.ts +176 -0
- package/lib/types/client/presentation-hydration.d.ts +36 -0
- package/lib/types/client/selection-dock.d.ts +18 -0
- package/lib/types/client/selection-polling.d.ts +24 -0
- package/lib/types/client/selection-store.d.ts +26 -0
- package/lib/types/tool-names.d.ts +13 -0
- package/lib/viewer-assets/canvaskit/canvaskit.js +288 -0
- package/lib/viewer-assets/canvaskit/canvaskit.wasm +0 -0
- package/lib/viewer-assets/manifest.json +22 -0
- package/lib/viewer-assets/op_web_sdk_bg.wasm +0 -0
- package/lib/viewer-assets/sdk.js +225 -0
- package/lib/viewer-assets.d.ts +74 -0
- package/lib/viewer-assets.js +303 -0
- package/package.json +95 -0
- package/scripts/build-client.mjs +27 -0
- package/scripts/run-tool.mjs +63 -0
- package/scripts/sync-viewer-assets.mjs +304 -0
- package/scripts/test-host.mjs +311 -0
- package/scripts/test-viewer-assets.mjs +54 -0
- package/src/client/details-compat.ts +44 -0
- package/src/client/editor-bridge.ts +170 -0
- package/src/client/editor-dock-layout.ts +56 -0
- package/src/client/editor-modal.tsx +607 -0
- package/src/client/editor-panel.tsx +823 -0
- package/src/client/editor-recovery.ts +116 -0
- package/src/client/editor-successor.ts +136 -0
- package/src/client/editor-workbench-host.tsx +280 -0
- package/src/client/frame-gallery.tsx +546 -0
- package/src/client/index.tsx +987 -0
- package/src/client/presentation-hydration.ts +210 -0
- package/src/client/react-dom-compat.d.ts +22 -0
- package/src/client/selection-dock.tsx +89 -0
- package/src/client/selection-polling.ts +93 -0
- package/src/client/selection-store.ts +108 -0
- package/src/design-tools.ts +242 -0
- package/src/editor-host.ts +1238 -0
- package/src/editor-recovery.ts +374 -0
- package/src/index.ts +219 -0
- package/src/mcp-client.ts +193 -0
- package/src/new-tool.ts +160 -0
- package/src/presentation-hydration.ts +634 -0
- package/src/renderer.ts +884 -0
- package/src/tool-names.ts +21 -0
- package/src/tool.ts +258 -0
- package/src/viewer-assets.ts +353 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,4033 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({ id: "@zseven-w/dsh-openpencil", factory: (require) => {
|
|
2
|
+
var module = { exports: {} }; var exports = module.exports;
|
|
3
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
4
|
+
let react = require("react");
|
|
5
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
6
|
+
let react_dom_client = require("react-dom/client");
|
|
7
|
+
let react_dom = require("react-dom");
|
|
8
|
+
//#region src/client/editor-bridge.ts
|
|
9
|
+
function isRecord$6(value) {
|
|
10
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
function safeInteger(value) {
|
|
13
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
14
|
+
}
|
|
15
|
+
function string(value) {
|
|
16
|
+
return typeof value === "string";
|
|
17
|
+
}
|
|
18
|
+
/** Parse only the editor/host messages DSH implements. Unknown traffic is ignored. */
|
|
19
|
+
function parseEditorInbound(raw) {
|
|
20
|
+
if (typeof raw !== "string") return void 0;
|
|
21
|
+
let value;
|
|
22
|
+
try {
|
|
23
|
+
value = JSON.parse(raw);
|
|
24
|
+
} catch {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!isRecord$6(value) || typeof value.type !== "string") return void 0;
|
|
28
|
+
switch (value.type) {
|
|
29
|
+
case "op-bridge/ready": return safeInteger(value.generation) && safeInteger(value.revision) ? {
|
|
30
|
+
type: value.type,
|
|
31
|
+
generation: value.generation,
|
|
32
|
+
revision: value.revision
|
|
33
|
+
} : void 0;
|
|
34
|
+
case "op-bridge/opened": return safeInteger(value.generation) ? {
|
|
35
|
+
type: value.type,
|
|
36
|
+
generation: value.generation
|
|
37
|
+
} : void 0;
|
|
38
|
+
case "op-bridge/dirty-changed": return safeInteger(value.generation) && safeInteger(value.revision) && typeof value.dirty === "boolean" ? {
|
|
39
|
+
type: value.type,
|
|
40
|
+
generation: value.generation,
|
|
41
|
+
revision: value.revision,
|
|
42
|
+
dirty: value.dirty
|
|
43
|
+
} : void 0;
|
|
44
|
+
case "op-bridge/snapshot-result": return string(value.requestId) && string(value.docJson) && safeInteger(value.generation) && safeInteger(value.revision) ? {
|
|
45
|
+
type: value.type,
|
|
46
|
+
requestId: value.requestId,
|
|
47
|
+
docJson: value.docJson,
|
|
48
|
+
generation: value.generation,
|
|
49
|
+
revision: value.revision
|
|
50
|
+
} : void 0;
|
|
51
|
+
case "op-bridge/snapshot-conflict": return string(value.requestId) && safeInteger(value.serverVersion) ? {
|
|
52
|
+
type: value.type,
|
|
53
|
+
requestId: value.requestId,
|
|
54
|
+
serverVersion: value.serverVersion
|
|
55
|
+
} : void 0;
|
|
56
|
+
case "op-bridge/sync-conflict": return safeInteger(value.generation) && safeInteger(value.revision) && safeInteger(value.serverVersion) ? {
|
|
57
|
+
type: value.type,
|
|
58
|
+
generation: value.generation,
|
|
59
|
+
revision: value.revision,
|
|
60
|
+
serverVersion: value.serverVersion
|
|
61
|
+
} : void 0;
|
|
62
|
+
case "op-bridge/conflict-resolved": return string(value.requestId) ? {
|
|
63
|
+
type: value.type,
|
|
64
|
+
requestId: value.requestId
|
|
65
|
+
} : void 0;
|
|
66
|
+
case "op-shell/save": return { type: value.type };
|
|
67
|
+
case "op-shell/copy": return string(value.text) ? {
|
|
68
|
+
type: value.type,
|
|
69
|
+
text: value.text
|
|
70
|
+
} : void 0;
|
|
71
|
+
default: return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function encodeEditorOutbound(message) {
|
|
75
|
+
return JSON.stringify(message);
|
|
76
|
+
}
|
|
77
|
+
/** Require an absolute loopback editor URL and derive its exact target origin. */
|
|
78
|
+
function editorOrigin(iframeUrl) {
|
|
79
|
+
const url = new URL(iframeUrl);
|
|
80
|
+
if (!(url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "::1") || url.protocol !== "http:" && url.protocol !== "https:") throw new Error("OpenPencil editor URL must use an HTTP loopback origin");
|
|
81
|
+
return url.origin;
|
|
82
|
+
}
|
|
83
|
+
/** Pin the host's resolved theme into the editor's first navigation. */
|
|
84
|
+
function editorIframeUrlWithTheme(iframeUrl, colorScheme) {
|
|
85
|
+
const url = new URL(iframeUrl);
|
|
86
|
+
url.searchParams.set("theme", colorScheme);
|
|
87
|
+
return url.href;
|
|
88
|
+
}
|
|
89
|
+
/** Pin the host's resolved locale into the editor's first navigation. */
|
|
90
|
+
function editorIframeUrlWithLocale(iframeUrl, locale) {
|
|
91
|
+
const url = new URL(iframeUrl);
|
|
92
|
+
url.searchParams.set("locale", locale);
|
|
93
|
+
return url.href;
|
|
94
|
+
}
|
|
95
|
+
/** Translate DSH's compact locale id to the editor's BCP 47 contract. */
|
|
96
|
+
function editorLocaleFromDsh(locale) {
|
|
97
|
+
return locale === "zh" ? "zh-CN" : "en-US";
|
|
98
|
+
}
|
|
99
|
+
/** Resolve a launch/save/close capability and reject cross-origin control routes. */
|
|
100
|
+
function editorControlUrl(raw, base = window.location.href) {
|
|
101
|
+
const page = new URL(base);
|
|
102
|
+
const url = new URL(raw, page);
|
|
103
|
+
if (url.origin !== page.origin) throw new Error("OpenPencil editor control URL must be same-origin");
|
|
104
|
+
return url.href;
|
|
105
|
+
}
|
|
106
|
+
/** Validate source and exact origin before parsing any iframe message. */
|
|
107
|
+
function editorMessageFrom(event, frameWindow, origin) {
|
|
108
|
+
if (frameWindow === null || event.source !== frameWindow || event.origin !== origin) return void 0;
|
|
109
|
+
return parseEditorInbound(event.data);
|
|
110
|
+
}
|
|
111
|
+
let activeEditor;
|
|
112
|
+
/** Read-only gate for background auto-open flows; never asks an owner to close. */
|
|
113
|
+
function hasActiveEditor() {
|
|
114
|
+
return activeEditor !== void 0;
|
|
115
|
+
}
|
|
116
|
+
/** Page-wide single-editor coordinator. An existing dirty editor may veto takeover. */
|
|
117
|
+
function claimEditor(token, close, options = {}) {
|
|
118
|
+
const previous = activeEditor;
|
|
119
|
+
if (previous !== void 0 && previous.token !== token) {
|
|
120
|
+
if (options.replace === false || previous.close() === false) return void 0;
|
|
121
|
+
}
|
|
122
|
+
activeEditor = {
|
|
123
|
+
token,
|
|
124
|
+
close
|
|
125
|
+
};
|
|
126
|
+
return () => {
|
|
127
|
+
if (activeEditor?.token === token) activeEditor = void 0;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** Confirm before a user-driven panel close would discard unsaved canvas edits. */
|
|
131
|
+
function confirmEditorClose(dirty, confirm = window.confirm) {
|
|
132
|
+
return !dirty || confirm("OpenPencil has unsaved changes. Close the editor and discard them?");
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/client/editor-successor.ts
|
|
136
|
+
/** Session-scoped successor capabilities for reopening a saved editor card. */
|
|
137
|
+
const STORAGE_PREFIX = "dsh-openpencil:editor-successor:v1:";
|
|
138
|
+
function isRecord$5(value) {
|
|
139
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
140
|
+
}
|
|
141
|
+
function browserStorage() {
|
|
142
|
+
try {
|
|
143
|
+
return window.sessionStorage;
|
|
144
|
+
} catch {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function storageOf(options) {
|
|
149
|
+
return options.storage === void 0 ? browserStorage() : options.storage ?? void 0;
|
|
150
|
+
}
|
|
151
|
+
function baseUrlOf(options) {
|
|
152
|
+
return options.baseUrl ?? window.location.href;
|
|
153
|
+
}
|
|
154
|
+
/** The original Tool grant scopes one successor chain for the life of this tab. */
|
|
155
|
+
function editorSuccessorStorageKey(originalLaunchUrl, baseUrl = window.location.href) {
|
|
156
|
+
return `${STORAGE_PREFIX}${editorControlUrl(originalLaunchUrl, baseUrl)}`;
|
|
157
|
+
}
|
|
158
|
+
function persistedSuccessorOf(value, baseUrl) {
|
|
159
|
+
if (!isRecord$5(value)) return void 0;
|
|
160
|
+
const launchUrl = value.launchUrl;
|
|
161
|
+
const refreshUrl = value.refreshUrl;
|
|
162
|
+
if (typeof launchUrl !== "string" || launchUrl.length === 0 || typeof refreshUrl !== "string" || refreshUrl.length === 0) return void 0;
|
|
163
|
+
try {
|
|
164
|
+
return {
|
|
165
|
+
launchUrl: editorControlUrl(launchUrl, baseUrl),
|
|
166
|
+
refreshUrl: editorControlUrl(refreshUrl, baseUrl)
|
|
167
|
+
};
|
|
168
|
+
} catch {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** Parse only the successor grant from a successful save response. */
|
|
173
|
+
function editorSuccessorFromSave(value, baseUrl = window.location.href) {
|
|
174
|
+
if (!isRecord$5(value) || !isRecord$5(value.editor) || value.editor.enabled !== true) return void 0;
|
|
175
|
+
const persisted = persistedSuccessorOf(value.editor, baseUrl);
|
|
176
|
+
return persisted === void 0 ? void 0 : {
|
|
177
|
+
enabled: true,
|
|
178
|
+
...persisted
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Persist the newest save successor under the immutable Tool grant. Invalid or
|
|
183
|
+
* absent successors clear an older value so a later reopen cannot use a stale
|
|
184
|
+
* source capability. Storage denial is intentionally non-fatal to saving.
|
|
185
|
+
*/
|
|
186
|
+
function rememberEditorSuccessor(originalLaunchUrl, saveResponse, options = {}) {
|
|
187
|
+
const baseUrl = baseUrlOf(options);
|
|
188
|
+
const successor = editorSuccessorFromSave(saveResponse, baseUrl);
|
|
189
|
+
const storage = storageOf(options);
|
|
190
|
+
if (storage === void 0) return successor;
|
|
191
|
+
try {
|
|
192
|
+
const key = editorSuccessorStorageKey(originalLaunchUrl, baseUrl);
|
|
193
|
+
if (successor === void 0 || successor.refreshUrl === void 0) {
|
|
194
|
+
storage.removeItem(key);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
storage.setItem(key, JSON.stringify({
|
|
198
|
+
launchUrl: successor.launchUrl,
|
|
199
|
+
refreshUrl: successor.refreshUrl
|
|
200
|
+
}));
|
|
201
|
+
} catch {}
|
|
202
|
+
return successor;
|
|
203
|
+
}
|
|
204
|
+
/** Resolve a saved successor, falling back to the original Tool grant safely. */
|
|
205
|
+
function editorGrantForBoot(original, options = {}) {
|
|
206
|
+
const storage = storageOf(options);
|
|
207
|
+
if (storage === void 0) return original;
|
|
208
|
+
const baseUrl = baseUrlOf(options);
|
|
209
|
+
let key;
|
|
210
|
+
try {
|
|
211
|
+
key = editorSuccessorStorageKey(original.launchUrl, baseUrl);
|
|
212
|
+
} catch {
|
|
213
|
+
return original;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
const raw = storage.getItem(key);
|
|
217
|
+
if (raw === null) return original;
|
|
218
|
+
const successor = persistedSuccessorOf(JSON.parse(raw), baseUrl);
|
|
219
|
+
if (successor !== void 0) return {
|
|
220
|
+
enabled: true,
|
|
221
|
+
...successor
|
|
222
|
+
};
|
|
223
|
+
} catch {}
|
|
224
|
+
try {
|
|
225
|
+
storage.removeItem(key);
|
|
226
|
+
} catch {}
|
|
227
|
+
return original;
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/client/selection-store.ts
|
|
231
|
+
const stores = /* @__PURE__ */ new Map();
|
|
232
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
233
|
+
const EMPTY_SELECTION_STORE_SNAPSHOT = Object.freeze({ revision: 0 });
|
|
234
|
+
function isRecord$4(value) {
|
|
235
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
236
|
+
}
|
|
237
|
+
function finite(value) {
|
|
238
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
239
|
+
}
|
|
240
|
+
function nodeOf(value) {
|
|
241
|
+
if (!isRecord$4(value) || typeof value.id !== "string" || value.id.length === 0) return void 0;
|
|
242
|
+
return {
|
|
243
|
+
id: value.id,
|
|
244
|
+
...typeof value.type === "string" && value.type.length > 0 ? { type: value.type } : {},
|
|
245
|
+
...typeof value.name === "string" && value.name.length > 0 ? { name: value.name } : {},
|
|
246
|
+
...finite(value.x) === void 0 ? {} : { x: finite(value.x) },
|
|
247
|
+
...finite(value.y) === void 0 ? {} : { y: finite(value.y) },
|
|
248
|
+
...finite(value.width) === void 0 ? {} : { width: finite(value.width) },
|
|
249
|
+
...finite(value.height) === void 0 ? {} : { height: finite(value.height) }
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function liveSelectionOf(value) {
|
|
253
|
+
if (!isRecord$4(value) || typeof value.sourcePath !== "string" || value.sourcePath.length === 0) return void 0;
|
|
254
|
+
return {
|
|
255
|
+
sourcePath: value.sourcePath,
|
|
256
|
+
activePageId: typeof value.activePageId === "string" ? value.activePageId : "",
|
|
257
|
+
selectedIds: Array.isArray(value.selectedIds) ? value.selectedIds.filter((id) => typeof id === "string" && id.length > 0) : [],
|
|
258
|
+
nodes: Array.isArray(value.nodes) ? value.nodes.map(nodeOf).filter((node) => node !== void 0) : [],
|
|
259
|
+
updatedAt: finite(value.updatedAt) ?? Date.now()
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function sameSelection(a, b) {
|
|
263
|
+
if (a === void 0 || b === void 0) return a === b;
|
|
264
|
+
return a.sourcePath === b.sourcePath && a.activePageId === b.activePageId && JSON.stringify(a.selectedIds) === JSON.stringify(b.selectedIds) && JSON.stringify(a.nodes) === JSON.stringify(b.nodes);
|
|
265
|
+
}
|
|
266
|
+
function publishOpenPencilSelection(sessionId, selection) {
|
|
267
|
+
const current = stores.get(sessionId) ?? { revision: 0 };
|
|
268
|
+
if (sameSelection(current.selection, selection)) return;
|
|
269
|
+
stores.set(sessionId, {
|
|
270
|
+
revision: current.revision + 1,
|
|
271
|
+
selection
|
|
272
|
+
});
|
|
273
|
+
for (const listener of listeners.get(sessionId) ?? []) listener();
|
|
274
|
+
}
|
|
275
|
+
function clearOpenPencilSelection(sessionId, sourcePath) {
|
|
276
|
+
const current = stores.get(sessionId);
|
|
277
|
+
if (current === void 0) return;
|
|
278
|
+
if (current.selection === void 0) return;
|
|
279
|
+
if (sourcePath !== void 0 && current.selection.sourcePath !== sourcePath) return;
|
|
280
|
+
stores.set(sessionId, { revision: current.revision + 1 });
|
|
281
|
+
for (const listener of listeners.get(sessionId) ?? []) listener();
|
|
282
|
+
}
|
|
283
|
+
function subscribeOpenPencilSelection(sessionId, listener) {
|
|
284
|
+
let scoped = listeners.get(sessionId);
|
|
285
|
+
if (scoped === void 0) {
|
|
286
|
+
scoped = /* @__PURE__ */ new Set();
|
|
287
|
+
listeners.set(sessionId, scoped);
|
|
288
|
+
}
|
|
289
|
+
scoped.add(listener);
|
|
290
|
+
return () => {
|
|
291
|
+
scoped?.delete(listener);
|
|
292
|
+
if (scoped?.size === 0) listeners.delete(sessionId);
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function getOpenPencilSelectionSnapshot(sessionId) {
|
|
296
|
+
return stores.get(sessionId) ?? EMPTY_SELECTION_STORE_SNAPSHOT;
|
|
297
|
+
}
|
|
298
|
+
//#endregion
|
|
299
|
+
//#region src/client/selection-polling.ts
|
|
300
|
+
const DEFAULT_INTERVAL_MS = 400;
|
|
301
|
+
const DEFAULT_TIMER = {
|
|
302
|
+
schedule: (callback, delayMs) => setTimeout(callback, delayMs),
|
|
303
|
+
cancel: (handle) => {
|
|
304
|
+
clearTimeout(handle);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
/** Client failures other than retry-oriented HTTP statuses cannot recover by polling. */
|
|
308
|
+
function isTerminalEditorSelectionStatus(status) {
|
|
309
|
+
return status >= 400 && status < 500 && status !== 408 && status !== 425 && status !== 429;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Start one immediate poll followed by non-overlapping delayed polls.
|
|
313
|
+
*
|
|
314
|
+
* The returned cleanup is idempotent, aborts an in-flight request, cancels the
|
|
315
|
+
* pending timer, and invokes `onStop` exactly once. Terminal HTTP responses
|
|
316
|
+
* use the same cleanup path; network errors and retryable statuses schedule a
|
|
317
|
+
* later attempt without disrupting the editor itself.
|
|
318
|
+
*/
|
|
319
|
+
function startEditorSelectionPolling(options) {
|
|
320
|
+
const fetcher = options.fetcher ?? fetch;
|
|
321
|
+
const timer = options.timer ?? DEFAULT_TIMER;
|
|
322
|
+
const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
323
|
+
let stopped = false;
|
|
324
|
+
let timerHandle;
|
|
325
|
+
let requestAbort;
|
|
326
|
+
const stop = () => {
|
|
327
|
+
if (stopped) return;
|
|
328
|
+
stopped = true;
|
|
329
|
+
if (timerHandle !== void 0) {
|
|
330
|
+
timer.cancel(timerHandle);
|
|
331
|
+
timerHandle = void 0;
|
|
332
|
+
}
|
|
333
|
+
requestAbort?.abort();
|
|
334
|
+
requestAbort = void 0;
|
|
335
|
+
options.onStop();
|
|
336
|
+
};
|
|
337
|
+
const schedule = () => {
|
|
338
|
+
if (stopped) return;
|
|
339
|
+
timerHandle = timer.schedule(() => {
|
|
340
|
+
timerHandle = void 0;
|
|
341
|
+
poll();
|
|
342
|
+
}, intervalMs);
|
|
343
|
+
};
|
|
344
|
+
const poll = async () => {
|
|
345
|
+
if (stopped) return;
|
|
346
|
+
const abort = new AbortController();
|
|
347
|
+
requestAbort = abort;
|
|
348
|
+
try {
|
|
349
|
+
const response = await fetcher(options.url, {
|
|
350
|
+
credentials: "same-origin",
|
|
351
|
+
signal: abort.signal
|
|
352
|
+
});
|
|
353
|
+
if (stopped) return;
|
|
354
|
+
if (isTerminalEditorSelectionStatus(response.status)) {
|
|
355
|
+
stop();
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (response.ok) {
|
|
359
|
+
const value = await response.json();
|
|
360
|
+
if (!stopped) options.onValue(value);
|
|
361
|
+
}
|
|
362
|
+
} catch {} finally {
|
|
363
|
+
if (requestAbort === abort) requestAbort = void 0;
|
|
364
|
+
schedule();
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
poll();
|
|
368
|
+
return stop;
|
|
369
|
+
}
|
|
370
|
+
//#endregion
|
|
371
|
+
//#region src/client/editor-recovery.ts
|
|
372
|
+
/** Same-origin client controls for durable managed-editor recovery. */
|
|
373
|
+
function isRecord$3(value) {
|
|
374
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
375
|
+
}
|
|
376
|
+
function editorRecoverySummaryOf(value) {
|
|
377
|
+
if (!isRecord$3(value)) return void 0;
|
|
378
|
+
if (typeof value.id !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(value.id) || typeof value.capturedAt !== "number" || !Number.isSafeInteger(value.capturedAt) || value.capturedAt <= 0 || typeof value.bytes !== "number" || !Number.isSafeInteger(value.bytes) || value.bytes <= 0 || typeof value.sourceName !== "string" || value.sourceName.length <= 0 || value.sourceName.length > 512 || typeof value.sourceChangedSinceCapture !== "boolean" || typeof value.cacheLabel !== "string" || !value.cacheLabel.startsWith("dsh-openpencil/recovery/")) return void 0;
|
|
379
|
+
return value;
|
|
380
|
+
}
|
|
381
|
+
function editorRecoveryItemUrl(launch, recoveryId) {
|
|
382
|
+
if (launch.recoveryUrl === void 0) throw new Error("OpenPencil recovery control is unavailable");
|
|
383
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(recoveryId)) throw new Error("OpenPencil recovery id is invalid");
|
|
384
|
+
return `${editorControlUrl(launch.recoveryUrl)}/${recoveryId}`;
|
|
385
|
+
}
|
|
386
|
+
async function checkedJson(response, action) {
|
|
387
|
+
if (!response.ok) throw new Error(`${action} failed (${response.status})`);
|
|
388
|
+
return response.json();
|
|
389
|
+
}
|
|
390
|
+
/** Capture the authoritative daemon document; never serializes the iframe from React. */
|
|
391
|
+
async function captureManagedEditorRecovery(launch, fetcher = fetch) {
|
|
392
|
+
if (launch.recoveryUrl === void 0) return void 0;
|
|
393
|
+
const value = await checkedJson(await fetcher(editorControlUrl(launch.recoveryUrl), {
|
|
394
|
+
method: "POST",
|
|
395
|
+
credentials: "same-origin",
|
|
396
|
+
headers: { "content-type": "application/json" },
|
|
397
|
+
body: JSON.stringify({ sessionId: launch.sessionId }),
|
|
398
|
+
keepalive: true
|
|
399
|
+
}), "OpenPencil recovery capture");
|
|
400
|
+
if (!isRecord$3(value) || value.ok !== true) throw new Error("OpenPencil recovery capture returned an invalid result");
|
|
401
|
+
if (value.recovery === null) return void 0;
|
|
402
|
+
const recovery = editorRecoverySummaryOf(value.recovery);
|
|
403
|
+
if (recovery === void 0) throw new Error("OpenPencil recovery capture omitted recovery metadata");
|
|
404
|
+
return recovery;
|
|
405
|
+
}
|
|
406
|
+
/** Explicitly restore into the live daemon. The user must still press Save to update `.op`. */
|
|
407
|
+
async function restoreManagedEditorRecovery(launch, recovery, fetcher = fetch) {
|
|
408
|
+
const value = await checkedJson(await fetcher(editorRecoveryItemUrl(launch, recovery.id), {
|
|
409
|
+
method: "POST",
|
|
410
|
+
credentials: "same-origin",
|
|
411
|
+
headers: { "content-type": "application/json" },
|
|
412
|
+
body: JSON.stringify({ sessionId: launch.sessionId })
|
|
413
|
+
}), "OpenPencil recovery restore");
|
|
414
|
+
if (!isRecord$3(value) || value.ok !== true || typeof value.docJson !== "string") throw new Error("OpenPencil recovery restore returned an invalid result");
|
|
415
|
+
return value.docJson;
|
|
416
|
+
}
|
|
417
|
+
async function discardManagedEditorRecovery(launch, recovery, fetcher = fetch) {
|
|
418
|
+
const value = await checkedJson(await fetcher(editorRecoveryItemUrl(launch, recovery.id), {
|
|
419
|
+
method: "DELETE",
|
|
420
|
+
credentials: "same-origin",
|
|
421
|
+
headers: { "content-type": "application/json" },
|
|
422
|
+
body: JSON.stringify({ sessionId: launch.sessionId })
|
|
423
|
+
}), "OpenPencil recovery discard");
|
|
424
|
+
if (!isRecord$3(value) || value.ok !== true) throw new Error("OpenPencil recovery discard returned an invalid result");
|
|
425
|
+
}
|
|
426
|
+
const COPY = {
|
|
427
|
+
"zh-CN": {
|
|
428
|
+
available: (sourceName) => `发现 ${sourceName} 的未保存恢复稿。是否恢复?恢复后仍需点击“保存”才会写入源文件。`,
|
|
429
|
+
conflict: (sourceName) => `发现 ${sourceName} 的未保存恢复稿,但源文件之后已被修改。仍要将恢复稿载入画布吗?恢复不会自动覆盖源文件。`
|
|
430
|
+
},
|
|
431
|
+
"en-US": {
|
|
432
|
+
available: (sourceName) => `An unsaved recovery draft for ${sourceName} is available. Restore it? You must still select Save to write the source file.`,
|
|
433
|
+
conflict: (sourceName) => `An unsaved recovery draft for ${sourceName} is available, but the source changed later. Load the draft into the canvas anyway? Recovery will not overwrite the source automatically.`
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
function editorRecoveryCopy(locale) {
|
|
437
|
+
return COPY[locale];
|
|
438
|
+
}
|
|
439
|
+
//#endregion
|
|
440
|
+
//#region src/client/editor-panel.tsx
|
|
441
|
+
/** Full OpenPencil editor shared by native Tool details and plugin-owned surfaces. */
|
|
442
|
+
const DEFAULT_REFRESH_URL = "/_dsh/dsh-openpencil/editor/refresh";
|
|
443
|
+
function isRecord$2(value) {
|
|
444
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
445
|
+
}
|
|
446
|
+
function launchResponseOf(value) {
|
|
447
|
+
if (!isRecord$2(value)) throw new Error("OpenPencil editor launch returned an invalid response");
|
|
448
|
+
for (const field of [
|
|
449
|
+
"sessionId",
|
|
450
|
+
"iframeUrl",
|
|
451
|
+
"token",
|
|
452
|
+
"saveUrl",
|
|
453
|
+
"closeUrl"
|
|
454
|
+
]) if (typeof value[field] !== "string" || value[field].length === 0) throw new Error(`OpenPencil editor launch omitted ${field}`);
|
|
455
|
+
return {
|
|
456
|
+
sessionId: value.sessionId,
|
|
457
|
+
iframeUrl: value.iframeUrl,
|
|
458
|
+
token: value.token,
|
|
459
|
+
saveUrl: editorControlUrl(value.saveUrl),
|
|
460
|
+
...typeof value.selectionUrl === "string" && value.selectionUrl.length > 0 ? { selectionUrl: editorControlUrl(value.selectionUrl) } : {},
|
|
461
|
+
...typeof value.recoveryUrl === "string" && value.recoveryUrl.length > 0 ? { recoveryUrl: editorControlUrl(value.recoveryUrl) } : {},
|
|
462
|
+
closeUrl: editorControlUrl(value.closeUrl),
|
|
463
|
+
...editorRecoverySummaryOf(value.recovery) === void 0 ? {} : { recovery: editorRecoverySummaryOf(value.recovery) },
|
|
464
|
+
...typeof value.docJson === "string" ? { docJson: value.docJson } : {}
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
async function responseJson(response, action) {
|
|
468
|
+
if (!response.ok) throw new Error(`${action} failed (${response.status})`);
|
|
469
|
+
return response.json();
|
|
470
|
+
}
|
|
471
|
+
function refreshedLaunchUrlOf(value) {
|
|
472
|
+
if (!isRecord$2(value) || typeof value.launchUrl !== "string" || value.launchUrl.length === 0) throw new Error("OpenPencil editor refresh omitted launchUrl");
|
|
473
|
+
return editorControlUrl(value.launchUrl);
|
|
474
|
+
}
|
|
475
|
+
const EDITOR_PANEL_COPY = {
|
|
476
|
+
"zh-CN": {
|
|
477
|
+
save: "保存",
|
|
478
|
+
saving: "保存中…",
|
|
479
|
+
unsaved: "未保存",
|
|
480
|
+
saved: "已保存",
|
|
481
|
+
unavailable: "当前结果无法使用可编辑的 OpenPencil 画布。",
|
|
482
|
+
loading: "正在加载可编辑的 OpenPencil 画布…",
|
|
483
|
+
errorTitle: "OpenPencil 编辑器不可用",
|
|
484
|
+
pngFallback: "打开 PNG 预览",
|
|
485
|
+
editorTitle: (title) => `OpenPencil 编辑器:${title}`,
|
|
486
|
+
editorTimeout: "OpenPencil 编辑器未能及时就绪",
|
|
487
|
+
editorBusy: "另一个 OpenPencil 编辑器仍有未保存的更改。",
|
|
488
|
+
discard: "OpenPencil 中有未保存的更改,确定关闭并放弃吗?",
|
|
489
|
+
saveConflict: (serverVersion) => `OpenPencil 保存冲突(服务器版本 ${serverVersion})`,
|
|
490
|
+
syncConflict: (serverVersion) => `源文件已在编辑器外部更改(服务器版本 ${serverVersion}),已停止保存。`
|
|
491
|
+
},
|
|
492
|
+
"en-US": {
|
|
493
|
+
save: "Save",
|
|
494
|
+
saving: "Saving…",
|
|
495
|
+
unsaved: "Unsaved",
|
|
496
|
+
saved: "Saved",
|
|
497
|
+
unavailable: "Editable OpenPencil canvas is not available for this result.",
|
|
498
|
+
loading: "Loading editable OpenPencil canvas…",
|
|
499
|
+
errorTitle: "OpenPencil editor unavailable",
|
|
500
|
+
pngFallback: "Open PNG fallback",
|
|
501
|
+
editorTitle: (title) => `OpenPencil editor: ${title}`,
|
|
502
|
+
editorTimeout: "OpenPencil editor did not become ready",
|
|
503
|
+
editorBusy: "Another OpenPencil editor still has unsaved changes.",
|
|
504
|
+
discard: "OpenPencil has unsaved changes. Close and discard them?",
|
|
505
|
+
saveConflict: (serverVersion) => `OpenPencil save conflict (server v${serverVersion})`,
|
|
506
|
+
syncConflict: (serverVersion) => `The source changed outside this editor (server v${serverVersion}). Save was stopped.`
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
/** Chrome copy for the locale already resolved by the DSH host. */
|
|
510
|
+
function editorPanelCopy(locale) {
|
|
511
|
+
return EDITOR_PANEL_COPY[locale];
|
|
512
|
+
}
|
|
513
|
+
function launchRequest(fetcher, url, signal, sessionId) {
|
|
514
|
+
return fetcher(editorControlUrl(url), {
|
|
515
|
+
method: "POST",
|
|
516
|
+
credentials: "same-origin",
|
|
517
|
+
...sessionId === void 0 ? {} : {
|
|
518
|
+
headers: { "content-type": "application/json" },
|
|
519
|
+
body: JSON.stringify({ sessionId })
|
|
520
|
+
},
|
|
521
|
+
...signal === void 0 ? {} : { signal }
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Launch one editor, renewing exactly once when a replayed launch capability
|
|
526
|
+
* has expired. A refreshed capability is never persisted back into the Tool
|
|
527
|
+
* block, and only same-origin control routes can receive document metadata.
|
|
528
|
+
*/
|
|
529
|
+
async function launchManagedEditor(editor, document, options = {}) {
|
|
530
|
+
const fetcher = options.fetcher ?? fetch;
|
|
531
|
+
let launchUrl = editor.launchUrl;
|
|
532
|
+
let renewed = false;
|
|
533
|
+
let response = await launchRequest(fetcher, launchUrl, options.signal, options.sessionId);
|
|
534
|
+
if (response.status === 410) {
|
|
535
|
+
if (document.path === void 0) throw new Error("OpenPencil editor launch expired and cannot be refreshed without a source path");
|
|
536
|
+
launchUrl = refreshedLaunchUrlOf(await responseJson(await fetcher(editorControlUrl(editor.refreshUrl ?? DEFAULT_REFRESH_URL), {
|
|
537
|
+
method: "POST",
|
|
538
|
+
credentials: "same-origin",
|
|
539
|
+
headers: { "content-type": "application/json" },
|
|
540
|
+
body: JSON.stringify({
|
|
541
|
+
launchUrl: editor.launchUrl,
|
|
542
|
+
sourcePath: document.path,
|
|
543
|
+
documentUrl: document.url
|
|
544
|
+
}),
|
|
545
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
546
|
+
}), "OpenPencil editor refresh"));
|
|
547
|
+
renewed = true;
|
|
548
|
+
response = await launchRequest(fetcher, launchUrl, options.signal, options.sessionId);
|
|
549
|
+
}
|
|
550
|
+
const launch = launchResponseOf(await responseJson(response, "OpenPencil editor launch"));
|
|
551
|
+
return renewed ? {
|
|
552
|
+
...launch,
|
|
553
|
+
renewed: true
|
|
554
|
+
} : launch;
|
|
555
|
+
}
|
|
556
|
+
/** Prefer the daemon's current source; fetch the immutable snapshot only for old hosts. */
|
|
557
|
+
async function prepareManagedEditor(editor, document, options = {}) {
|
|
558
|
+
const fetcher = options.fetcher ?? fetch;
|
|
559
|
+
const launch = await launchManagedEditor(editor, document, {
|
|
560
|
+
...options,
|
|
561
|
+
fetcher
|
|
562
|
+
});
|
|
563
|
+
try {
|
|
564
|
+
if (launch.docJson !== void 0) return {
|
|
565
|
+
launch,
|
|
566
|
+
documentJson: launch.docJson
|
|
567
|
+
};
|
|
568
|
+
if (editor.refreshUrl !== void 0 || launch.renewed === true) throw new Error("OpenPencil editor launch omitted current docJson");
|
|
569
|
+
const documentResponse = await fetcher(editorControlUrl(document.url), {
|
|
570
|
+
credentials: "same-origin",
|
|
571
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
572
|
+
});
|
|
573
|
+
if (!documentResponse.ok) throw new Error(`OpenPencil document request failed (${documentResponse.status})`);
|
|
574
|
+
return {
|
|
575
|
+
launch,
|
|
576
|
+
documentJson: await documentResponse.text()
|
|
577
|
+
};
|
|
578
|
+
} catch (error) {
|
|
579
|
+
await closeManagedEditorLaunch(launch, {
|
|
580
|
+
fetcher,
|
|
581
|
+
keepalive: true
|
|
582
|
+
}).catch(() => {});
|
|
583
|
+
throw error;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/** Close exactly the managed session returned by one launch response. */
|
|
587
|
+
async function closeManagedEditorLaunch(launch, options = {}) {
|
|
588
|
+
const response = await (options.fetcher ?? fetch)(launch.closeUrl, {
|
|
589
|
+
method: "DELETE",
|
|
590
|
+
credentials: "same-origin",
|
|
591
|
+
headers: { "content-type": "application/json" },
|
|
592
|
+
body: JSON.stringify({
|
|
593
|
+
sessionId: launch.sessionId,
|
|
594
|
+
dirty: options.dirty ?? false
|
|
595
|
+
}),
|
|
596
|
+
...options.keepalive === void 0 ? {} : { keepalive: options.keepalive }
|
|
597
|
+
});
|
|
598
|
+
if (!response.ok) throw new Error(`OpenPencil editor close failed (${response.status})`);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Mount-aware boot boundary. React may cancel an effect after its launch POST
|
|
602
|
+
* has committed; release that precise returned session before ignoring it.
|
|
603
|
+
*/
|
|
604
|
+
async function prepareManagedEditorForMount(editor, document, accept, options = {}) {
|
|
605
|
+
const prepared = await prepareManagedEditor(editor, document, options);
|
|
606
|
+
if (accept()) return prepared;
|
|
607
|
+
await closeManagedEditorLaunch(prepared.launch, {
|
|
608
|
+
...options.fetcher === void 0 ? {} : { fetcher: options.fetcher },
|
|
609
|
+
keepalive: true
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
const panelStyles = {
|
|
613
|
+
root: {
|
|
614
|
+
height: "100%",
|
|
615
|
+
minHeight: 0,
|
|
616
|
+
display: "flex",
|
|
617
|
+
flexDirection: "column",
|
|
618
|
+
color: "var(--dsw-alias-label-primary)",
|
|
619
|
+
background: "var(--dsw-alias-bg-base)"
|
|
620
|
+
},
|
|
621
|
+
toolbar: {
|
|
622
|
+
minHeight: 48,
|
|
623
|
+
display: "flex",
|
|
624
|
+
alignItems: "center",
|
|
625
|
+
gap: 8,
|
|
626
|
+
padding: "8px 10px 8px 12px",
|
|
627
|
+
borderBottom: "1px solid var(--dsw-alias-border-l2)"
|
|
628
|
+
},
|
|
629
|
+
title: {
|
|
630
|
+
minWidth: 0,
|
|
631
|
+
marginRight: "auto",
|
|
632
|
+
overflow: "hidden",
|
|
633
|
+
textOverflow: "ellipsis",
|
|
634
|
+
whiteSpace: "nowrap",
|
|
635
|
+
fontSize: 14,
|
|
636
|
+
lineHeight: "20px",
|
|
637
|
+
fontWeight: 500
|
|
638
|
+
},
|
|
639
|
+
status: {
|
|
640
|
+
fontSize: 11,
|
|
641
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
642
|
+
whiteSpace: "nowrap"
|
|
643
|
+
},
|
|
644
|
+
button: {
|
|
645
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
646
|
+
borderRadius: 6,
|
|
647
|
+
color: "var(--dsw-alias-label-primary)",
|
|
648
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
649
|
+
minHeight: 28,
|
|
650
|
+
padding: "4px 8px",
|
|
651
|
+
cursor: "pointer",
|
|
652
|
+
fontFamily: "inherit",
|
|
653
|
+
fontSize: 12,
|
|
654
|
+
fontWeight: "inherit",
|
|
655
|
+
lineHeight: 1
|
|
656
|
+
},
|
|
657
|
+
stage: {
|
|
658
|
+
position: "relative",
|
|
659
|
+
flex: 1,
|
|
660
|
+
minHeight: 0,
|
|
661
|
+
overflow: "hidden",
|
|
662
|
+
background: "var(--dsw-alias-bg-base)"
|
|
663
|
+
},
|
|
664
|
+
iframe: {
|
|
665
|
+
display: "block",
|
|
666
|
+
width: "100%",
|
|
667
|
+
height: "100%",
|
|
668
|
+
border: 0,
|
|
669
|
+
background: "var(--dsw-alias-bg-base)"
|
|
670
|
+
},
|
|
671
|
+
overlay: {
|
|
672
|
+
position: "absolute",
|
|
673
|
+
inset: 0,
|
|
674
|
+
display: "flex",
|
|
675
|
+
alignItems: "center",
|
|
676
|
+
justifyContent: "center",
|
|
677
|
+
flexDirection: "column",
|
|
678
|
+
gap: 10,
|
|
679
|
+
padding: 24,
|
|
680
|
+
textAlign: "center",
|
|
681
|
+
color: "var(--dsw-alias-label-primary)",
|
|
682
|
+
background: "var(--dsw-alias-bg-base)",
|
|
683
|
+
fontSize: 12
|
|
684
|
+
},
|
|
685
|
+
error: {
|
|
686
|
+
color: "var(--dsw-alias-state-error-primary)",
|
|
687
|
+
maxWidth: 420,
|
|
688
|
+
overflowWrap: "anywhere"
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
const INITIAL_EDITOR_LIFECYCLE_STATE = {
|
|
692
|
+
dirty: false,
|
|
693
|
+
phase: "launching"
|
|
694
|
+
};
|
|
695
|
+
/**
|
|
696
|
+
* Start the managed-editor init handshake immediately, then retry on a bounded
|
|
697
|
+
* interval until the caller stops it after `op-bridge/ready`.
|
|
698
|
+
*
|
|
699
|
+
* This deliberately starts before the iframe `load` event. OpenPencil waits
|
|
700
|
+
* briefly for this token before starting daemon-backed services (including the
|
|
701
|
+
* account-status request), while a browser may delay `load` until module/Wasm
|
|
702
|
+
* startup is already complete. Waiting for `load` can therefore make the first
|
|
703
|
+
* account request run unauthenticated and hide the login button until the
|
|
704
|
+
* editor's later health refresh.
|
|
705
|
+
*/
|
|
706
|
+
function beginEditorInitRetry(send, onExhausted, timer, options = {}) {
|
|
707
|
+
const intervalMs = options.intervalMs ?? 500;
|
|
708
|
+
const maxAttempts = options.maxAttempts ?? 20;
|
|
709
|
+
let attempts = 0;
|
|
710
|
+
let handle;
|
|
711
|
+
let stopped = false;
|
|
712
|
+
const stop = () => {
|
|
713
|
+
if (stopped) return;
|
|
714
|
+
stopped = true;
|
|
715
|
+
if (handle !== void 0) timer.cancel(handle);
|
|
716
|
+
};
|
|
717
|
+
const attempt = () => {
|
|
718
|
+
if (stopped) return;
|
|
719
|
+
attempts += 1;
|
|
720
|
+
send();
|
|
721
|
+
if (attempts >= maxAttempts) {
|
|
722
|
+
stop();
|
|
723
|
+
onExhausted();
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
attempt();
|
|
727
|
+
if (!stopped) handle = timer.schedule(attempt, intervalMs);
|
|
728
|
+
return stop;
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Apply the unmount policy without letting React cleanup accidentally issue
|
|
732
|
+
* DELETE. Dirty live launches are retained by default even when their native
|
|
733
|
+
* Tool-details owner has no lifecycle controller.
|
|
734
|
+
*/
|
|
735
|
+
function applyManagedEditorUnmountPolicy(state, closeDaemon) {
|
|
736
|
+
if (state.retainServerSession || state.dirty && state.hasLiveLaunch) return "retained";
|
|
737
|
+
closeDaemon();
|
|
738
|
+
return "closed";
|
|
739
|
+
}
|
|
740
|
+
/** Editable panel. The daemon is created lazily only while this component is mounted. */
|
|
741
|
+
function ManagedOpenPencilEditor({ grant, colorScheme, locale, sessionId, onTakeoverRequest, onLifecycleState, onLifecycleController, workbenchActions, allowTakeover = true }) {
|
|
742
|
+
const iframeRef = (0, react.useRef)(null);
|
|
743
|
+
const launchRef = (0, react.useRef)();
|
|
744
|
+
const iframeSrcRef = (0, react.useRef)("");
|
|
745
|
+
const originRef = (0, react.useRef)("");
|
|
746
|
+
const docJsonRef = (0, react.useRef)("");
|
|
747
|
+
const colorSchemeRef = (0, react.useRef)(colorScheme);
|
|
748
|
+
colorSchemeRef.current = colorScheme;
|
|
749
|
+
const localeRef = (0, react.useRef)(locale);
|
|
750
|
+
localeRef.current = locale;
|
|
751
|
+
const takeoverRequestRef = (0, react.useRef)(onTakeoverRequest);
|
|
752
|
+
takeoverRequestRef.current = onTakeoverRequest;
|
|
753
|
+
const lifecycleStateRef = (0, react.useRef)(onLifecycleState);
|
|
754
|
+
lifecycleStateRef.current = onLifecycleState;
|
|
755
|
+
const lifecycleControllerRef = (0, react.useRef)(onLifecycleController);
|
|
756
|
+
lifecycleControllerRef.current = onLifecycleController;
|
|
757
|
+
const stopInitLoopRef = (0, react.useRef)();
|
|
758
|
+
const selectionPollStopRef = (0, react.useRef)();
|
|
759
|
+
const requestCounterRef = (0, react.useRef)(0);
|
|
760
|
+
const saveWaitersRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
761
|
+
const [phase, setPhase] = (0, react.useState)("launching");
|
|
762
|
+
const phaseRef = (0, react.useRef)("launching");
|
|
763
|
+
const [failure, setFailure] = (0, react.useState)("");
|
|
764
|
+
const [dirty, setDirty] = (0, react.useState)(false);
|
|
765
|
+
const dirtyRef = (0, react.useRef)(false);
|
|
766
|
+
const saveInFlightRef = (0, react.useRef)();
|
|
767
|
+
const closeDaemonRef = (0, react.useRef)();
|
|
768
|
+
const retainServerSessionOnUnmountRef = (0, react.useRef)(false);
|
|
769
|
+
const restoredRecoveryRef = (0, react.useRef)(false);
|
|
770
|
+
const documentGrant = grant.document;
|
|
771
|
+
const editorGrant = grant.editor;
|
|
772
|
+
const publishLifecycle = (0, react.useCallback)(() => {
|
|
773
|
+
lifecycleStateRef.current?.({
|
|
774
|
+
dirty: dirtyRef.current,
|
|
775
|
+
phase: phaseRef.current
|
|
776
|
+
});
|
|
777
|
+
}, []);
|
|
778
|
+
const updatePhase = (0, react.useCallback)((next) => {
|
|
779
|
+
phaseRef.current = next;
|
|
780
|
+
setPhase(next);
|
|
781
|
+
publishLifecycle();
|
|
782
|
+
}, [publishLifecycle]);
|
|
783
|
+
const updateDirty = (0, react.useCallback)((next) => {
|
|
784
|
+
dirtyRef.current = next;
|
|
785
|
+
setDirty(next);
|
|
786
|
+
publishLifecycle();
|
|
787
|
+
}, [publishLifecycle]);
|
|
788
|
+
const post = (0, react.useCallback)((message) => {
|
|
789
|
+
const frame = iframeRef.current?.contentWindow;
|
|
790
|
+
if (frame === null || frame === void 0 || originRef.current === "") return;
|
|
791
|
+
frame.postMessage(encodeEditorOutbound(message), originRef.current);
|
|
792
|
+
}, []);
|
|
793
|
+
const save = (0, react.useCallback)(async () => {
|
|
794
|
+
if (saveInFlightRef.current !== void 0) return saveInFlightRef.current;
|
|
795
|
+
const launch = launchRef.current;
|
|
796
|
+
if (launch === void 0 || phaseRef.current === "launching" || phaseRef.current === "loading") return false;
|
|
797
|
+
if (!dirtyRef.current) return true;
|
|
798
|
+
const operation = (async () => {
|
|
799
|
+
updatePhase("saving");
|
|
800
|
+
setFailure("");
|
|
801
|
+
const requestId = `dsh-save-${++requestCounterRef.current}`;
|
|
802
|
+
let snapshotTimer;
|
|
803
|
+
try {
|
|
804
|
+
const snapshot = await Promise.race([new Promise((resolve, reject) => {
|
|
805
|
+
saveWaitersRef.current.set(requestId, {
|
|
806
|
+
resolve,
|
|
807
|
+
reject
|
|
808
|
+
});
|
|
809
|
+
post({
|
|
810
|
+
type: "op-bridge/snapshot",
|
|
811
|
+
purpose: "save",
|
|
812
|
+
requestId
|
|
813
|
+
});
|
|
814
|
+
}), new Promise((_, reject) => {
|
|
815
|
+
snapshotTimer = setTimeout(() => {
|
|
816
|
+
reject(/* @__PURE__ */ new Error("OpenPencil snapshot timed out"));
|
|
817
|
+
}, 8e3);
|
|
818
|
+
})]);
|
|
819
|
+
const saveResponse = await responseJson(await fetch(launch.saveUrl, {
|
|
820
|
+
method: "POST",
|
|
821
|
+
credentials: "same-origin",
|
|
822
|
+
headers: { "content-type": "application/json" },
|
|
823
|
+
body: JSON.stringify({
|
|
824
|
+
sessionId: launch.sessionId,
|
|
825
|
+
docJson: snapshot.docJson,
|
|
826
|
+
generation: snapshot.generation,
|
|
827
|
+
revision: snapshot.revision
|
|
828
|
+
})
|
|
829
|
+
}), "OpenPencil save");
|
|
830
|
+
rememberEditorSuccessor(editorGrant.launchUrl, saveResponse);
|
|
831
|
+
post({
|
|
832
|
+
type: "op-bridge/save-committed",
|
|
833
|
+
generation: snapshot.generation,
|
|
834
|
+
revision: snapshot.revision
|
|
835
|
+
});
|
|
836
|
+
restoredRecoveryRef.current = false;
|
|
837
|
+
updateDirty(false);
|
|
838
|
+
updatePhase("ready");
|
|
839
|
+
return true;
|
|
840
|
+
} catch (error) {
|
|
841
|
+
setFailure(error instanceof Error ? error.message : String(error));
|
|
842
|
+
updatePhase("error");
|
|
843
|
+
return false;
|
|
844
|
+
} finally {
|
|
845
|
+
if (snapshotTimer !== void 0) clearTimeout(snapshotTimer);
|
|
846
|
+
saveWaitersRef.current.delete(requestId);
|
|
847
|
+
}
|
|
848
|
+
})();
|
|
849
|
+
saveInFlightRef.current = operation;
|
|
850
|
+
try {
|
|
851
|
+
return await operation;
|
|
852
|
+
} finally {
|
|
853
|
+
if (saveInFlightRef.current === operation) saveInFlightRef.current = void 0;
|
|
854
|
+
}
|
|
855
|
+
}, [
|
|
856
|
+
editorGrant.launchUrl,
|
|
857
|
+
post,
|
|
858
|
+
updateDirty,
|
|
859
|
+
updatePhase
|
|
860
|
+
]);
|
|
861
|
+
(0, react.useEffect)(() => {
|
|
862
|
+
publishLifecycle();
|
|
863
|
+
lifecycleControllerRef.current?.({
|
|
864
|
+
requestClose: async () => {
|
|
865
|
+
if (phaseRef.current === "saving") return false;
|
|
866
|
+
const closeDaemon = closeDaemonRef.current;
|
|
867
|
+
if (closeDaemon === void 0) return false;
|
|
868
|
+
try {
|
|
869
|
+
await closeDaemon(dirtyRef.current);
|
|
870
|
+
return true;
|
|
871
|
+
} catch (error) {
|
|
872
|
+
setFailure(error instanceof Error ? error.message : String(error));
|
|
873
|
+
updatePhase("error");
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
captureRecovery: async () => {
|
|
878
|
+
const launch = launchRef.current;
|
|
879
|
+
if (launch?.recoveryUrl === void 0) return false;
|
|
880
|
+
try {
|
|
881
|
+
await captureManagedEditorRecovery(launch);
|
|
882
|
+
return true;
|
|
883
|
+
} catch {
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
},
|
|
887
|
+
awaitExistingSave: async () => {
|
|
888
|
+
const pending = saveInFlightRef.current;
|
|
889
|
+
return pending === void 0 ? true : pending;
|
|
890
|
+
},
|
|
891
|
+
retainServerSessionOnUnmount: () => {
|
|
892
|
+
retainServerSessionOnUnmountRef.current = true;
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
return () => {
|
|
896
|
+
lifecycleControllerRef.current?.(void 0);
|
|
897
|
+
};
|
|
898
|
+
}, [publishLifecycle, updatePhase]);
|
|
899
|
+
(0, react.useEffect)(() => {
|
|
900
|
+
let cancelled = false;
|
|
901
|
+
const abort = new AbortController();
|
|
902
|
+
const coordinatorToken = Symbol("openpencil-editor");
|
|
903
|
+
const closeDaemon = async (dirtyAtClose = dirtyRef.current) => {
|
|
904
|
+
selectionPollStopRef.current?.();
|
|
905
|
+
selectionPollStopRef.current = void 0;
|
|
906
|
+
clearOpenPencilSelection(sessionId, documentGrant.path);
|
|
907
|
+
const launch = launchRef.current;
|
|
908
|
+
if (launch === void 0) return;
|
|
909
|
+
await closeManagedEditorLaunch(launch, {
|
|
910
|
+
dirty: dirtyAtClose,
|
|
911
|
+
keepalive: true
|
|
912
|
+
});
|
|
913
|
+
if (launchRef.current === launch) launchRef.current = void 0;
|
|
914
|
+
};
|
|
915
|
+
closeDaemonRef.current = closeDaemon;
|
|
916
|
+
const releaseEditor = claimEditor(coordinatorToken, () => {
|
|
917
|
+
const takeoverRequest = takeoverRequestRef.current;
|
|
918
|
+
const lifecycle = {
|
|
919
|
+
dirty: dirtyRef.current,
|
|
920
|
+
phase: phaseRef.current
|
|
921
|
+
};
|
|
922
|
+
if (takeoverRequest !== void 0 && !takeoverRequest(lifecycle)) return false;
|
|
923
|
+
if (phaseRef.current === "saving") return false;
|
|
924
|
+
if (takeoverRequest === void 0 && !confirmEditorClose(dirtyRef.current, () => window.confirm(editorPanelCopy(localeRef.current).discard))) return false;
|
|
925
|
+
abort.abort();
|
|
926
|
+
closeDaemon(dirtyRef.current);
|
|
927
|
+
return true;
|
|
928
|
+
}, { replace: allowTakeover });
|
|
929
|
+
if (releaseEditor === void 0) {
|
|
930
|
+
setFailure(editorPanelCopy(localeRef.current).editorBusy);
|
|
931
|
+
updatePhase("error");
|
|
932
|
+
return () => {
|
|
933
|
+
abort.abort();
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
const boot = async () => {
|
|
937
|
+
try {
|
|
938
|
+
const prepared = await prepareManagedEditorForMount(editorGrantForBoot(editorGrant), documentGrant, () => !cancelled && !abort.signal.aborted, { sessionId });
|
|
939
|
+
if (prepared === void 0) return;
|
|
940
|
+
const { launch } = prepared;
|
|
941
|
+
launchRef.current = launch;
|
|
942
|
+
let { documentJson } = prepared;
|
|
943
|
+
if (launch.recovery !== void 0) {
|
|
944
|
+
const recoveryCopy = editorRecoveryCopy(localeRef.current);
|
|
945
|
+
const message = launch.recovery.sourceChangedSinceCapture ? recoveryCopy.conflict(launch.recovery.sourceName) : recoveryCopy.available(launch.recovery.sourceName);
|
|
946
|
+
if (window.confirm(message)) {
|
|
947
|
+
documentJson = await restoreManagedEditorRecovery(launch, launch.recovery);
|
|
948
|
+
restoredRecoveryRef.current = true;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
const origin = editorOrigin(launch.iframeUrl);
|
|
952
|
+
iframeSrcRef.current = editorIframeUrlWithLocale(editorIframeUrlWithTheme(launch.iframeUrl, colorSchemeRef.current), localeRef.current);
|
|
953
|
+
docJsonRef.current = documentJson;
|
|
954
|
+
originRef.current = origin;
|
|
955
|
+
updatePhase("loading");
|
|
956
|
+
} catch (error) {
|
|
957
|
+
await closeDaemon(dirtyRef.current).catch(() => {});
|
|
958
|
+
if (cancelled || abort.signal.aborted) return;
|
|
959
|
+
setFailure(error instanceof Error ? error.message : String(error));
|
|
960
|
+
updatePhase("error");
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
boot();
|
|
964
|
+
return () => {
|
|
965
|
+
cancelled = true;
|
|
966
|
+
abort.abort();
|
|
967
|
+
releaseEditor();
|
|
968
|
+
stopInitLoopRef.current?.();
|
|
969
|
+
stopInitLoopRef.current = void 0;
|
|
970
|
+
const disposed = /* @__PURE__ */ new Error("OpenPencil editor closed");
|
|
971
|
+
for (const waiter of saveWaitersRef.current.values()) waiter.reject(disposed);
|
|
972
|
+
saveWaitersRef.current.clear();
|
|
973
|
+
if (closeDaemonRef.current === closeDaemon) closeDaemonRef.current = void 0;
|
|
974
|
+
if (applyManagedEditorUnmountPolicy({
|
|
975
|
+
retainServerSession: retainServerSessionOnUnmountRef.current,
|
|
976
|
+
dirty: dirtyRef.current,
|
|
977
|
+
hasLiveLaunch: launchRef.current !== void 0
|
|
978
|
+
}, () => {
|
|
979
|
+
closeDaemon().catch(() => {});
|
|
980
|
+
}) === "retained") {
|
|
981
|
+
selectionPollStopRef.current?.();
|
|
982
|
+
selectionPollStopRef.current = void 0;
|
|
983
|
+
clearOpenPencilSelection(sessionId, documentGrant.path);
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
}, [
|
|
987
|
+
allowTakeover,
|
|
988
|
+
documentGrant.path,
|
|
989
|
+
documentGrant.url,
|
|
990
|
+
editorGrant.launchUrl,
|
|
991
|
+
editorGrant.refreshUrl,
|
|
992
|
+
updatePhase
|
|
993
|
+
]);
|
|
994
|
+
const startInitLoop = (0, react.useCallback)(() => {
|
|
995
|
+
const launch = launchRef.current;
|
|
996
|
+
if (launch === void 0) return;
|
|
997
|
+
stopInitLoopRef.current?.();
|
|
998
|
+
stopInitLoopRef.current = beginEditorInitRetry(() => {
|
|
999
|
+
post({
|
|
1000
|
+
type: "op-bridge/init",
|
|
1001
|
+
token: launch.token
|
|
1002
|
+
});
|
|
1003
|
+
post({
|
|
1004
|
+
type: "op-bridge/theme",
|
|
1005
|
+
colorScheme: colorSchemeRef.current
|
|
1006
|
+
});
|
|
1007
|
+
post({
|
|
1008
|
+
type: "op-bridge/locale",
|
|
1009
|
+
locale: localeRef.current
|
|
1010
|
+
});
|
|
1011
|
+
}, () => {
|
|
1012
|
+
stopInitLoopRef.current = void 0;
|
|
1013
|
+
setFailure(editorPanelCopy(localeRef.current).editorTimeout);
|
|
1014
|
+
updatePhase("error");
|
|
1015
|
+
}, {
|
|
1016
|
+
schedule: (callback, delayMs) => window.setInterval(callback, delayMs),
|
|
1017
|
+
cancel: (handle) => {
|
|
1018
|
+
window.clearInterval(handle);
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
}, [post, updatePhase]);
|
|
1022
|
+
(0, react.useEffect)(() => {
|
|
1023
|
+
if (phase !== "loading" || launchRef.current === void 0 || iframeRef.current === null) return;
|
|
1024
|
+
startInitLoop();
|
|
1025
|
+
return () => {
|
|
1026
|
+
stopInitLoopRef.current?.();
|
|
1027
|
+
stopInitLoopRef.current = void 0;
|
|
1028
|
+
};
|
|
1029
|
+
}, [phase, startInitLoop]);
|
|
1030
|
+
(0, react.useEffect)(() => {
|
|
1031
|
+
if (phase !== "ready" && phase !== "saving") return;
|
|
1032
|
+
const selectionUrl = launchRef.current?.selectionUrl;
|
|
1033
|
+
if (selectionUrl === void 0) return;
|
|
1034
|
+
const stop = startEditorSelectionPolling({
|
|
1035
|
+
url: selectionUrl,
|
|
1036
|
+
onValue: (value) => {
|
|
1037
|
+
if (!isRecord$2(value)) return;
|
|
1038
|
+
const selection = liveSelectionOf(value.selection);
|
|
1039
|
+
if (selection !== void 0) publishOpenPencilSelection(sessionId, selection);
|
|
1040
|
+
},
|
|
1041
|
+
onStop: () => {
|
|
1042
|
+
clearOpenPencilSelection(sessionId, documentGrant.path);
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
selectionPollStopRef.current = stop;
|
|
1046
|
+
return () => {
|
|
1047
|
+
if (selectionPollStopRef.current === stop) selectionPollStopRef.current = void 0;
|
|
1048
|
+
stop();
|
|
1049
|
+
};
|
|
1050
|
+
}, [
|
|
1051
|
+
documentGrant.path,
|
|
1052
|
+
phase,
|
|
1053
|
+
sessionId
|
|
1054
|
+
]);
|
|
1055
|
+
(0, react.useEffect)(() => {
|
|
1056
|
+
const listener = (event) => {
|
|
1057
|
+
const message = editorMessageFrom(event, iframeRef.current?.contentWindow ?? null, originRef.current);
|
|
1058
|
+
if (message === void 0) return;
|
|
1059
|
+
switch (message.type) {
|
|
1060
|
+
case "op-bridge/ready":
|
|
1061
|
+
stopInitLoopRef.current?.();
|
|
1062
|
+
stopInitLoopRef.current = void 0;
|
|
1063
|
+
post({
|
|
1064
|
+
type: "op-bridge/theme",
|
|
1065
|
+
colorScheme: colorSchemeRef.current
|
|
1066
|
+
});
|
|
1067
|
+
post({
|
|
1068
|
+
type: "op-bridge/locale",
|
|
1069
|
+
locale: localeRef.current
|
|
1070
|
+
});
|
|
1071
|
+
post({
|
|
1072
|
+
type: "op-bridge/open-document",
|
|
1073
|
+
json: docJsonRef.current
|
|
1074
|
+
});
|
|
1075
|
+
break;
|
|
1076
|
+
case "op-bridge/opened":
|
|
1077
|
+
updatePhase("ready");
|
|
1078
|
+
if (restoredRecoveryRef.current) updateDirty(true);
|
|
1079
|
+
break;
|
|
1080
|
+
case "op-bridge/dirty-changed":
|
|
1081
|
+
updateDirty(restoredRecoveryRef.current || message.dirty);
|
|
1082
|
+
break;
|
|
1083
|
+
case "op-bridge/snapshot-result":
|
|
1084
|
+
saveWaitersRef.current.get(message.requestId)?.resolve(message);
|
|
1085
|
+
break;
|
|
1086
|
+
case "op-bridge/snapshot-conflict":
|
|
1087
|
+
saveWaitersRef.current.get(message.requestId)?.reject(new Error(editorPanelCopy(localeRef.current).saveConflict(message.serverVersion)));
|
|
1088
|
+
break;
|
|
1089
|
+
case "op-bridge/sync-conflict":
|
|
1090
|
+
setFailure(editorPanelCopy(localeRef.current).syncConflict(message.serverVersion));
|
|
1091
|
+
updatePhase("error");
|
|
1092
|
+
break;
|
|
1093
|
+
case "op-shell/save":
|
|
1094
|
+
save();
|
|
1095
|
+
break;
|
|
1096
|
+
case "op-shell/copy": navigator.clipboard?.writeText(message.text).catch(() => {});
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
window.addEventListener("message", listener);
|
|
1100
|
+
return () => {
|
|
1101
|
+
window.removeEventListener("message", listener);
|
|
1102
|
+
};
|
|
1103
|
+
}, [
|
|
1104
|
+
post,
|
|
1105
|
+
save,
|
|
1106
|
+
updateDirty,
|
|
1107
|
+
updatePhase
|
|
1108
|
+
]);
|
|
1109
|
+
(0, react.useEffect)(() => {
|
|
1110
|
+
post({
|
|
1111
|
+
type: "op-bridge/theme",
|
|
1112
|
+
colorScheme
|
|
1113
|
+
});
|
|
1114
|
+
}, [colorScheme, post]);
|
|
1115
|
+
(0, react.useEffect)(() => {
|
|
1116
|
+
post({
|
|
1117
|
+
type: "op-bridge/locale",
|
|
1118
|
+
locale
|
|
1119
|
+
});
|
|
1120
|
+
}, [locale, post]);
|
|
1121
|
+
(0, react.useEffect)(() => {
|
|
1122
|
+
const beforeUnload = (event) => {
|
|
1123
|
+
if (!dirtyRef.current) return;
|
|
1124
|
+
event.preventDefault();
|
|
1125
|
+
event.returnValue = "";
|
|
1126
|
+
};
|
|
1127
|
+
window.addEventListener("beforeunload", beforeUnload);
|
|
1128
|
+
return () => {
|
|
1129
|
+
window.removeEventListener("beforeunload", beforeUnload);
|
|
1130
|
+
};
|
|
1131
|
+
}, []);
|
|
1132
|
+
const title = documentGrant.path?.replaceAll("\\", "/").split("/").at(-1) ?? "OpenPencil";
|
|
1133
|
+
const copy = editorPanelCopy(locale);
|
|
1134
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1135
|
+
style: panelStyles.root,
|
|
1136
|
+
"data-tool-details-fill": "true",
|
|
1137
|
+
"data-tool-details-dirty": dirty || void 0,
|
|
1138
|
+
"data-openpencil-editor-panel": "true",
|
|
1139
|
+
"aria-busy": phase === "launching" || phase === "loading" || phase === "saving" || void 0,
|
|
1140
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1141
|
+
style: panelStyles.toolbar,
|
|
1142
|
+
children: [
|
|
1143
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
1144
|
+
style: panelStyles.title,
|
|
1145
|
+
title: documentGrant.path,
|
|
1146
|
+
children: title
|
|
1147
|
+
}),
|
|
1148
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1149
|
+
style: panelStyles.status,
|
|
1150
|
+
children: phase === "saving" ? copy.saving : dirty ? copy.unsaved : phase === "ready" ? copy.saved : ""
|
|
1151
|
+
}),
|
|
1152
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1153
|
+
type: "button",
|
|
1154
|
+
style: panelStyles.button,
|
|
1155
|
+
disabled: !dirty || phase === "saving",
|
|
1156
|
+
onClick: () => {
|
|
1157
|
+
save();
|
|
1158
|
+
},
|
|
1159
|
+
children: copy.save
|
|
1160
|
+
}),
|
|
1161
|
+
workbenchActions
|
|
1162
|
+
]
|
|
1163
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1164
|
+
style: panelStyles.stage,
|
|
1165
|
+
children: [
|
|
1166
|
+
launchRef.current !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
|
|
1167
|
+
ref: iframeRef,
|
|
1168
|
+
style: panelStyles.iframe,
|
|
1169
|
+
src: iframeSrcRef.current,
|
|
1170
|
+
title: copy.editorTitle(title),
|
|
1171
|
+
allow: "clipboard-read; clipboard-write",
|
|
1172
|
+
tabIndex: phase === "ready" || phase === "saving" ? 0 : -1
|
|
1173
|
+
}) : null,
|
|
1174
|
+
phase === "launching" || phase === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1175
|
+
style: panelStyles.overlay,
|
|
1176
|
+
role: "status",
|
|
1177
|
+
children: copy.loading
|
|
1178
|
+
}) : null,
|
|
1179
|
+
phase === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1180
|
+
style: panelStyles.overlay,
|
|
1181
|
+
role: "alert",
|
|
1182
|
+
children: [
|
|
1183
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: copy.errorTitle }),
|
|
1184
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1185
|
+
style: panelStyles.error,
|
|
1186
|
+
children: failure
|
|
1187
|
+
}),
|
|
1188
|
+
grant.image !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
1189
|
+
href: grant.image.previewUrl,
|
|
1190
|
+
target: "_blank",
|
|
1191
|
+
rel: "noreferrer",
|
|
1192
|
+
children: copy.pngFallback
|
|
1193
|
+
}) : null
|
|
1194
|
+
]
|
|
1195
|
+
}) : null
|
|
1196
|
+
]
|
|
1197
|
+
})]
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
//#endregion
|
|
1201
|
+
//#region src/client/details-compat.ts
|
|
1202
|
+
/** Prefer the native resident details panel and otherwise open our own modal. */
|
|
1203
|
+
function requestOpenPencilEditor(openDetails, openModal) {
|
|
1204
|
+
if (openDetails !== void 0) {
|
|
1205
|
+
openDetails();
|
|
1206
|
+
return "details";
|
|
1207
|
+
}
|
|
1208
|
+
openModal();
|
|
1209
|
+
return "modal";
|
|
1210
|
+
}
|
|
1211
|
+
//#endregion
|
|
1212
|
+
//#region src/client/editor-dock-layout.ts
|
|
1213
|
+
/** Self-contained DSH layout push used by the fallback OpenPencil workbench. */
|
|
1214
|
+
const OPENPENCIL_WORKBENCH_DOCK_ATTRIBUTE = "openpencilWorkbenchDockOwner";
|
|
1215
|
+
function dockWidth(width) {
|
|
1216
|
+
return `${Math.max(0, Math.round(width))}px`;
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Reserve real layout space for the fixed right-hand workbench.
|
|
1220
|
+
*
|
|
1221
|
+
* DSH's root is an auto-width block, so a right margin shrinks its AppFrame
|
|
1222
|
+
* grid instead of covering the conversation. Ownership and exact inline-style
|
|
1223
|
+
* restoration keep this compatible with HMR and fail closed around another
|
|
1224
|
+
* plugin that already owns the root margin.
|
|
1225
|
+
*/
|
|
1226
|
+
function claimEditorWorkbenchDock(root, owner, initialWidth, computedMarginRight = 0) {
|
|
1227
|
+
const existingOwner = root.dataset[OPENPENCIL_WORKBENCH_DOCK_ATTRIBUTE];
|
|
1228
|
+
if (existingOwner !== void 0 && existingOwner !== owner) return void 0;
|
|
1229
|
+
if (existingOwner === void 0 && (root.style.marginRight.trim() !== "" || Number.isFinite(computedMarginRight) && computedMarginRight > .5)) return void 0;
|
|
1230
|
+
const previousMarginRight = root.style.marginRight;
|
|
1231
|
+
const previousMinWidth = root.style.minWidth;
|
|
1232
|
+
root.dataset[OPENPENCIL_WORKBENCH_DOCK_ATTRIBUTE] = owner;
|
|
1233
|
+
root.style.minWidth = "0";
|
|
1234
|
+
let released = false;
|
|
1235
|
+
const update = (width) => {
|
|
1236
|
+
if (released || root.dataset["openpencilWorkbenchDockOwner"] !== owner) return;
|
|
1237
|
+
root.style.marginRight = dockWidth(width);
|
|
1238
|
+
};
|
|
1239
|
+
const release = () => {
|
|
1240
|
+
if (released) return;
|
|
1241
|
+
released = true;
|
|
1242
|
+
if (root.dataset["openpencilWorkbenchDockOwner"] !== owner) return;
|
|
1243
|
+
root.style.marginRight = previousMarginRight;
|
|
1244
|
+
root.style.minWidth = previousMinWidth;
|
|
1245
|
+
delete root.dataset[OPENPENCIL_WORKBENCH_DOCK_ATTRIBUTE];
|
|
1246
|
+
};
|
|
1247
|
+
update(initialWidth);
|
|
1248
|
+
return {
|
|
1249
|
+
update,
|
|
1250
|
+
release
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
//#endregion
|
|
1254
|
+
//#region src/client/editor-modal.tsx
|
|
1255
|
+
/** Plugin-owned OpenPencil workbench for DSH builds without Tool details. */
|
|
1256
|
+
const EDITOR_WORKBENCH_FULLSCREEN_BREAKPOINT = 1480;
|
|
1257
|
+
const EDITOR_WORKBENCH_MIN_WIDTH = 640;
|
|
1258
|
+
const EDITOR_WORKBENCH_MAX_WIDTH = 960;
|
|
1259
|
+
const EDITOR_WORKBENCH_LEFT_CLEARANCE = 840;
|
|
1260
|
+
const EDITOR_WORKBENCH_RESIZE_STEP = 32;
|
|
1261
|
+
let bodyScrollLockCount = 0;
|
|
1262
|
+
let bodyOverflowBeforeLock = "";
|
|
1263
|
+
function lockBodyScroll() {
|
|
1264
|
+
if (bodyScrollLockCount === 0) {
|
|
1265
|
+
bodyOverflowBeforeLock = document.body.style.overflow;
|
|
1266
|
+
document.body.style.overflow = "hidden";
|
|
1267
|
+
}
|
|
1268
|
+
bodyScrollLockCount += 1;
|
|
1269
|
+
let released = false;
|
|
1270
|
+
return () => {
|
|
1271
|
+
if (released) return;
|
|
1272
|
+
released = true;
|
|
1273
|
+
bodyScrollLockCount = Math.max(0, bodyScrollLockCount - 1);
|
|
1274
|
+
if (bodyScrollLockCount === 0) document.body.style.overflow = bodyOverflowBeforeLock;
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
const EDITOR_WORKBENCH_COPY = {
|
|
1278
|
+
"zh-CN": {
|
|
1279
|
+
title: "OpenPencil 编辑器",
|
|
1280
|
+
close: "关闭",
|
|
1281
|
+
fullscreen: "全屏",
|
|
1282
|
+
restore: "退出全屏",
|
|
1283
|
+
resize: "拖动调整编辑区宽度",
|
|
1284
|
+
discard: "OpenPencil 中有未保存的更改,确定关闭并放弃吗?"
|
|
1285
|
+
},
|
|
1286
|
+
"en-US": {
|
|
1287
|
+
title: "OpenPencil editor",
|
|
1288
|
+
close: "Close",
|
|
1289
|
+
fullscreen: "Full screen",
|
|
1290
|
+
restore: "Exit full screen",
|
|
1291
|
+
resize: "Drag to resize the editor",
|
|
1292
|
+
discard: "OpenPencil has unsaved changes. Close and discard them?"
|
|
1293
|
+
}
|
|
1294
|
+
};
|
|
1295
|
+
function editorModalCopy(locale) {
|
|
1296
|
+
return EDITOR_WORKBENCH_COPY[locale];
|
|
1297
|
+
}
|
|
1298
|
+
function editorWorkbenchUsesFullscreen(viewportWidth) {
|
|
1299
|
+
return !Number.isFinite(viewportWidth) || viewportWidth < 1480;
|
|
1300
|
+
}
|
|
1301
|
+
/** Keep useful DSH conversation space while allowing a large desktop canvas. */
|
|
1302
|
+
function editorWorkbenchWidthBounds(viewportWidth) {
|
|
1303
|
+
const available = Math.max(0, (Number.isFinite(viewportWidth) ? Math.max(0, viewportWidth) : 0) - 840);
|
|
1304
|
+
const max = Math.min(960, Math.max(640, available));
|
|
1305
|
+
const min = Math.min(640, max);
|
|
1306
|
+
return {
|
|
1307
|
+
min,
|
|
1308
|
+
max,
|
|
1309
|
+
initial: Math.min(max, Math.max(min, 720))
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
function clampEditorWorkbenchWidth(width, viewportWidth) {
|
|
1313
|
+
const bounds = editorWorkbenchWidthBounds(viewportWidth);
|
|
1314
|
+
const safeWidth = Number.isFinite(width) ? width : bounds.initial;
|
|
1315
|
+
return Math.min(bounds.max, Math.max(bounds.min, safeWidth));
|
|
1316
|
+
}
|
|
1317
|
+
/** A left-edge drag grows the right-docked workbench as the pointer moves left. */
|
|
1318
|
+
function resizedEditorWorkbenchWidth(startWidth, startClientX, clientX, viewportWidth) {
|
|
1319
|
+
return clampEditorWorkbenchWidth(startWidth + startClientX - clientX, viewportWidth);
|
|
1320
|
+
}
|
|
1321
|
+
/** Key only the editor process; outer workbench geometry remains stable. */
|
|
1322
|
+
function editorWorkbenchEditorKey(grant, sessionId) {
|
|
1323
|
+
return `${sessionId}\n${grant.editor?.launchUrl ?? ""}`;
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Return the focus target used at a fullscreen Tab boundary.
|
|
1327
|
+
*
|
|
1328
|
+
* `activeIndex` is -1 when focus is outside the workbench. Returning -1 means
|
|
1329
|
+
* normal browser tab order should continue inside the workbench.
|
|
1330
|
+
*/
|
|
1331
|
+
function editorWorkbenchFocusTargetIndex(focusableCount, activeIndex, backwards) {
|
|
1332
|
+
if (!Number.isInteger(focusableCount) || focusableCount <= 0) return -1;
|
|
1333
|
+
if (!Number.isInteger(activeIndex) || activeIndex < 0 || activeIndex >= focusableCount) return backwards ? focusableCount - 1 : 0;
|
|
1334
|
+
if (backwards && activeIndex === 0) return focusableCount - 1;
|
|
1335
|
+
if (!backwards && activeIndex === focusableCount - 1) return 0;
|
|
1336
|
+
return -1;
|
|
1337
|
+
}
|
|
1338
|
+
/** Side mode is non-modal, so Escape only belongs to it while focus is inside. */
|
|
1339
|
+
function editorWorkbenchShouldHandleEscape(fullscreen, targetInside) {
|
|
1340
|
+
return fullscreen || targetInside;
|
|
1341
|
+
}
|
|
1342
|
+
/** Read the editor's durable dirty marker before allowing the workbench to close. */
|
|
1343
|
+
function confirmEditorModalClose(root, message, confirm = window.confirm) {
|
|
1344
|
+
return confirmEditorClose((root?.querySelector("[data-tool-details-dirty=\"true\"]") ?? null) !== null, () => confirm(message));
|
|
1345
|
+
}
|
|
1346
|
+
const styles$3 = {
|
|
1347
|
+
surface: {
|
|
1348
|
+
position: "fixed",
|
|
1349
|
+
top: 0,
|
|
1350
|
+
right: 0,
|
|
1351
|
+
bottom: 0,
|
|
1352
|
+
zIndex: 1100,
|
|
1353
|
+
boxSizing: "border-box",
|
|
1354
|
+
minWidth: 0,
|
|
1355
|
+
minHeight: 0,
|
|
1356
|
+
display: "flex",
|
|
1357
|
+
flexDirection: "column",
|
|
1358
|
+
overflow: "hidden",
|
|
1359
|
+
borderLeft: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.5))",
|
|
1360
|
+
color: "var(--dsw-alias-label-primary, #202124)",
|
|
1361
|
+
background: "var(--dsw-alias-bg-base, #fff)"
|
|
1362
|
+
},
|
|
1363
|
+
fullscreen: {
|
|
1364
|
+
left: 0,
|
|
1365
|
+
width: "auto",
|
|
1366
|
+
borderLeft: 0
|
|
1367
|
+
},
|
|
1368
|
+
resizeHandle: {
|
|
1369
|
+
position: "absolute",
|
|
1370
|
+
zIndex: 3,
|
|
1371
|
+
top: 0,
|
|
1372
|
+
bottom: 0,
|
|
1373
|
+
left: -6,
|
|
1374
|
+
width: 12,
|
|
1375
|
+
display: "flex",
|
|
1376
|
+
alignItems: "center",
|
|
1377
|
+
justifyContent: "center",
|
|
1378
|
+
cursor: "ew-resize",
|
|
1379
|
+
touchAction: "none",
|
|
1380
|
+
background: "transparent"
|
|
1381
|
+
},
|
|
1382
|
+
resizeGrip: {
|
|
1383
|
+
width: 3,
|
|
1384
|
+
height: 32,
|
|
1385
|
+
flex: "none",
|
|
1386
|
+
borderRadius: 999,
|
|
1387
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.45))",
|
|
1388
|
+
background: "var(--dsw-alias-button-floating-fill, var(--dsw-alias-bg-layer-2, #fff))",
|
|
1389
|
+
boxShadow: "0 1px 3px rgba(0,0,0,0.18)",
|
|
1390
|
+
pointerEvents: "none"
|
|
1391
|
+
},
|
|
1392
|
+
button: {
|
|
1393
|
+
boxSizing: "border-box",
|
|
1394
|
+
width: 28,
|
|
1395
|
+
height: 28,
|
|
1396
|
+
flex: "none",
|
|
1397
|
+
display: "inline-flex",
|
|
1398
|
+
alignItems: "center",
|
|
1399
|
+
justifyContent: "center",
|
|
1400
|
+
border: 0,
|
|
1401
|
+
borderRadius: 6,
|
|
1402
|
+
color: "var(--dsw-alias-label-secondary, inherit)",
|
|
1403
|
+
background: "transparent",
|
|
1404
|
+
padding: 0,
|
|
1405
|
+
cursor: "pointer",
|
|
1406
|
+
fontFamily: "inherit",
|
|
1407
|
+
fontSize: 12,
|
|
1408
|
+
fontWeight: "inherit",
|
|
1409
|
+
lineHeight: 1
|
|
1410
|
+
},
|
|
1411
|
+
body: {
|
|
1412
|
+
flex: 1,
|
|
1413
|
+
minHeight: 0,
|
|
1414
|
+
overflow: "hidden"
|
|
1415
|
+
},
|
|
1416
|
+
focusGuard: {
|
|
1417
|
+
position: "fixed",
|
|
1418
|
+
width: 1,
|
|
1419
|
+
height: 1,
|
|
1420
|
+
padding: 0,
|
|
1421
|
+
margin: 0,
|
|
1422
|
+
overflow: "hidden",
|
|
1423
|
+
opacity: 0,
|
|
1424
|
+
pointerEvents: "none"
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
const EDITOR_WORKBENCH_FOCUSABLE_SELECTOR = [
|
|
1428
|
+
"a[href]",
|
|
1429
|
+
"button:not([disabled])",
|
|
1430
|
+
"input:not([disabled])",
|
|
1431
|
+
"select:not([disabled])",
|
|
1432
|
+
"textarea:not([disabled])",
|
|
1433
|
+
"iframe:not([tabindex=\"-1\"])",
|
|
1434
|
+
"[tabindex]:not([tabindex=\"-1\"])"
|
|
1435
|
+
].join(",");
|
|
1436
|
+
function editorWorkbenchFocusableElements(surface) {
|
|
1437
|
+
return Array.from(surface.querySelectorAll(EDITOR_WORKBENCH_FOCUSABLE_SELECTOR)).filter((element) => {
|
|
1438
|
+
if (element.dataset.openpencilFocusGuard === "true") return false;
|
|
1439
|
+
if (element.hidden || element.getAttribute("aria-hidden") === "true") return false;
|
|
1440
|
+
if (element.closest("[hidden], [inert], [aria-hidden=\"true\"]")) return false;
|
|
1441
|
+
const style = window.getComputedStyle(element);
|
|
1442
|
+
return style.display !== "none" && style.visibility !== "hidden";
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
function focusEditorWorkbenchBoundary(surface, backwards) {
|
|
1446
|
+
const focusable = editorWorkbenchFocusableElements(surface);
|
|
1447
|
+
((backwards ? focusable.at(-1) : focusable[0]) ?? surface).focus();
|
|
1448
|
+
}
|
|
1449
|
+
function FullscreenIcon() {
|
|
1450
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1451
|
+
"aria-hidden": "true",
|
|
1452
|
+
width: "14",
|
|
1453
|
+
height: "14",
|
|
1454
|
+
viewBox: "0 0 16 16",
|
|
1455
|
+
fill: "none",
|
|
1456
|
+
stroke: "currentColor",
|
|
1457
|
+
strokeWidth: "1.5",
|
|
1458
|
+
strokeLinecap: "round",
|
|
1459
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2.5 6V2.5H6M10 2.5h3.5V6M13.5 10v3.5H10M6 13.5H2.5V10" })
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
function RestoreIcon() {
|
|
1463
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1464
|
+
"aria-hidden": "true",
|
|
1465
|
+
width: "14",
|
|
1466
|
+
height: "14",
|
|
1467
|
+
viewBox: "0 0 16 16",
|
|
1468
|
+
fill: "none",
|
|
1469
|
+
stroke: "currentColor",
|
|
1470
|
+
strokeWidth: "1.5",
|
|
1471
|
+
strokeLinecap: "round",
|
|
1472
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M6 2.5V6H2.5M13.5 6H10V2.5M10 13.5V10h3.5M2.5 10H6v3.5" })
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
function CloseIcon() {
|
|
1476
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1477
|
+
"aria-hidden": "true",
|
|
1478
|
+
width: "14",
|
|
1479
|
+
height: "14",
|
|
1480
|
+
viewBox: "0 0 16 16",
|
|
1481
|
+
fill: "none",
|
|
1482
|
+
stroke: "currentColor",
|
|
1483
|
+
strokeWidth: "1.5",
|
|
1484
|
+
strokeLinecap: "round",
|
|
1485
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3.5 3.5l9 9M12.5 3.5l-9 9" })
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
function ManagedOpenPencilEditorModal({ grant, colorScheme, locale, sessionId, ownerId, onLifecycleState, onLifecycleController, onClose, allowEditorTakeover = true }) {
|
|
1489
|
+
const bodyRef = (0, react.useRef)(null);
|
|
1490
|
+
const surfaceRef = (0, react.useRef)(null);
|
|
1491
|
+
const closeRef = (0, react.useRef)(null);
|
|
1492
|
+
const openerRef = (0, react.useRef)(null);
|
|
1493
|
+
const resizeCleanupRef = (0, react.useRef)();
|
|
1494
|
+
const dockLeaseRef = (0, react.useRef)();
|
|
1495
|
+
const dockOwnerId = (0, react.useId)();
|
|
1496
|
+
const copy = editorModalCopy(locale);
|
|
1497
|
+
const [viewportWidth, setViewportWidth] = (0, react.useState)(() => window.innerWidth);
|
|
1498
|
+
const [requestedFullscreen, setRequestedFullscreen] = (0, react.useState)(false);
|
|
1499
|
+
const [dockUnavailable, setDockUnavailable] = (0, react.useState)(false);
|
|
1500
|
+
const [preferredWidth, setPreferredWidth] = (0, react.useState)(() => editorWorkbenchWidthBounds(window.innerWidth).initial);
|
|
1501
|
+
const [lifecycle, setLifecycle] = (0, react.useState)(INITIAL_EDITOR_LIFECYCLE_STATE);
|
|
1502
|
+
const lifecycleRef = (0, react.useRef)(INITIAL_EDITOR_LIFECYCLE_STATE);
|
|
1503
|
+
const lifecycleControllerRef = (0, react.useRef)();
|
|
1504
|
+
const automaticFullscreen = editorWorkbenchUsesFullscreen(viewportWidth);
|
|
1505
|
+
const fullscreen = automaticFullscreen || requestedFullscreen || dockUnavailable;
|
|
1506
|
+
const width = clampEditorWorkbenchWidth(preferredWidth, viewportWidth);
|
|
1507
|
+
(0, react.useLayoutEffect)(() => {
|
|
1508
|
+
if (fullscreen) return;
|
|
1509
|
+
const root = document.getElementById("root");
|
|
1510
|
+
if (root === null) {
|
|
1511
|
+
setDockUnavailable(true);
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
const computedMarginRight = Number.parseFloat(window.getComputedStyle(root).marginRight);
|
|
1515
|
+
const lease = claimEditorWorkbenchDock(root, dockOwnerId, width, computedMarginRight);
|
|
1516
|
+
if (lease === void 0) {
|
|
1517
|
+
setDockUnavailable(true);
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
dockLeaseRef.current = lease;
|
|
1521
|
+
return () => {
|
|
1522
|
+
if (dockLeaseRef.current === lease) dockLeaseRef.current = void 0;
|
|
1523
|
+
lease.release();
|
|
1524
|
+
};
|
|
1525
|
+
}, [dockOwnerId, fullscreen]);
|
|
1526
|
+
(0, react.useLayoutEffect)(() => {
|
|
1527
|
+
if (!fullscreen) dockLeaseRef.current?.update(width);
|
|
1528
|
+
}, [fullscreen, width]);
|
|
1529
|
+
const closeWithoutPrompt = (0, react.useCallback)(() => {
|
|
1530
|
+
onClose();
|
|
1531
|
+
}, [onClose]);
|
|
1532
|
+
const requestClose = (0, react.useCallback)(async () => {
|
|
1533
|
+
if (lifecycleRef.current.phase === "saving") return;
|
|
1534
|
+
if (!confirmEditorClose(lifecycleRef.current.dirty, () => window.confirm(copy.discard))) return;
|
|
1535
|
+
if (lifecycleControllerRef.current !== void 0) {
|
|
1536
|
+
if (!await lifecycleControllerRef.current.requestClose()) return;
|
|
1537
|
+
}
|
|
1538
|
+
closeWithoutPrompt();
|
|
1539
|
+
}, [closeWithoutPrompt, copy.discard]);
|
|
1540
|
+
const requestTakeover = (0, react.useCallback)((_state) => {
|
|
1541
|
+
return false;
|
|
1542
|
+
}, []);
|
|
1543
|
+
const updateLifecycle = (0, react.useCallback)((next) => {
|
|
1544
|
+
lifecycleRef.current = next;
|
|
1545
|
+
setLifecycle(next);
|
|
1546
|
+
onLifecycleState?.(next);
|
|
1547
|
+
}, [onLifecycleState]);
|
|
1548
|
+
const updateLifecycleController = (0, react.useCallback)((next) => {
|
|
1549
|
+
lifecycleControllerRef.current = next;
|
|
1550
|
+
onLifecycleController?.(next);
|
|
1551
|
+
}, [onLifecycleController]);
|
|
1552
|
+
(0, react.useEffect)(() => {
|
|
1553
|
+
openerRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
1554
|
+
const mountedSurface = surfaceRef.current;
|
|
1555
|
+
closeRef.current?.focus();
|
|
1556
|
+
return () => {
|
|
1557
|
+
const opener = openerRef.current;
|
|
1558
|
+
const activeElement = document.activeElement;
|
|
1559
|
+
if ((activeElement === document.body || activeElement !== null && mountedSurface?.contains(activeElement)) && opener?.isConnected === true) opener.focus();
|
|
1560
|
+
};
|
|
1561
|
+
}, []);
|
|
1562
|
+
(0, react.useEffect)(() => {
|
|
1563
|
+
const onResize = () => {
|
|
1564
|
+
setViewportWidth(window.innerWidth);
|
|
1565
|
+
};
|
|
1566
|
+
window.addEventListener("resize", onResize);
|
|
1567
|
+
return () => {
|
|
1568
|
+
window.removeEventListener("resize", onResize);
|
|
1569
|
+
};
|
|
1570
|
+
}, []);
|
|
1571
|
+
(0, react.useEffect)(() => {
|
|
1572
|
+
const onKeyDown = (event) => {
|
|
1573
|
+
if (event.key !== "Escape") return;
|
|
1574
|
+
const surface = surfaceRef.current;
|
|
1575
|
+
const targetInside = event.target instanceof Node && surface?.contains(event.target) === true;
|
|
1576
|
+
if (!editorWorkbenchShouldHandleEscape(fullscreen, targetInside)) return;
|
|
1577
|
+
event.preventDefault();
|
|
1578
|
+
requestClose();
|
|
1579
|
+
};
|
|
1580
|
+
window.addEventListener("keydown", onKeyDown);
|
|
1581
|
+
return () => {
|
|
1582
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
1583
|
+
};
|
|
1584
|
+
}, [fullscreen, requestClose]);
|
|
1585
|
+
(0, react.useEffect)(() => {
|
|
1586
|
+
if (!fullscreen) return;
|
|
1587
|
+
const surface = surfaceRef.current;
|
|
1588
|
+
if (!surface) return;
|
|
1589
|
+
const containFocus = (event) => {
|
|
1590
|
+
if (event.target instanceof Node && surface.contains(event.target)) return;
|
|
1591
|
+
focusEditorWorkbenchBoundary(surface, false);
|
|
1592
|
+
};
|
|
1593
|
+
const wrapTab = (event) => {
|
|
1594
|
+
if (event.key !== "Tab") return;
|
|
1595
|
+
const focusable = editorWorkbenchFocusableElements(surface);
|
|
1596
|
+
const activeIndex = focusable.indexOf(document.activeElement);
|
|
1597
|
+
const targetIndex = editorWorkbenchFocusTargetIndex(focusable.length, activeIndex, event.shiftKey);
|
|
1598
|
+
if (targetIndex < 0) return;
|
|
1599
|
+
event.preventDefault();
|
|
1600
|
+
event.stopPropagation();
|
|
1601
|
+
focusable[targetIndex]?.focus();
|
|
1602
|
+
};
|
|
1603
|
+
document.addEventListener("focusin", containFocus, true);
|
|
1604
|
+
document.addEventListener("keydown", wrapTab, true);
|
|
1605
|
+
if (!surface.contains(document.activeElement)) focusEditorWorkbenchBoundary(surface, false);
|
|
1606
|
+
return () => {
|
|
1607
|
+
document.removeEventListener("focusin", containFocus, true);
|
|
1608
|
+
document.removeEventListener("keydown", wrapTab, true);
|
|
1609
|
+
};
|
|
1610
|
+
}, [fullscreen]);
|
|
1611
|
+
(0, react.useEffect)(() => {
|
|
1612
|
+
if (!fullscreen) return;
|
|
1613
|
+
return lockBodyScroll();
|
|
1614
|
+
}, [fullscreen]);
|
|
1615
|
+
(0, react.useEffect)(() => () => {
|
|
1616
|
+
resizeCleanupRef.current?.();
|
|
1617
|
+
}, []);
|
|
1618
|
+
(0, react.useEffect)(() => {
|
|
1619
|
+
if (fullscreen) resizeCleanupRef.current?.();
|
|
1620
|
+
}, [fullscreen]);
|
|
1621
|
+
const startResize = (0, react.useCallback)((event) => {
|
|
1622
|
+
if (fullscreen) return;
|
|
1623
|
+
event.preventDefault();
|
|
1624
|
+
resizeCleanupRef.current?.();
|
|
1625
|
+
const handle = event.currentTarget;
|
|
1626
|
+
const pointerId = event.pointerId;
|
|
1627
|
+
const surface = surfaceRef.current;
|
|
1628
|
+
const editorFrame = surface?.querySelector("iframe") ?? null;
|
|
1629
|
+
const inlineSurfaceWidth = surface === null ? NaN : Number.parseFloat(surface.style.width);
|
|
1630
|
+
let liveWidth = Number.isFinite(inlineSurfaceWidth) ? inlineSurfaceWidth : width;
|
|
1631
|
+
let appliedClientX = event.clientX;
|
|
1632
|
+
const previousCursor = document.body.style.cursor;
|
|
1633
|
+
const previousUserSelect = document.body.style.userSelect;
|
|
1634
|
+
const previousFrameStyle = editorFrame === null ? void 0 : {
|
|
1635
|
+
position: editorFrame.style.position,
|
|
1636
|
+
top: editorFrame.style.top,
|
|
1637
|
+
right: editorFrame.style.right,
|
|
1638
|
+
bottom: editorFrame.style.bottom,
|
|
1639
|
+
left: editorFrame.style.left,
|
|
1640
|
+
width: editorFrame.style.width,
|
|
1641
|
+
height: editorFrame.style.height,
|
|
1642
|
+
maxWidth: editorFrame.style.maxWidth,
|
|
1643
|
+
pointerEvents: editorFrame.style.pointerEvents
|
|
1644
|
+
};
|
|
1645
|
+
document.body.style.cursor = "ew-resize";
|
|
1646
|
+
document.body.style.userSelect = "none";
|
|
1647
|
+
if (editorFrame !== null) {
|
|
1648
|
+
const frameWidth = editorFrame.getBoundingClientRect().width;
|
|
1649
|
+
Object.assign(editorFrame.style, {
|
|
1650
|
+
position: "absolute",
|
|
1651
|
+
top: "0",
|
|
1652
|
+
right: "0",
|
|
1653
|
+
bottom: "0",
|
|
1654
|
+
left: "auto",
|
|
1655
|
+
width: `${frameWidth}px`,
|
|
1656
|
+
height: "100%",
|
|
1657
|
+
maxWidth: "none",
|
|
1658
|
+
pointerEvents: "none"
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
try {
|
|
1662
|
+
handle.setPointerCapture(pointerId);
|
|
1663
|
+
} catch {}
|
|
1664
|
+
let animationFrame;
|
|
1665
|
+
let nextClientX = event.clientX;
|
|
1666
|
+
let stopped = false;
|
|
1667
|
+
const applyWidth = (clientX) => {
|
|
1668
|
+
liveWidth = resizedEditorWorkbenchWidth(liveWidth, appliedClientX, clientX, window.innerWidth);
|
|
1669
|
+
appliedClientX = clientX;
|
|
1670
|
+
if (surface !== null) surface.style.width = `${liveWidth}px`;
|
|
1671
|
+
dockLeaseRef.current?.update(liveWidth);
|
|
1672
|
+
handle.setAttribute("aria-valuenow", String(Math.round(liveWidth)));
|
|
1673
|
+
return liveWidth;
|
|
1674
|
+
};
|
|
1675
|
+
const flushWidth = () => {
|
|
1676
|
+
if (animationFrame !== void 0) {
|
|
1677
|
+
window.cancelAnimationFrame(animationFrame);
|
|
1678
|
+
animationFrame = void 0;
|
|
1679
|
+
}
|
|
1680
|
+
return applyWidth(nextClientX);
|
|
1681
|
+
};
|
|
1682
|
+
const onMove = (moveEvent) => {
|
|
1683
|
+
if (moveEvent.pointerId !== pointerId) return;
|
|
1684
|
+
nextClientX = (moveEvent.getCoalescedEvents?.())?.at(-1)?.clientX ?? moveEvent.clientX;
|
|
1685
|
+
if (animationFrame !== void 0) return;
|
|
1686
|
+
animationFrame = window.requestAnimationFrame(() => {
|
|
1687
|
+
animationFrame = void 0;
|
|
1688
|
+
applyWidth(nextClientX);
|
|
1689
|
+
});
|
|
1690
|
+
};
|
|
1691
|
+
const cleanup = () => {
|
|
1692
|
+
if (stopped) return;
|
|
1693
|
+
stopped = true;
|
|
1694
|
+
if (animationFrame !== void 0) window.cancelAnimationFrame(animationFrame);
|
|
1695
|
+
window.removeEventListener("pointermove", onMove, true);
|
|
1696
|
+
window.removeEventListener("pointerup", onPointerEnd, true);
|
|
1697
|
+
window.removeEventListener("pointercancel", onPointerCancel, true);
|
|
1698
|
+
window.removeEventListener("blur", onBlur);
|
|
1699
|
+
handle.removeEventListener("lostpointercapture", onLostPointerCapture);
|
|
1700
|
+
try {
|
|
1701
|
+
if (handle.hasPointerCapture(pointerId)) handle.releasePointerCapture(pointerId);
|
|
1702
|
+
} catch {}
|
|
1703
|
+
document.body.style.cursor = previousCursor;
|
|
1704
|
+
document.body.style.userSelect = previousUserSelect;
|
|
1705
|
+
if (editorFrame !== null && previousFrameStyle !== void 0) Object.assign(editorFrame.style, previousFrameStyle);
|
|
1706
|
+
resizeCleanupRef.current = void 0;
|
|
1707
|
+
};
|
|
1708
|
+
const finish = () => {
|
|
1709
|
+
if (stopped) return;
|
|
1710
|
+
const finalWidth = flushWidth();
|
|
1711
|
+
cleanup();
|
|
1712
|
+
setPreferredWidth(finalWidth);
|
|
1713
|
+
};
|
|
1714
|
+
function onPointerEnd(endEvent) {
|
|
1715
|
+
if (endEvent.pointerId !== pointerId) return;
|
|
1716
|
+
nextClientX = Number.isFinite(endEvent.clientX) ? endEvent.clientX : nextClientX;
|
|
1717
|
+
finish();
|
|
1718
|
+
}
|
|
1719
|
+
function onPointerCancel(cancelEvent) {
|
|
1720
|
+
if (cancelEvent.pointerId === pointerId) finish();
|
|
1721
|
+
}
|
|
1722
|
+
function onBlur() {
|
|
1723
|
+
finish();
|
|
1724
|
+
}
|
|
1725
|
+
function onLostPointerCapture(lostEvent) {
|
|
1726
|
+
if (lostEvent.pointerId === pointerId) finish();
|
|
1727
|
+
}
|
|
1728
|
+
resizeCleanupRef.current = cleanup;
|
|
1729
|
+
window.addEventListener("pointermove", onMove, true);
|
|
1730
|
+
window.addEventListener("pointerup", onPointerEnd, true);
|
|
1731
|
+
window.addEventListener("pointercancel", onPointerCancel, true);
|
|
1732
|
+
window.addEventListener("blur", onBlur);
|
|
1733
|
+
handle.addEventListener("lostpointercapture", onLostPointerCapture);
|
|
1734
|
+
}, [fullscreen, width]);
|
|
1735
|
+
const resizeWithKeyboard = (0, react.useCallback)((event) => {
|
|
1736
|
+
if (fullscreen) return;
|
|
1737
|
+
const bounds = editorWorkbenchWidthBounds(window.innerWidth);
|
|
1738
|
+
let next;
|
|
1739
|
+
if (event.key === "ArrowLeft") next = width + 32;
|
|
1740
|
+
if (event.key === "ArrowRight") next = width - 32;
|
|
1741
|
+
if (event.key === "Home") next = bounds.min;
|
|
1742
|
+
if (event.key === "End") next = bounds.max;
|
|
1743
|
+
if (next === void 0) return;
|
|
1744
|
+
event.preventDefault();
|
|
1745
|
+
setPreferredWidth(clampEditorWorkbenchWidth(next, window.innerWidth));
|
|
1746
|
+
}, [fullscreen, width]);
|
|
1747
|
+
const surface = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1748
|
+
ref: surfaceRef,
|
|
1749
|
+
style: {
|
|
1750
|
+
...styles$3.surface,
|
|
1751
|
+
...fullscreen ? styles$3.fullscreen : { width },
|
|
1752
|
+
...colorScheme === "dark" ? {
|
|
1753
|
+
color: "var(--dsw-alias-label-primary, #eee)",
|
|
1754
|
+
background: "var(--dsw-alias-bg-base, #17171a)"
|
|
1755
|
+
} : {}
|
|
1756
|
+
},
|
|
1757
|
+
role: fullscreen ? "dialog" : "complementary",
|
|
1758
|
+
"aria-modal": fullscreen ? true : void 0,
|
|
1759
|
+
"aria-label": copy.title,
|
|
1760
|
+
"data-openpencil-editor-workbench": "true",
|
|
1761
|
+
"data-openpencil-editor-workbench-owner": ownerId,
|
|
1762
|
+
"data-openpencil-editor-modal": "true",
|
|
1763
|
+
"data-openpencil-editor-mode": fullscreen ? "fullscreen" : "side",
|
|
1764
|
+
tabIndex: -1,
|
|
1765
|
+
children: [
|
|
1766
|
+
fullscreen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1767
|
+
"data-openpencil-focus-guard": "true",
|
|
1768
|
+
style: styles$3.focusGuard,
|
|
1769
|
+
tabIndex: 0,
|
|
1770
|
+
onFocus: () => {
|
|
1771
|
+
if (surfaceRef.current) focusEditorWorkbenchBoundary(surfaceRef.current, true);
|
|
1772
|
+
}
|
|
1773
|
+
}) : null,
|
|
1774
|
+
!fullscreen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1775
|
+
style: styles$3.resizeHandle,
|
|
1776
|
+
role: "separator",
|
|
1777
|
+
"aria-orientation": "vertical",
|
|
1778
|
+
"aria-label": copy.resize,
|
|
1779
|
+
"aria-valuemin": editorWorkbenchWidthBounds(viewportWidth).min,
|
|
1780
|
+
"aria-valuemax": editorWorkbenchWidthBounds(viewportWidth).max,
|
|
1781
|
+
"aria-valuenow": Math.round(width),
|
|
1782
|
+
tabIndex: 0,
|
|
1783
|
+
title: copy.resize,
|
|
1784
|
+
onPointerDown: startResize,
|
|
1785
|
+
onKeyDown: resizeWithKeyboard,
|
|
1786
|
+
onDoubleClick: () => {
|
|
1787
|
+
setPreferredWidth(editorWorkbenchWidthBounds(window.innerWidth).initial);
|
|
1788
|
+
},
|
|
1789
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1790
|
+
style: styles$3.resizeGrip,
|
|
1791
|
+
"aria-hidden": "true"
|
|
1792
|
+
})
|
|
1793
|
+
}) : null,
|
|
1794
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1795
|
+
ref: bodyRef,
|
|
1796
|
+
style: styles$3.body,
|
|
1797
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ManagedOpenPencilEditor, {
|
|
1798
|
+
grant,
|
|
1799
|
+
colorScheme,
|
|
1800
|
+
locale,
|
|
1801
|
+
sessionId,
|
|
1802
|
+
onTakeoverRequest: requestTakeover,
|
|
1803
|
+
onLifecycleState: updateLifecycle,
|
|
1804
|
+
onLifecycleController: updateLifecycleController,
|
|
1805
|
+
allowTakeover: allowEditorTakeover,
|
|
1806
|
+
workbenchActions: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [!automaticFullscreen && !dockUnavailable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1807
|
+
type: "button",
|
|
1808
|
+
style: styles$3.button,
|
|
1809
|
+
"aria-label": fullscreen ? copy.restore : copy.fullscreen,
|
|
1810
|
+
title: fullscreen ? copy.restore : copy.fullscreen,
|
|
1811
|
+
onClick: () => {
|
|
1812
|
+
setRequestedFullscreen((current) => !current);
|
|
1813
|
+
},
|
|
1814
|
+
children: fullscreen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestoreIcon, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FullscreenIcon, {})
|
|
1815
|
+
}) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1816
|
+
ref: closeRef,
|
|
1817
|
+
type: "button",
|
|
1818
|
+
style: {
|
|
1819
|
+
...styles$3.button,
|
|
1820
|
+
...lifecycle.phase === "saving" ? {
|
|
1821
|
+
cursor: "not-allowed",
|
|
1822
|
+
opacity: .55
|
|
1823
|
+
} : {}
|
|
1824
|
+
},
|
|
1825
|
+
"aria-label": copy.close,
|
|
1826
|
+
title: copy.close,
|
|
1827
|
+
disabled: lifecycle.phase === "saving",
|
|
1828
|
+
onClick: () => {
|
|
1829
|
+
requestClose();
|
|
1830
|
+
},
|
|
1831
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CloseIcon, {})
|
|
1832
|
+
})] })
|
|
1833
|
+
}, editorWorkbenchEditorKey(grant, sessionId))
|
|
1834
|
+
}),
|
|
1835
|
+
fullscreen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1836
|
+
"data-openpencil-focus-guard": "true",
|
|
1837
|
+
style: styles$3.focusGuard,
|
|
1838
|
+
tabIndex: 0,
|
|
1839
|
+
onFocus: () => {
|
|
1840
|
+
if (surfaceRef.current) focusEditorWorkbenchBoundary(surfaceRef.current, false);
|
|
1841
|
+
}
|
|
1842
|
+
}) : null
|
|
1843
|
+
]
|
|
1844
|
+
});
|
|
1845
|
+
return (0, react_dom.createPortal)(surface, document.body);
|
|
1846
|
+
}
|
|
1847
|
+
//#endregion
|
|
1848
|
+
//#region src/client/editor-workbench-host.tsx
|
|
1849
|
+
/** Page-stable owner for the plugin fallback editor workbench. */
|
|
1850
|
+
function requestIdentity(request) {
|
|
1851
|
+
return editorWorkbenchEditorKey(request.grant, request.sessionId);
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Small external store that is deliberately not owned by a Tool card. The
|
|
1855
|
+
* replacement gate lets the mounted host retain a dirty editor when another
|
|
1856
|
+
* historical card asks to open a different document.
|
|
1857
|
+
*/
|
|
1858
|
+
function createEditorWorkbenchStore(canReplace = () => true, onRepeat = () => {}) {
|
|
1859
|
+
let current;
|
|
1860
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1861
|
+
const emit = () => {
|
|
1862
|
+
for (const listener of listeners) listener();
|
|
1863
|
+
};
|
|
1864
|
+
return {
|
|
1865
|
+
getSnapshot: () => current,
|
|
1866
|
+
subscribe(listener) {
|
|
1867
|
+
listeners.add(listener);
|
|
1868
|
+
return () => {
|
|
1869
|
+
listeners.delete(listener);
|
|
1870
|
+
};
|
|
1871
|
+
},
|
|
1872
|
+
open(request) {
|
|
1873
|
+
if (current !== void 0 && requestIdentity(current) === requestIdentity(request)) {
|
|
1874
|
+
onRepeat();
|
|
1875
|
+
return true;
|
|
1876
|
+
}
|
|
1877
|
+
if (current !== void 0 && !canReplace(current)) return false;
|
|
1878
|
+
current = request;
|
|
1879
|
+
emit();
|
|
1880
|
+
return true;
|
|
1881
|
+
},
|
|
1882
|
+
close() {
|
|
1883
|
+
if (current === void 0) return;
|
|
1884
|
+
current = void 0;
|
|
1885
|
+
emit();
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
/**
|
|
1890
|
+
* Preserve dirty state without inventing a save the user did not request.
|
|
1891
|
+
* An already-running save may finish; an idle dirty editor is recovery-only.
|
|
1892
|
+
*/
|
|
1893
|
+
async function preserveEditorBeforeWorkbenchDispose(state, controller) {
|
|
1894
|
+
const unrecovered = () => {
|
|
1895
|
+
controller.retainServerSessionOnUnmount();
|
|
1896
|
+
return "unrecovered";
|
|
1897
|
+
};
|
|
1898
|
+
if (state.phase === "saving") {
|
|
1899
|
+
if (await controller.awaitExistingSave().catch(() => false)) return "saved";
|
|
1900
|
+
return await controller.captureRecovery().catch(() => false) ? "recovered" : unrecovered();
|
|
1901
|
+
}
|
|
1902
|
+
if (!state.dirty) return "clean";
|
|
1903
|
+
return await controller.captureRecovery().catch(() => false) ? "recovered" : unrecovered();
|
|
1904
|
+
}
|
|
1905
|
+
function EditorWorkbenchHostView({ store, subscribeTheme, getColorScheme, subscribeLocale, getLocale, ownerId, onLifecycleState, onLifecycleController, close }) {
|
|
1906
|
+
const request = (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
1907
|
+
const colorScheme = (0, react.useSyncExternalStore)(subscribeTheme, getColorScheme, getColorScheme);
|
|
1908
|
+
const locale = (0, react.useSyncExternalStore)(subscribeLocale, getLocale, getLocale);
|
|
1909
|
+
if (request === void 0) return null;
|
|
1910
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ManagedOpenPencilEditorModal, {
|
|
1911
|
+
grant: request.grant,
|
|
1912
|
+
colorScheme,
|
|
1913
|
+
locale: editorLocaleFromDsh(locale),
|
|
1914
|
+
sessionId: request.sessionId,
|
|
1915
|
+
ownerId,
|
|
1916
|
+
onLifecycleState,
|
|
1917
|
+
onLifecycleController,
|
|
1918
|
+
onClose: close,
|
|
1919
|
+
allowEditorTakeover: request.automatic !== true
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
let nextHostId = 0;
|
|
1923
|
+
/** Mount one imperative React root for the whole plugin fiber. */
|
|
1924
|
+
function mountEditorWorkbenchHost(options) {
|
|
1925
|
+
const ownerDocument = options.document ?? document;
|
|
1926
|
+
const hostId = `dsh-openpencil-workbench-${++nextHostId}`;
|
|
1927
|
+
const container = ownerDocument.createElement("div");
|
|
1928
|
+
container.dataset.openpencilWorkbenchHost = hostId;
|
|
1929
|
+
ownerDocument.body.append(container);
|
|
1930
|
+
let root = (0, react_dom_client.createRoot)(container);
|
|
1931
|
+
let destroyed = false;
|
|
1932
|
+
let disposing = false;
|
|
1933
|
+
let disposePromise;
|
|
1934
|
+
let openQueue = Promise.resolve();
|
|
1935
|
+
let lifecycle = INITIAL_EDITOR_LIFECYCLE_STATE;
|
|
1936
|
+
let lifecycleController;
|
|
1937
|
+
const focusSurface = () => {
|
|
1938
|
+
ownerDocument.querySelector(`[data-openpencil-editor-workbench-owner="${hostId}"] button, [data-openpencil-editor-workbench-owner="${hostId}"] [tabindex="0"]`)?.focus();
|
|
1939
|
+
};
|
|
1940
|
+
const canDiscard = () => {
|
|
1941
|
+
if (lifecycle.phase === "saving") return false;
|
|
1942
|
+
return !lifecycle.dirty || window.confirm(editorModalCopy(editorLocaleFromDsh(options.getLocale())).discard);
|
|
1943
|
+
};
|
|
1944
|
+
const store = createEditorWorkbenchStore(() => true, focusSurface);
|
|
1945
|
+
const close = () => {
|
|
1946
|
+
lifecycle = INITIAL_EDITOR_LIFECYCLE_STATE;
|
|
1947
|
+
lifecycleController = void 0;
|
|
1948
|
+
store.close();
|
|
1949
|
+
};
|
|
1950
|
+
const destroy = () => {
|
|
1951
|
+
if (destroyed) return;
|
|
1952
|
+
destroyed = true;
|
|
1953
|
+
root?.unmount();
|
|
1954
|
+
root = void 0;
|
|
1955
|
+
container.remove();
|
|
1956
|
+
};
|
|
1957
|
+
root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditorWorkbenchHostView, {
|
|
1958
|
+
store,
|
|
1959
|
+
subscribeTheme: options.subscribeTheme,
|
|
1960
|
+
getColorScheme: options.getColorScheme,
|
|
1961
|
+
subscribeLocale: options.subscribeLocale,
|
|
1962
|
+
getLocale: options.getLocale,
|
|
1963
|
+
ownerId: hostId,
|
|
1964
|
+
onLifecycleState: (next) => {
|
|
1965
|
+
lifecycle = next;
|
|
1966
|
+
},
|
|
1967
|
+
onLifecycleController: (next) => {
|
|
1968
|
+
lifecycleController = next;
|
|
1969
|
+
},
|
|
1970
|
+
close
|
|
1971
|
+
}));
|
|
1972
|
+
return {
|
|
1973
|
+
open(request) {
|
|
1974
|
+
if (destroyed || disposing) return Promise.resolve(false);
|
|
1975
|
+
const operation = openQueue.then(async () => {
|
|
1976
|
+
if (destroyed || disposing) return false;
|
|
1977
|
+
const previous = store.getSnapshot();
|
|
1978
|
+
if (previous !== void 0 && requestIdentity(previous) === requestIdentity(request)) {
|
|
1979
|
+
store.open(request);
|
|
1980
|
+
queueMicrotask(focusSurface);
|
|
1981
|
+
return true;
|
|
1982
|
+
}
|
|
1983
|
+
if (previous !== void 0 && !canDiscard()) return false;
|
|
1984
|
+
if (previous !== void 0 && lifecycleController !== void 0) {
|
|
1985
|
+
if (!await lifecycleController.requestClose() || destroyed || disposing) return false;
|
|
1986
|
+
}
|
|
1987
|
+
const accepted = store.open(request);
|
|
1988
|
+
if (accepted) {
|
|
1989
|
+
lifecycle = INITIAL_EDITOR_LIFECYCLE_STATE;
|
|
1990
|
+
lifecycleController = void 0;
|
|
1991
|
+
queueMicrotask(focusSurface);
|
|
1992
|
+
}
|
|
1993
|
+
return accepted;
|
|
1994
|
+
});
|
|
1995
|
+
openQueue = operation.then(() => {}, () => {});
|
|
1996
|
+
return operation;
|
|
1997
|
+
},
|
|
1998
|
+
openIfIdle(request) {
|
|
1999
|
+
if (destroyed || disposing) return Promise.resolve(false);
|
|
2000
|
+
const operation = openQueue.then(() => {
|
|
2001
|
+
if (destroyed || disposing || store.getSnapshot() !== void 0 || hasActiveEditor()) return false;
|
|
2002
|
+
if (editorWorkbenchUsesFullscreen(ownerDocument.defaultView?.innerWidth ?? window.innerWidth)) return false;
|
|
2003
|
+
const dshRoot = ownerDocument.getElementById("root");
|
|
2004
|
+
if (dshRoot === null) return false;
|
|
2005
|
+
if (dshRoot.dataset["openpencilWorkbenchDockOwner"] !== void 0) return false;
|
|
2006
|
+
const computedMarginRight = Number.parseFloat(ownerDocument.defaultView?.getComputedStyle(dshRoot).marginRight ?? "0");
|
|
2007
|
+
if (dshRoot.style.marginRight.trim() !== "" || Number.isFinite(computedMarginRight) && computedMarginRight > .5) return false;
|
|
2008
|
+
const accepted = store.open({
|
|
2009
|
+
...request,
|
|
2010
|
+
automatic: true
|
|
2011
|
+
});
|
|
2012
|
+
if (accepted) {
|
|
2013
|
+
lifecycle = INITIAL_EDITOR_LIFECYCLE_STATE;
|
|
2014
|
+
lifecycleController = void 0;
|
|
2015
|
+
}
|
|
2016
|
+
return accepted;
|
|
2017
|
+
});
|
|
2018
|
+
openQueue = operation.then(() => {}, () => {});
|
|
2019
|
+
return operation;
|
|
2020
|
+
},
|
|
2021
|
+
async dispose() {
|
|
2022
|
+
if (destroyed) return;
|
|
2023
|
+
if (disposePromise !== void 0) return disposePromise;
|
|
2024
|
+
disposing = true;
|
|
2025
|
+
disposePromise = (async () => {
|
|
2026
|
+
await openQueue;
|
|
2027
|
+
if ((lifecycle.dirty || lifecycle.phase === "saving") && lifecycleController !== void 0) await preserveEditorBeforeWorkbenchDispose(lifecycle, lifecycleController);
|
|
2028
|
+
close();
|
|
2029
|
+
destroy();
|
|
2030
|
+
})();
|
|
2031
|
+
return disposePromise;
|
|
2032
|
+
}
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
//#endregion
|
|
2036
|
+
//#region src/client/frame-gallery.tsx
|
|
2037
|
+
const FRAME_GALLERY_COPY = {
|
|
2038
|
+
en: {
|
|
2039
|
+
frame: "Frame",
|
|
2040
|
+
carousel: "carousel",
|
|
2041
|
+
gallery: "OpenPencil frames",
|
|
2042
|
+
toolbar: "Preview zoom and card size controls",
|
|
2043
|
+
zoomOut: "Zoom out preview",
|
|
2044
|
+
zoomOutTitle: "Zoom out by 25% (Ctrl/Cmd −)",
|
|
2045
|
+
zoomIn: "Zoom in preview",
|
|
2046
|
+
zoomInTitle: "Zoom in by 25% (Ctrl/Cmd +)",
|
|
2047
|
+
previewZoom: "Preview zoom",
|
|
2048
|
+
reset: "Reset",
|
|
2049
|
+
resetAria: "Reset preview zoom to 100%",
|
|
2050
|
+
resetTitle: "Reset zoom to 100% (Ctrl/Cmd 0)",
|
|
2051
|
+
fitFrame: "Fit frame",
|
|
2052
|
+
fitFrameAria: "Fit entire frame inside the current card",
|
|
2053
|
+
fitFrameTitle: "Fit the entire frame without changing the card size",
|
|
2054
|
+
fitContent: "Fit content",
|
|
2055
|
+
fitContentAria: "Fit card height to the entire frame",
|
|
2056
|
+
fitContentTitle: "Expand the card to show the entire frame",
|
|
2057
|
+
restoreCard: "Restore card",
|
|
2058
|
+
restoreCardAria: "Restore compact card height",
|
|
2059
|
+
previous: "Previous frame",
|
|
2060
|
+
next: "Next frame",
|
|
2061
|
+
failed: "This frame preview could not be loaded. Choose another frame or use the download action.",
|
|
2062
|
+
rendered: "Rendered OpenPencil frame",
|
|
2063
|
+
thumbnails: "Frame thumbnails",
|
|
2064
|
+
showFrame: "Show frame"
|
|
2065
|
+
},
|
|
2066
|
+
zh: {
|
|
2067
|
+
frame: "页面",
|
|
2068
|
+
carousel: "轮播",
|
|
2069
|
+
gallery: "OpenPencil 页面",
|
|
2070
|
+
toolbar: "预览缩放与卡片尺寸控制",
|
|
2071
|
+
zoomOut: "缩小预览",
|
|
2072
|
+
zoomOutTitle: "缩小 25%(Ctrl/Cmd −)",
|
|
2073
|
+
zoomIn: "放大预览",
|
|
2074
|
+
zoomInTitle: "放大 25%(Ctrl/Cmd +)",
|
|
2075
|
+
previewZoom: "预览缩放",
|
|
2076
|
+
reset: "重置",
|
|
2077
|
+
resetAria: "将预览缩放重置为 100%",
|
|
2078
|
+
resetTitle: "重置为 100%(Ctrl/Cmd 0)",
|
|
2079
|
+
fitFrame: "适应画面",
|
|
2080
|
+
fitFrameAria: "将整个页面缩放到当前卡片内",
|
|
2081
|
+
fitFrameTitle: "不改变卡片大小,完整显示当前页面",
|
|
2082
|
+
fitContent: "适应内容",
|
|
2083
|
+
fitContentAria: "让卡片高度适应完整页面",
|
|
2084
|
+
fitContentTitle: "展开卡片以显示完整页面",
|
|
2085
|
+
restoreCard: "还原卡片",
|
|
2086
|
+
restoreCardAria: "还原紧凑卡片高度",
|
|
2087
|
+
previous: "上一页",
|
|
2088
|
+
next: "下一页",
|
|
2089
|
+
failed: "当前页面预览加载失败,请选择其他页面或使用下载操作。",
|
|
2090
|
+
rendered: "OpenPencil 页面渲染图",
|
|
2091
|
+
thumbnails: "页面缩略图",
|
|
2092
|
+
showFrame: "显示页面"
|
|
2093
|
+
}
|
|
2094
|
+
};
|
|
2095
|
+
function frameGalleryCopy(locale) {
|
|
2096
|
+
return FRAME_GALLERY_COPY[locale];
|
|
2097
|
+
}
|
|
2098
|
+
function normalizeFrameIndex(index, length) {
|
|
2099
|
+
if (length <= 0) return 0;
|
|
2100
|
+
return Math.min(length - 1, Math.max(0, Math.trunc(index)));
|
|
2101
|
+
}
|
|
2102
|
+
function frameLabel(frame, index, locale = "en") {
|
|
2103
|
+
return frame.name ?? frame.id ?? `${frameGalleryCopy(locale).frame} ${index + 1}`;
|
|
2104
|
+
}
|
|
2105
|
+
/** Preview zoom limits are intentionally broad enough for detail inspection. */
|
|
2106
|
+
const GALLERY_ZOOM_MIN = .25;
|
|
2107
|
+
const GALLERY_ZOOM_MAX = 4;
|
|
2108
|
+
const GALLERY_ZOOM_STEP = .25;
|
|
2109
|
+
function clampGalleryZoom(zoom) {
|
|
2110
|
+
if (!Number.isFinite(zoom)) return 1;
|
|
2111
|
+
return Math.min(4, Math.max(GALLERY_ZOOM_MIN, zoom));
|
|
2112
|
+
}
|
|
2113
|
+
/** Move one predictable 25% stop in either direction. */
|
|
2114
|
+
function nextGalleryZoom(zoom, direction) {
|
|
2115
|
+
if (Number.isFinite(zoom) && zoom < .25) return GALLERY_ZOOM_MIN;
|
|
2116
|
+
if (Number.isFinite(zoom) && zoom > 4) return 4;
|
|
2117
|
+
const stops = clampGalleryZoom(zoom) / GALLERY_ZOOM_STEP;
|
|
2118
|
+
return clampGalleryZoom((direction > 0 ? Math.floor(stops + 1e-8) + 1 : Math.ceil(stops - 1e-8) - 1) * GALLERY_ZOOM_STEP);
|
|
2119
|
+
}
|
|
2120
|
+
function galleryZoomPercent(zoom) {
|
|
2121
|
+
const percent = (Number.isFinite(zoom) && zoom > 0 ? zoom : 1) * 100;
|
|
2122
|
+
return `${percent < 1 ? Math.max(.1, Math.round(percent * 10) / 10) : Math.round(percent)}%`;
|
|
2123
|
+
}
|
|
2124
|
+
/** Contain the entire frame inside the current viewport without resizing the card. */
|
|
2125
|
+
function calculateGalleryFitViewZoom(viewportWidth, viewportHeight, contentWidth, contentHeight) {
|
|
2126
|
+
if (!Number.isFinite(viewportWidth) || viewportWidth <= 0 || !Number.isFinite(viewportHeight) || viewportHeight <= 0 || !Number.isFinite(contentWidth) || contentWidth <= 0 || !Number.isFinite(contentHeight) || contentHeight <= 0) return 1;
|
|
2127
|
+
return Math.min(4, viewportWidth / contentWidth, viewportHeight / contentHeight);
|
|
2128
|
+
}
|
|
2129
|
+
/** Resolve a keyboard zoom command without reversing direction at either limit. */
|
|
2130
|
+
function galleryZoomCommandTarget(zoom, command) {
|
|
2131
|
+
if (command === "reset") return 1;
|
|
2132
|
+
if (command === "in") {
|
|
2133
|
+
if (zoom >= 3.99999999) return void 0;
|
|
2134
|
+
return nextGalleryZoom(zoom, 1);
|
|
2135
|
+
}
|
|
2136
|
+
if (zoom <= .25000001) return void 0;
|
|
2137
|
+
return nextGalleryZoom(zoom, -1);
|
|
2138
|
+
}
|
|
2139
|
+
function galleryZoomShortcut(key, modifier) {
|
|
2140
|
+
if (!modifier) return void 0;
|
|
2141
|
+
if (key === "+" || key === "=") return "in";
|
|
2142
|
+
if (key === "-" || key === "_") return "out";
|
|
2143
|
+
if (key === "0") return "reset";
|
|
2144
|
+
}
|
|
2145
|
+
const GALLERY_COMPACT_MAX_HEIGHT = 560;
|
|
2146
|
+
/** Shared geometry keeps labels and glyphs on one visual center line. */
|
|
2147
|
+
const GALLERY_TOOLBAR_CONTROL_HEIGHT = 28;
|
|
2148
|
+
const GALLERY_TOOLBAR_CONTROL_LAYOUT = Object.freeze({
|
|
2149
|
+
display: "inline-flex",
|
|
2150
|
+
alignItems: "center",
|
|
2151
|
+
justifyContent: "center",
|
|
2152
|
+
boxSizing: "border-box",
|
|
2153
|
+
height: 28,
|
|
2154
|
+
lineHeight: 1,
|
|
2155
|
+
verticalAlign: "middle"
|
|
2156
|
+
});
|
|
2157
|
+
/** Optical correction for CJK labels and +/- glyphs inside the centered control box. */
|
|
2158
|
+
const GALLERY_TOOLBAR_CONTROL_CONTENT_LAYOUT = Object.freeze({
|
|
2159
|
+
display: "inline-block",
|
|
2160
|
+
lineHeight: 1,
|
|
2161
|
+
transform: "translateY(-1px)",
|
|
2162
|
+
pointerEvents: "none"
|
|
2163
|
+
});
|
|
2164
|
+
function galleryViewportMaxHeight(fitContent) {
|
|
2165
|
+
return fitContent ? void 0 : 560;
|
|
2166
|
+
}
|
|
2167
|
+
const styles$2 = {
|
|
2168
|
+
gallery: {
|
|
2169
|
+
display: "flex",
|
|
2170
|
+
flexDirection: "column",
|
|
2171
|
+
gap: 8
|
|
2172
|
+
},
|
|
2173
|
+
mainShell: {
|
|
2174
|
+
display: "flex",
|
|
2175
|
+
flexDirection: "column",
|
|
2176
|
+
gap: 8,
|
|
2177
|
+
minWidth: 0
|
|
2178
|
+
},
|
|
2179
|
+
previewShell: {
|
|
2180
|
+
position: "relative",
|
|
2181
|
+
minWidth: 0
|
|
2182
|
+
},
|
|
2183
|
+
mainViewport: {
|
|
2184
|
+
maxHeight: 560,
|
|
2185
|
+
overflow: "auto",
|
|
2186
|
+
overscrollBehavior: "contain",
|
|
2187
|
+
borderRadius: 6,
|
|
2188
|
+
border: "1px solid rgba(128,128,128,0.25)",
|
|
2189
|
+
background: "rgba(128,128,128,0.06)"
|
|
2190
|
+
},
|
|
2191
|
+
mainImage: {
|
|
2192
|
+
display: "block",
|
|
2193
|
+
maxWidth: "none",
|
|
2194
|
+
height: "auto",
|
|
2195
|
+
margin: "0 auto"
|
|
2196
|
+
},
|
|
2197
|
+
zoomToolbar: {
|
|
2198
|
+
display: "flex",
|
|
2199
|
+
alignItems: "center",
|
|
2200
|
+
flexWrap: "wrap",
|
|
2201
|
+
gap: 4,
|
|
2202
|
+
marginLeft: "auto",
|
|
2203
|
+
minWidth: 0
|
|
2204
|
+
},
|
|
2205
|
+
zoomButton: {
|
|
2206
|
+
...GALLERY_TOOLBAR_CONTROL_LAYOUT,
|
|
2207
|
+
minWidth: 28,
|
|
2208
|
+
padding: "0 8px",
|
|
2209
|
+
borderRadius: 5,
|
|
2210
|
+
border: "1px solid var(--ui-border, rgba(128,128,128,0.35))",
|
|
2211
|
+
color: "var(--ui-text, inherit)",
|
|
2212
|
+
background: "var(--ui-card-bg, rgba(128,128,128,0.08))",
|
|
2213
|
+
cursor: "pointer",
|
|
2214
|
+
fontFamily: "inherit",
|
|
2215
|
+
fontWeight: "inherit",
|
|
2216
|
+
fontStyle: "inherit",
|
|
2217
|
+
fontSize: 12,
|
|
2218
|
+
lineHeight: 1,
|
|
2219
|
+
whiteSpace: "nowrap"
|
|
2220
|
+
},
|
|
2221
|
+
controlContent: GALLERY_TOOLBAR_CONTROL_CONTENT_LAYOUT,
|
|
2222
|
+
zoomPercent: {
|
|
2223
|
+
...GALLERY_TOOLBAR_CONTROL_LAYOUT,
|
|
2224
|
+
minWidth: 42,
|
|
2225
|
+
padding: "0 3px",
|
|
2226
|
+
textAlign: "center",
|
|
2227
|
+
fontSize: 11,
|
|
2228
|
+
fontVariantNumeric: "tabular-nums",
|
|
2229
|
+
lineHeight: 1
|
|
2230
|
+
},
|
|
2231
|
+
counter: {
|
|
2232
|
+
position: "absolute",
|
|
2233
|
+
right: 9,
|
|
2234
|
+
top: 9,
|
|
2235
|
+
padding: "3px 7px",
|
|
2236
|
+
borderRadius: 99,
|
|
2237
|
+
color: "#fff",
|
|
2238
|
+
background: "rgba(15,15,18,0.72)",
|
|
2239
|
+
fontSize: 11,
|
|
2240
|
+
lineHeight: 1.3,
|
|
2241
|
+
pointerEvents: "none",
|
|
2242
|
+
backdropFilter: "blur(4px)"
|
|
2243
|
+
},
|
|
2244
|
+
controls: {
|
|
2245
|
+
display: "flex",
|
|
2246
|
+
alignItems: "center",
|
|
2247
|
+
flexWrap: "wrap",
|
|
2248
|
+
minWidth: 0,
|
|
2249
|
+
gap: 7,
|
|
2250
|
+
fontSize: 12,
|
|
2251
|
+
color: "var(--ui-text-muted, #888)"
|
|
2252
|
+
},
|
|
2253
|
+
currentName: {
|
|
2254
|
+
flex: "1 1 120px",
|
|
2255
|
+
minWidth: 0,
|
|
2256
|
+
overflow: "hidden",
|
|
2257
|
+
textOverflow: "ellipsis",
|
|
2258
|
+
whiteSpace: "nowrap"
|
|
2259
|
+
},
|
|
2260
|
+
arrow: {
|
|
2261
|
+
...GALLERY_TOOLBAR_CONTROL_LAYOUT,
|
|
2262
|
+
width: 28,
|
|
2263
|
+
minWidth: 28,
|
|
2264
|
+
padding: 0,
|
|
2265
|
+
borderRadius: 99,
|
|
2266
|
+
border: "1px solid var(--ui-border, rgba(128,128,128,0.35))",
|
|
2267
|
+
color: "var(--ui-text, inherit)",
|
|
2268
|
+
background: "var(--ui-card-bg, rgba(128,128,128,0.08))",
|
|
2269
|
+
cursor: "pointer",
|
|
2270
|
+
fontFamily: "inherit",
|
|
2271
|
+
fontWeight: "inherit",
|
|
2272
|
+
fontStyle: "inherit",
|
|
2273
|
+
fontSize: 20,
|
|
2274
|
+
lineHeight: 1
|
|
2275
|
+
},
|
|
2276
|
+
strip: {
|
|
2277
|
+
display: "flex",
|
|
2278
|
+
gap: 8,
|
|
2279
|
+
minWidth: 0,
|
|
2280
|
+
overflowX: "auto",
|
|
2281
|
+
overflowY: "hidden",
|
|
2282
|
+
padding: "1px 1px 7px",
|
|
2283
|
+
scrollSnapType: "x proximity",
|
|
2284
|
+
scrollbarWidth: "thin",
|
|
2285
|
+
overscrollBehaviorX: "contain"
|
|
2286
|
+
},
|
|
2287
|
+
thumbnail: {
|
|
2288
|
+
flex: "0 0 112px",
|
|
2289
|
+
width: 112,
|
|
2290
|
+
height: 84,
|
|
2291
|
+
padding: 3,
|
|
2292
|
+
overflow: "hidden",
|
|
2293
|
+
scrollSnapAlign: "start",
|
|
2294
|
+
borderRadius: 7,
|
|
2295
|
+
border: "1px solid rgba(128,128,128,0.3)",
|
|
2296
|
+
background: "rgba(128,128,128,0.06)",
|
|
2297
|
+
cursor: "pointer"
|
|
2298
|
+
},
|
|
2299
|
+
thumbnailSelected: {
|
|
2300
|
+
border: "2px solid var(--ui-accent, #0ea5e9)",
|
|
2301
|
+
padding: 2,
|
|
2302
|
+
boxShadow: "0 0 0 1px color-mix(in srgb, var(--ui-accent, #0ea5e9) 28%, transparent)"
|
|
2303
|
+
},
|
|
2304
|
+
thumbnailImage: {
|
|
2305
|
+
display: "block",
|
|
2306
|
+
width: "100%",
|
|
2307
|
+
height: "100%",
|
|
2308
|
+
objectFit: "contain",
|
|
2309
|
+
borderRadius: 4
|
|
2310
|
+
},
|
|
2311
|
+
failure: {
|
|
2312
|
+
minHeight: 128,
|
|
2313
|
+
display: "flex",
|
|
2314
|
+
alignItems: "center",
|
|
2315
|
+
justifyContent: "center",
|
|
2316
|
+
padding: 18,
|
|
2317
|
+
color: "var(--ui-text-muted, #888)",
|
|
2318
|
+
fontSize: 12,
|
|
2319
|
+
textAlign: "center"
|
|
2320
|
+
}
|
|
2321
|
+
};
|
|
2322
|
+
/** Large selected preview plus a horizontally-scrollable thumbnail rail. */
|
|
2323
|
+
function FrameGallery({ frames, selectedIndex, onSelect, locale }) {
|
|
2324
|
+
const stripRef = (0, react.useRef)(null);
|
|
2325
|
+
const viewportRef = (0, react.useRef)(null);
|
|
2326
|
+
const thumbnailRefs = (0, react.useRef)([]);
|
|
2327
|
+
const [failedUrls, setFailedUrls] = (0, react.useState)(() => /* @__PURE__ */ new Set());
|
|
2328
|
+
const [manualZoom, setManualZoom] = (0, react.useState)(1);
|
|
2329
|
+
const [zoomMode, setZoomMode] = (0, react.useState)("manual");
|
|
2330
|
+
const [fitContent, setFitContent] = (0, react.useState)(false);
|
|
2331
|
+
const [viewportSize, setViewportSize] = (0, react.useState)({
|
|
2332
|
+
width: 0,
|
|
2333
|
+
height: 0
|
|
2334
|
+
});
|
|
2335
|
+
const [loadedDimensions, setLoadedDimensions] = (0, react.useState)({});
|
|
2336
|
+
const currentIndex = normalizeFrameIndex(selectedIndex, frames.length);
|
|
2337
|
+
const current = frames[currentIndex];
|
|
2338
|
+
(0, react.useEffect)(() => {
|
|
2339
|
+
setFailedUrls(/* @__PURE__ */ new Set());
|
|
2340
|
+
}, [frames.map((frame) => frame.previewUrl).join("\n")]);
|
|
2341
|
+
(0, react.useEffect)(() => {
|
|
2342
|
+
const viewport = viewportRef.current;
|
|
2343
|
+
if (viewport === null) return;
|
|
2344
|
+
const measure = () => {
|
|
2345
|
+
const next = {
|
|
2346
|
+
width: viewport.clientWidth,
|
|
2347
|
+
height: viewport.clientHeight
|
|
2348
|
+
};
|
|
2349
|
+
setViewportSize((previous) => previous.width === next.width && previous.height === next.height ? previous : next);
|
|
2350
|
+
};
|
|
2351
|
+
measure();
|
|
2352
|
+
if (typeof ResizeObserver === "undefined") {
|
|
2353
|
+
window.addEventListener("resize", measure);
|
|
2354
|
+
return () => {
|
|
2355
|
+
window.removeEventListener("resize", measure);
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
const observer = new ResizeObserver(measure);
|
|
2359
|
+
observer.observe(viewport);
|
|
2360
|
+
return () => {
|
|
2361
|
+
observer.disconnect();
|
|
2362
|
+
};
|
|
2363
|
+
}, []);
|
|
2364
|
+
const select = (0, react.useCallback)((index) => {
|
|
2365
|
+
const next = normalizeFrameIndex(index, frames.length);
|
|
2366
|
+
onSelect(next);
|
|
2367
|
+
requestAnimationFrame(() => {
|
|
2368
|
+
const strip = stripRef.current;
|
|
2369
|
+
const item = thumbnailRefs.current[next];
|
|
2370
|
+
if (strip === null || item === null || item === void 0) return;
|
|
2371
|
+
const left = item.offsetLeft - (strip.clientWidth - item.offsetWidth) / 2;
|
|
2372
|
+
strip.scrollTo({
|
|
2373
|
+
left: Math.max(0, left),
|
|
2374
|
+
behavior: "smooth"
|
|
2375
|
+
});
|
|
2376
|
+
});
|
|
2377
|
+
}, [frames.length, onSelect]);
|
|
2378
|
+
(0, react.useEffect)(() => {
|
|
2379
|
+
viewportRef.current?.scrollTo({
|
|
2380
|
+
left: 0,
|
|
2381
|
+
top: 0
|
|
2382
|
+
});
|
|
2383
|
+
}, [current?.previewUrl]);
|
|
2384
|
+
if (current === void 0) return null;
|
|
2385
|
+
const copy = frameGalleryCopy(locale);
|
|
2386
|
+
const failed = failedUrls.has(current.previewUrl);
|
|
2387
|
+
const name = frameLabel(current, currentIndex, locale);
|
|
2388
|
+
const loaded = loadedDimensions[current.previewUrl];
|
|
2389
|
+
const contentWidth = current.width ?? loaded?.width ?? 0;
|
|
2390
|
+
const contentHeight = current.height ?? loaded?.height ?? 0;
|
|
2391
|
+
const fitViewZoom = calculateGalleryFitViewZoom(viewportSize.width, zoomMode === "fit-view" ? 560 : viewportSize.height, contentWidth, contentHeight);
|
|
2392
|
+
const zoom = zoomMode === "fit-view" ? fitViewZoom : manualZoom;
|
|
2393
|
+
const zoomLabel = galleryZoomPercent(zoom);
|
|
2394
|
+
const canZoomOut = zoom > .25000001;
|
|
2395
|
+
const canZoomIn = zoom < 3.99999999;
|
|
2396
|
+
const setZoom = (nextZoom) => {
|
|
2397
|
+
setManualZoom(clampGalleryZoom(nextZoom));
|
|
2398
|
+
setZoomMode("manual");
|
|
2399
|
+
};
|
|
2400
|
+
const resetZoom = () => {
|
|
2401
|
+
setZoom(1);
|
|
2402
|
+
viewportRef.current?.scrollTo({
|
|
2403
|
+
left: 0,
|
|
2404
|
+
top: 0
|
|
2405
|
+
});
|
|
2406
|
+
};
|
|
2407
|
+
const onKeyDown = (event) => {
|
|
2408
|
+
const command = galleryZoomShortcut(event.key, event.metaKey || event.ctrlKey);
|
|
2409
|
+
if (command !== void 0) {
|
|
2410
|
+
event.preventDefault();
|
|
2411
|
+
if (command === "reset") resetZoom();
|
|
2412
|
+
else {
|
|
2413
|
+
const target = galleryZoomCommandTarget(zoom, command);
|
|
2414
|
+
if (target !== void 0) setZoom(target);
|
|
2415
|
+
}
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2418
|
+
if (event.key === "ArrowLeft" && currentIndex > 0) {
|
|
2419
|
+
event.preventDefault();
|
|
2420
|
+
select(currentIndex - 1);
|
|
2421
|
+
} else if (event.key === "ArrowRight" && currentIndex < frames.length - 1) {
|
|
2422
|
+
event.preventDefault();
|
|
2423
|
+
select(currentIndex + 1);
|
|
2424
|
+
}
|
|
2425
|
+
};
|
|
2426
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2427
|
+
style: styles$2.gallery,
|
|
2428
|
+
role: "region",
|
|
2429
|
+
"aria-roledescription": copy.carousel,
|
|
2430
|
+
"aria-label": `${copy.gallery}: ${frames.length}`,
|
|
2431
|
+
"data-openpencil-frame-gallery": "true",
|
|
2432
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2433
|
+
style: styles$2.mainShell,
|
|
2434
|
+
tabIndex: 0,
|
|
2435
|
+
onKeyDown,
|
|
2436
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2437
|
+
style: styles$2.controls,
|
|
2438
|
+
children: [
|
|
2439
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2440
|
+
style: styles$2.currentName,
|
|
2441
|
+
title: name,
|
|
2442
|
+
children: [frames.length > 1 ? `${currentIndex + 1} / ${frames.length} · ` : "", name]
|
|
2443
|
+
}),
|
|
2444
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2445
|
+
style: styles$2.zoomToolbar,
|
|
2446
|
+
role: "toolbar",
|
|
2447
|
+
"aria-label": copy.toolbar,
|
|
2448
|
+
"data-openpencil-zoom-toolbar": "true",
|
|
2449
|
+
children: [
|
|
2450
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2451
|
+
type: "button",
|
|
2452
|
+
style: {
|
|
2453
|
+
...styles$2.zoomButton,
|
|
2454
|
+
opacity: canZoomOut ? 1 : .42
|
|
2455
|
+
},
|
|
2456
|
+
disabled: !canZoomOut,
|
|
2457
|
+
"aria-label": copy.zoomOut,
|
|
2458
|
+
title: copy.zoomOutTitle,
|
|
2459
|
+
onClick: () => {
|
|
2460
|
+
setZoom(nextGalleryZoom(zoom, -1));
|
|
2461
|
+
},
|
|
2462
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2463
|
+
style: styles$2.controlContent,
|
|
2464
|
+
children: "−"
|
|
2465
|
+
})
|
|
2466
|
+
}),
|
|
2467
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("output", {
|
|
2468
|
+
style: styles$2.zoomPercent,
|
|
2469
|
+
"aria-label": `${copy.previewZoom} ${zoomLabel}`,
|
|
2470
|
+
"aria-live": "polite",
|
|
2471
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2472
|
+
style: styles$2.controlContent,
|
|
2473
|
+
children: zoomLabel
|
|
2474
|
+
})
|
|
2475
|
+
}),
|
|
2476
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2477
|
+
type: "button",
|
|
2478
|
+
style: {
|
|
2479
|
+
...styles$2.zoomButton,
|
|
2480
|
+
opacity: canZoomIn ? 1 : .42
|
|
2481
|
+
},
|
|
2482
|
+
disabled: !canZoomIn,
|
|
2483
|
+
"aria-label": copy.zoomIn,
|
|
2484
|
+
title: copy.zoomInTitle,
|
|
2485
|
+
onClick: () => {
|
|
2486
|
+
setZoom(nextGalleryZoom(zoom, 1));
|
|
2487
|
+
},
|
|
2488
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2489
|
+
style: styles$2.controlContent,
|
|
2490
|
+
children: "+"
|
|
2491
|
+
})
|
|
2492
|
+
}),
|
|
2493
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2494
|
+
type: "button",
|
|
2495
|
+
style: {
|
|
2496
|
+
...styles$2.zoomButton,
|
|
2497
|
+
opacity: zoomMode === "manual" && manualZoom === 1 ? .42 : 1
|
|
2498
|
+
},
|
|
2499
|
+
disabled: zoomMode === "manual" && manualZoom === 1,
|
|
2500
|
+
"aria-label": copy.resetAria,
|
|
2501
|
+
title: copy.resetTitle,
|
|
2502
|
+
onClick: resetZoom,
|
|
2503
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2504
|
+
style: styles$2.controlContent,
|
|
2505
|
+
children: copy.reset
|
|
2506
|
+
})
|
|
2507
|
+
}),
|
|
2508
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2509
|
+
type: "button",
|
|
2510
|
+
style: {
|
|
2511
|
+
...styles$2.zoomButton,
|
|
2512
|
+
background: zoomMode === "fit-view" ? "color-mix(in srgb, var(--ui-accent, #0ea5e9) 18%, transparent)" : styles$2.zoomButton.background
|
|
2513
|
+
},
|
|
2514
|
+
"aria-label": copy.fitFrameAria,
|
|
2515
|
+
"aria-pressed": zoomMode === "fit-view",
|
|
2516
|
+
title: copy.fitFrameTitle,
|
|
2517
|
+
onClick: () => {
|
|
2518
|
+
const viewport = viewportRef.current;
|
|
2519
|
+
if (viewport !== null) setViewportSize({
|
|
2520
|
+
width: viewport.clientWidth,
|
|
2521
|
+
height: 560
|
|
2522
|
+
});
|
|
2523
|
+
setFitContent(false);
|
|
2524
|
+
setZoomMode("fit-view");
|
|
2525
|
+
viewport?.scrollTo({
|
|
2526
|
+
left: 0,
|
|
2527
|
+
top: 0
|
|
2528
|
+
});
|
|
2529
|
+
},
|
|
2530
|
+
"data-openpencil-fit-view": "true",
|
|
2531
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2532
|
+
style: styles$2.controlContent,
|
|
2533
|
+
children: copy.fitFrame
|
|
2534
|
+
})
|
|
2535
|
+
}),
|
|
2536
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2537
|
+
type: "button",
|
|
2538
|
+
style: {
|
|
2539
|
+
...styles$2.zoomButton,
|
|
2540
|
+
background: fitContent ? "color-mix(in srgb, var(--ui-accent, #0ea5e9) 18%, transparent)" : styles$2.zoomButton.background
|
|
2541
|
+
},
|
|
2542
|
+
"aria-label": fitContent ? copy.restoreCardAria : copy.fitContentAria,
|
|
2543
|
+
"aria-pressed": fitContent,
|
|
2544
|
+
title: fitContent ? locale === "zh" ? `${copy.restoreCardAria}(560px)` : `${copy.restoreCardAria} (560px)` : copy.fitContentTitle,
|
|
2545
|
+
onClick: () => {
|
|
2546
|
+
setZoomMode("manual");
|
|
2547
|
+
setFitContent((previous) => !previous);
|
|
2548
|
+
viewportRef.current?.scrollTo({
|
|
2549
|
+
left: 0,
|
|
2550
|
+
top: 0
|
|
2551
|
+
});
|
|
2552
|
+
},
|
|
2553
|
+
"data-openpencil-card-height-toggle": "true",
|
|
2554
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2555
|
+
style: styles$2.controlContent,
|
|
2556
|
+
children: fitContent ? copy.restoreCard : copy.fitContent
|
|
2557
|
+
})
|
|
2558
|
+
})
|
|
2559
|
+
]
|
|
2560
|
+
}),
|
|
2561
|
+
frames.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2562
|
+
type: "button",
|
|
2563
|
+
style: {
|
|
2564
|
+
...styles$2.arrow,
|
|
2565
|
+
opacity: currentIndex === 0 ? .42 : 1
|
|
2566
|
+
},
|
|
2567
|
+
disabled: currentIndex === 0,
|
|
2568
|
+
"aria-label": copy.previous,
|
|
2569
|
+
title: copy.previous,
|
|
2570
|
+
onClick: () => {
|
|
2571
|
+
select(currentIndex - 1);
|
|
2572
|
+
},
|
|
2573
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2574
|
+
style: styles$2.controlContent,
|
|
2575
|
+
children: "‹"
|
|
2576
|
+
})
|
|
2577
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2578
|
+
type: "button",
|
|
2579
|
+
style: {
|
|
2580
|
+
...styles$2.arrow,
|
|
2581
|
+
opacity: currentIndex === frames.length - 1 ? .42 : 1
|
|
2582
|
+
},
|
|
2583
|
+
disabled: currentIndex === frames.length - 1,
|
|
2584
|
+
"aria-label": copy.next,
|
|
2585
|
+
title: copy.next,
|
|
2586
|
+
onClick: () => {
|
|
2587
|
+
select(currentIndex + 1);
|
|
2588
|
+
},
|
|
2589
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2590
|
+
style: styles$2.controlContent,
|
|
2591
|
+
children: "›"
|
|
2592
|
+
})
|
|
2593
|
+
})] }) : null
|
|
2594
|
+
]
|
|
2595
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2596
|
+
style: styles$2.previewShell,
|
|
2597
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2598
|
+
ref: viewportRef,
|
|
2599
|
+
style: {
|
|
2600
|
+
...styles$2.mainViewport,
|
|
2601
|
+
display: zoomMode === "fit-view" ? "flex" : void 0,
|
|
2602
|
+
alignItems: zoomMode === "fit-view" ? "center" : void 0,
|
|
2603
|
+
justifyContent: zoomMode === "fit-view" ? "center" : void 0,
|
|
2604
|
+
height: zoomMode === "fit-view" ? 560 : void 0,
|
|
2605
|
+
maxHeight: galleryViewportMaxHeight(fitContent),
|
|
2606
|
+
overflow: zoomMode === "fit-view" ? "hidden" : styles$2.mainViewport.overflow
|
|
2607
|
+
},
|
|
2608
|
+
"data-openpencil-image-viewport": "true",
|
|
2609
|
+
"data-card-height-mode": fitContent ? "content" : "compact",
|
|
2610
|
+
"data-preview-zoom-mode": zoomMode,
|
|
2611
|
+
children: failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2612
|
+
style: styles$2.failure,
|
|
2613
|
+
role: "status",
|
|
2614
|
+
children: copy.failed
|
|
2615
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
2616
|
+
style: {
|
|
2617
|
+
...styles$2.mainImage,
|
|
2618
|
+
width: contentWidth > 0 ? contentWidth * zoom : "auto"
|
|
2619
|
+
},
|
|
2620
|
+
src: current.previewUrl,
|
|
2621
|
+
alt: `${copy.rendered}: ${name}`,
|
|
2622
|
+
loading: "lazy",
|
|
2623
|
+
"data-openpencil-preview-zoom": zoomLabel,
|
|
2624
|
+
onLoad: (event) => {
|
|
2625
|
+
if (current.width !== void 0 && current.height !== void 0) return;
|
|
2626
|
+
const image = event.currentTarget;
|
|
2627
|
+
if (image.naturalWidth <= 0 || image.naturalHeight <= 0) return;
|
|
2628
|
+
setLoadedDimensions((previous) => ({
|
|
2629
|
+
...previous,
|
|
2630
|
+
[current.previewUrl]: {
|
|
2631
|
+
width: image.naturalWidth,
|
|
2632
|
+
height: image.naturalHeight
|
|
2633
|
+
}
|
|
2634
|
+
}));
|
|
2635
|
+
},
|
|
2636
|
+
onError: () => {
|
|
2637
|
+
setFailedUrls((previous) => /* @__PURE__ */ new Set([...previous, current.previewUrl]));
|
|
2638
|
+
}
|
|
2639
|
+
})
|
|
2640
|
+
}), frames.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2641
|
+
style: styles$2.counter,
|
|
2642
|
+
children: [
|
|
2643
|
+
currentIndex + 1,
|
|
2644
|
+
" / ",
|
|
2645
|
+
frames.length
|
|
2646
|
+
]
|
|
2647
|
+
}) : null]
|
|
2648
|
+
})]
|
|
2649
|
+
}), frames.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2650
|
+
ref: stripRef,
|
|
2651
|
+
style: styles$2.strip,
|
|
2652
|
+
"aria-label": copy.thumbnails,
|
|
2653
|
+
"data-openpencil-frame-strip": "true",
|
|
2654
|
+
children: frames.map((frame, index) => {
|
|
2655
|
+
const selected = index === currentIndex;
|
|
2656
|
+
const label = frameLabel(frame, index, locale);
|
|
2657
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2658
|
+
ref: (element) => {
|
|
2659
|
+
thumbnailRefs.current[index] = element;
|
|
2660
|
+
},
|
|
2661
|
+
type: "button",
|
|
2662
|
+
style: {
|
|
2663
|
+
...styles$2.thumbnail,
|
|
2664
|
+
...selected ? styles$2.thumbnailSelected : {}
|
|
2665
|
+
},
|
|
2666
|
+
"aria-label": `${copy.showFrame} ${index + 1}: ${label}`,
|
|
2667
|
+
"aria-current": selected ? "true" : void 0,
|
|
2668
|
+
title: label,
|
|
2669
|
+
onClick: () => {
|
|
2670
|
+
select(index);
|
|
2671
|
+
},
|
|
2672
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
2673
|
+
style: styles$2.thumbnailImage,
|
|
2674
|
+
src: frame.previewUrl,
|
|
2675
|
+
alt: "",
|
|
2676
|
+
loading: "lazy"
|
|
2677
|
+
})
|
|
2678
|
+
}, `${frame.previewUrl}:${index}`);
|
|
2679
|
+
})
|
|
2680
|
+
}) : null]
|
|
2681
|
+
});
|
|
2682
|
+
}
|
|
2683
|
+
//#endregion
|
|
2684
|
+
//#region src/client/selection-dock.tsx
|
|
2685
|
+
/** Live OpenPencil selection chip rendered above the DSH composer. */
|
|
2686
|
+
const OPENPENCIL_SELECTION_DOCK_LAYOUT = {
|
|
2687
|
+
boxSizing: "border-box",
|
|
2688
|
+
flex: "none",
|
|
2689
|
+
width: "calc(100% - var(--dsh-composer-side-clearance, 16px) - var(--dsh-composer-side-clearance, 16px) - var(--dsh-composer-dock-inset, 8px) - var(--dsh-composer-dock-inset, 8px) - var(--dsh-composer-dock-inset, 8px) - var(--dsh-composer-dock-inset, 8px))",
|
|
2690
|
+
maxWidth: "calc(var(--dsh-composer-card-max-width, 780px) - var(--dsh-composer-dock-inset, 8px) - var(--dsh-composer-dock-inset, 8px) - var(--dsh-composer-dock-inset, 8px) - var(--dsh-composer-dock-inset, 8px))",
|
|
2691
|
+
margin: "0 auto"
|
|
2692
|
+
};
|
|
2693
|
+
function hasOpenPencilSelection(selection) {
|
|
2694
|
+
return (selection?.selectedIds.length ?? 0) > 0;
|
|
2695
|
+
}
|
|
2696
|
+
function selectionNodeLabel(selection, locale) {
|
|
2697
|
+
if (selection.selectedIds.length === 0) return locale === "zh" ? "未选择画布节点" : "No canvas node selected";
|
|
2698
|
+
if (selection.selectedIds.length > 1) return locale === "zh" ? `已选择 ${selection.selectedIds.length} 个节点` : `${selection.selectedIds.length} nodes selected`;
|
|
2699
|
+
const node = selection.nodes[0];
|
|
2700
|
+
return node?.name ?? node?.type ?? selection.selectedIds[0];
|
|
2701
|
+
}
|
|
2702
|
+
function selectionNodeDetail(selection, locale) {
|
|
2703
|
+
if (selection.selectedIds.length === 0) return locale === "zh" ? "在右侧 OpenPencil 画布中选择节点" : "Select a node on the OpenPencil canvas";
|
|
2704
|
+
const node = selection.nodes[0];
|
|
2705
|
+
if (selection.selectedIds.length > 1 || node === void 0) return selection.selectedIds.slice(0, 3).join(" · ");
|
|
2706
|
+
const dimensions = node.width === void 0 || node.height === void 0 ? void 0 : `${Math.round(node.width)} × ${Math.round(node.height)}`;
|
|
2707
|
+
return [
|
|
2708
|
+
node.type,
|
|
2709
|
+
dimensions,
|
|
2710
|
+
node.id
|
|
2711
|
+
].filter(Boolean).join(" · ");
|
|
2712
|
+
}
|
|
2713
|
+
const styles$1 = {
|
|
2714
|
+
root: {
|
|
2715
|
+
...OPENPENCIL_SELECTION_DOCK_LAYOUT,
|
|
2716
|
+
display: "flex",
|
|
2717
|
+
alignItems: "center",
|
|
2718
|
+
gap: 10,
|
|
2719
|
+
minHeight: 42,
|
|
2720
|
+
padding: "7px 10px",
|
|
2721
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
2722
|
+
borderRadius: 9,
|
|
2723
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
2724
|
+
color: "var(--dsw-alias-label-primary)"
|
|
2725
|
+
},
|
|
2726
|
+
icon: {
|
|
2727
|
+
width: 28,
|
|
2728
|
+
height: 28,
|
|
2729
|
+
flex: "0 0 28px",
|
|
2730
|
+
display: "inline-flex",
|
|
2731
|
+
alignItems: "center",
|
|
2732
|
+
justifyContent: "center",
|
|
2733
|
+
borderRadius: 7,
|
|
2734
|
+
background: "var(--dsw-alias-brand-primary, #3b82f6)",
|
|
2735
|
+
color: "#fff",
|
|
2736
|
+
fontSize: 15
|
|
2737
|
+
},
|
|
2738
|
+
text: {
|
|
2739
|
+
minWidth: 0,
|
|
2740
|
+
display: "flex",
|
|
2741
|
+
flexDirection: "column",
|
|
2742
|
+
gap: 1
|
|
2743
|
+
},
|
|
2744
|
+
title: {
|
|
2745
|
+
fontSize: 12,
|
|
2746
|
+
fontWeight: 600,
|
|
2747
|
+
overflow: "hidden",
|
|
2748
|
+
textOverflow: "ellipsis",
|
|
2749
|
+
whiteSpace: "nowrap"
|
|
2750
|
+
},
|
|
2751
|
+
detail: {
|
|
2752
|
+
fontSize: 11,
|
|
2753
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
2754
|
+
overflow: "hidden",
|
|
2755
|
+
textOverflow: "ellipsis",
|
|
2756
|
+
whiteSpace: "nowrap"
|
|
2757
|
+
},
|
|
2758
|
+
target: {
|
|
2759
|
+
marginLeft: "auto",
|
|
2760
|
+
fontSize: 11,
|
|
2761
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
2762
|
+
whiteSpace: "nowrap"
|
|
2763
|
+
}
|
|
2764
|
+
};
|
|
2765
|
+
function OpenPencilSelectionDock({ sessionId, locale }) {
|
|
2766
|
+
const subscribe = (0, react.useCallback)((notify) => subscribeOpenPencilSelection(String(sessionId), notify), [sessionId]);
|
|
2767
|
+
const getSnapshot = (0, react.useCallback)(() => getOpenPencilSelectionSnapshot(String(sessionId)), [sessionId]);
|
|
2768
|
+
const selection = (0, react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot).selection;
|
|
2769
|
+
if (!hasOpenPencilSelection(selection)) return null;
|
|
2770
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2771
|
+
style: styles$1.root,
|
|
2772
|
+
"data-openpencil-selection-dock": "true",
|
|
2773
|
+
role: "status",
|
|
2774
|
+
children: [
|
|
2775
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2776
|
+
style: styles$1.icon,
|
|
2777
|
+
"aria-hidden": "true",
|
|
2778
|
+
children: "◇"
|
|
2779
|
+
}),
|
|
2780
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2781
|
+
style: styles$1.text,
|
|
2782
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2783
|
+
style: styles$1.title,
|
|
2784
|
+
children: selectionNodeLabel(selection, locale)
|
|
2785
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2786
|
+
style: styles$1.detail,
|
|
2787
|
+
children: selectionNodeDetail(selection, locale)
|
|
2788
|
+
})]
|
|
2789
|
+
}),
|
|
2790
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2791
|
+
style: styles$1.target,
|
|
2792
|
+
children: locale === "zh" ? "OpenPencil 修改目标" : "OpenPencil edit target"
|
|
2793
|
+
})
|
|
2794
|
+
]
|
|
2795
|
+
});
|
|
2796
|
+
}
|
|
2797
|
+
//#endregion
|
|
2798
|
+
//#region src/tool-names.ts
|
|
2799
|
+
/** Canonical model-facing OpenPencil tool names. */
|
|
2800
|
+
const OPENPENCIL_RENDER_TOOL_NAME = "openpencil_render";
|
|
2801
|
+
/**
|
|
2802
|
+
* Historical render name retained only by the browser presentation layer so
|
|
2803
|
+
* existing conversation cards and details panels remain replayable. The host
|
|
2804
|
+
* deliberately does not register this alias as a model-facing tool.
|
|
2805
|
+
*/
|
|
2806
|
+
const LEGACY_DESIGN_RENDER_TOOL_NAME = "design_render";
|
|
2807
|
+
//#endregion
|
|
2808
|
+
//#region src/client/presentation-hydration.ts
|
|
2809
|
+
/** Recover browser-only presentation metadata omitted from nested Tool results. */
|
|
2810
|
+
const PRESENTATION_HYDRATION_ENDPOINT = "/_dsh/dsh-openpencil/presentation";
|
|
2811
|
+
const PRESENTATION_HYDRATION_META_KEY = "$dshOpenPencil";
|
|
2812
|
+
const MAX_CANONICAL_RESULT_CHARS = 1048576;
|
|
2813
|
+
const MAX_SESSION_ID_CHARS = 256;
|
|
2814
|
+
const MAX_CALL_ID_CHARS = 512;
|
|
2815
|
+
const pendingByFetcher = /* @__PURE__ */ new WeakMap();
|
|
2816
|
+
function isRecord$1(value) {
|
|
2817
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2818
|
+
}
|
|
2819
|
+
function isRequest(value) {
|
|
2820
|
+
return value.sessionId.length > 0 && value.sessionId.length <= MAX_SESSION_ID_CHARS && value.callId.length > 0 && value.callId.length <= MAX_CALL_ID_CHARS && /^[a-f0-9]{64}$/iu.test(value.documentSha256);
|
|
2821
|
+
}
|
|
2822
|
+
function requestKey(value) {
|
|
2823
|
+
return `${value.sessionId}\n${value.callId}\n${value.documentSha256.toLowerCase()}`;
|
|
2824
|
+
}
|
|
2825
|
+
function pendingEnvelope(request, fetcher) {
|
|
2826
|
+
let pending = pendingByFetcher.get(fetcher);
|
|
2827
|
+
if (pending === void 0) {
|
|
2828
|
+
pending = /* @__PURE__ */ new Map();
|
|
2829
|
+
pendingByFetcher.set(fetcher, pending);
|
|
2830
|
+
}
|
|
2831
|
+
const key = requestKey(request);
|
|
2832
|
+
const existing = pending.get(key);
|
|
2833
|
+
if (existing !== void 0) return existing;
|
|
2834
|
+
const controller = new AbortController();
|
|
2835
|
+
const entry = {
|
|
2836
|
+
subscribers: 0,
|
|
2837
|
+
settled: false,
|
|
2838
|
+
cancelIfUnused: () => {},
|
|
2839
|
+
promise: Promise.resolve(void 0)
|
|
2840
|
+
};
|
|
2841
|
+
entry.cancelIfUnused = () => {
|
|
2842
|
+
if (entry.subscribers !== 0 || entry.settled) return;
|
|
2843
|
+
if (pending?.get(key) === entry) pending.delete(key);
|
|
2844
|
+
controller.abort();
|
|
2845
|
+
};
|
|
2846
|
+
entry.promise = (async () => {
|
|
2847
|
+
const response = await fetcher(PRESENTATION_HYDRATION_ENDPOINT, {
|
|
2848
|
+
method: "POST",
|
|
2849
|
+
credentials: "same-origin",
|
|
2850
|
+
headers: {
|
|
2851
|
+
accept: "application/json",
|
|
2852
|
+
"content-type": "application/json"
|
|
2853
|
+
},
|
|
2854
|
+
body: JSON.stringify(request),
|
|
2855
|
+
signal: controller.signal
|
|
2856
|
+
});
|
|
2857
|
+
if (!response.ok) return void 0;
|
|
2858
|
+
try {
|
|
2859
|
+
return await response.json();
|
|
2860
|
+
} catch {
|
|
2861
|
+
return;
|
|
2862
|
+
}
|
|
2863
|
+
})().catch(() => void 0).finally(() => {
|
|
2864
|
+
entry.settled = true;
|
|
2865
|
+
if (pending?.get(key) === entry) pending.delete(key);
|
|
2866
|
+
});
|
|
2867
|
+
pending.set(key, entry);
|
|
2868
|
+
return entry;
|
|
2869
|
+
}
|
|
2870
|
+
/**
|
|
2871
|
+
* Read only the immutable document fingerprint from one canonical text result.
|
|
2872
|
+
* Paths, image data, and every other model-visible result field are ignored.
|
|
2873
|
+
*/
|
|
2874
|
+
function documentSha256FromCanonicalResult(block) {
|
|
2875
|
+
if (!isRecord$1(block) || block.isError !== false || !Array.isArray(block.content) || block.content.length !== 1) return;
|
|
2876
|
+
const content = block.content[0];
|
|
2877
|
+
if (!isRecord$1(content) || content.type !== "text" || typeof content.text !== "string") return void 0;
|
|
2878
|
+
if (content.text.length > MAX_CANONICAL_RESULT_CHARS) return void 0;
|
|
2879
|
+
let result;
|
|
2880
|
+
try {
|
|
2881
|
+
result = JSON.parse(content.text);
|
|
2882
|
+
} catch {
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
if (!isRecord$1(result) || !isRecord$1(result.document)) return void 0;
|
|
2886
|
+
const fingerprint = result.document.sha256;
|
|
2887
|
+
return typeof fingerprint === "string" && /^[a-f0-9]{64}$/iu.test(fingerprint) ? fingerprint.toLowerCase() : void 0;
|
|
2888
|
+
}
|
|
2889
|
+
/** Select only canonical nested render results that actually need hydration. */
|
|
2890
|
+
function presentationHydrationRequestOf(candidate) {
|
|
2891
|
+
if (candidate.embeddedGrant !== void 0 || candidate.toolName !== "openpencil_render") return;
|
|
2892
|
+
const documentSha256 = documentSha256FromCanonicalResult(candidate.block);
|
|
2893
|
+
if (documentSha256 === void 0 || candidate.sessionId.length === 0 || candidate.sessionId.length > MAX_SESSION_ID_CHARS || candidate.callId.length === 0 || candidate.callId.length > MAX_CALL_ID_CHARS) return;
|
|
2894
|
+
return {
|
|
2895
|
+
sessionId: candidate.sessionId,
|
|
2896
|
+
callId: candidate.callId,
|
|
2897
|
+
documentSha256
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2900
|
+
/**
|
|
2901
|
+
* Exchange a non-secret result fingerprint for a same-origin presentation
|
|
2902
|
+
* grant. Concurrent subscribers share one request; an unmounted subscriber
|
|
2903
|
+
* can abort independently, and the network request is cancelled once nobody
|
|
2904
|
+
* is waiting for it.
|
|
2905
|
+
*/
|
|
2906
|
+
function requestPresentationGrant(request, parseMeta, options = {}) {
|
|
2907
|
+
if (!isRequest(request) || options.signal?.aborted === true) return Promise.resolve(void 0);
|
|
2908
|
+
const fetcher = options.fetcher ?? globalThis.fetch;
|
|
2909
|
+
if (typeof fetcher !== "function") return Promise.resolve(void 0);
|
|
2910
|
+
const entry = pendingEnvelope(request, fetcher);
|
|
2911
|
+
entry.subscribers += 1;
|
|
2912
|
+
return new Promise((resolve) => {
|
|
2913
|
+
let finished = false;
|
|
2914
|
+
const release = () => {
|
|
2915
|
+
entry.subscribers = Math.max(0, entry.subscribers - 1);
|
|
2916
|
+
entry.cancelIfUnused();
|
|
2917
|
+
};
|
|
2918
|
+
const finish = (value) => {
|
|
2919
|
+
if (finished) return;
|
|
2920
|
+
finished = true;
|
|
2921
|
+
options.signal?.removeEventListener("abort", abort);
|
|
2922
|
+
release();
|
|
2923
|
+
resolve(value);
|
|
2924
|
+
};
|
|
2925
|
+
const abort = () => {
|
|
2926
|
+
finish(void 0);
|
|
2927
|
+
};
|
|
2928
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
2929
|
+
entry.promise.then((value) => {
|
|
2930
|
+
if (finished || options.signal?.aborted === true) {
|
|
2931
|
+
finish(void 0);
|
|
2932
|
+
return;
|
|
2933
|
+
}
|
|
2934
|
+
if (!isRecord$1(value) || !Object.hasOwn(value, "$dshOpenPencil")) {
|
|
2935
|
+
finish(void 0);
|
|
2936
|
+
return;
|
|
2937
|
+
}
|
|
2938
|
+
try {
|
|
2939
|
+
finish(parseMeta({ [PRESENTATION_HYDRATION_META_KEY]: value[PRESENTATION_HYDRATION_META_KEY] }));
|
|
2940
|
+
} catch {
|
|
2941
|
+
finish(void 0);
|
|
2942
|
+
}
|
|
2943
|
+
}, () => {
|
|
2944
|
+
finish(void 0);
|
|
2945
|
+
});
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2948
|
+
//#endregion
|
|
2949
|
+
//#region src/client/index.tsx
|
|
2950
|
+
/**
|
|
2951
|
+
* Browser presentation for `openpencil_render` and historical
|
|
2952
|
+
* `design_render` conversation cards.
|
|
2953
|
+
*
|
|
2954
|
+
* PNG remains the replay-safe default. When the host also grants access to
|
|
2955
|
+
* the source `.op`, the user can opt into one shared, read-only Web SDK
|
|
2956
|
+
* canvas. The SDK and document are fetched only after that explicit action.
|
|
2957
|
+
*/
|
|
2958
|
+
/** Presentation metadata key the host half projects into `block.meta`. */
|
|
2959
|
+
const PRESENTATION_META_KEY = "$dshOpenPencil";
|
|
2960
|
+
const LIVE_AUTO_OPEN_TTL_MS = 9e5;
|
|
2961
|
+
const LIVE_AUTO_OPEN_MAX = 256;
|
|
2962
|
+
const liveAutoOpenActivatedAt = Date.now();
|
|
2963
|
+
const liveAutoOpenCalls = /* @__PURE__ */ new Map();
|
|
2964
|
+
function liveAutoOpenKey(sessionId, callId) {
|
|
2965
|
+
return `${sessionId.length}:${sessionId}${callId}`;
|
|
2966
|
+
}
|
|
2967
|
+
function pruneLiveAutoOpenCalls(now = Date.now()) {
|
|
2968
|
+
for (const [key, expiresAt] of liveAutoOpenCalls) if (expiresAt <= now) liveAutoOpenCalls.delete(key);
|
|
2969
|
+
while (liveAutoOpenCalls.size > LIVE_AUTO_OPEN_MAX) {
|
|
2970
|
+
const oldest = liveAutoOpenCalls.keys().next().value;
|
|
2971
|
+
if (oldest === void 0) break;
|
|
2972
|
+
liveAutoOpenCalls.delete(oldest);
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
function rememberLiveAutoOpenCall(key) {
|
|
2976
|
+
liveAutoOpenCalls.delete(key);
|
|
2977
|
+
liveAutoOpenCalls.set(key, Date.now() + LIVE_AUTO_OPEN_TTL_MS);
|
|
2978
|
+
pruneLiveAutoOpenCalls();
|
|
2979
|
+
}
|
|
2980
|
+
function takeLiveAutoOpenCall(key) {
|
|
2981
|
+
pruneLiveAutoOpenCalls();
|
|
2982
|
+
if (!liveAutoOpenCalls.has(key)) return false;
|
|
2983
|
+
liveAutoOpenCalls.delete(key);
|
|
2984
|
+
return true;
|
|
2985
|
+
}
|
|
2986
|
+
function forgetLiveAutoOpenCall(key) {
|
|
2987
|
+
liveAutoOpenCalls.delete(key);
|
|
2988
|
+
}
|
|
2989
|
+
const DESIGN_RENDER_COPY = {
|
|
2990
|
+
en: {
|
|
2991
|
+
designRender: "OpenPencil render",
|
|
2992
|
+
error: "error",
|
|
2993
|
+
rendering: "rendering…",
|
|
2994
|
+
done: "done",
|
|
2995
|
+
renderingDocument: "Rendering the design document…",
|
|
2996
|
+
renderFailed: "The render failed.",
|
|
2997
|
+
frames: "frames",
|
|
2998
|
+
openInteractiveCanvas: "Open interactive canvas",
|
|
2999
|
+
editCanvas: "Edit canvas",
|
|
3000
|
+
editInSidebar: "Edit in sidebar",
|
|
3001
|
+
openRenderedPng: "Open rendered PNG",
|
|
3002
|
+
downloadPng: "Download PNG",
|
|
3003
|
+
editSource: "Edit source .op",
|
|
3004
|
+
downloadSource: "Download source .op",
|
|
3005
|
+
inspectToolCall: "Inspect tool call",
|
|
3006
|
+
recoveringPreview: "Recovering the OpenPencil preview…",
|
|
3007
|
+
noPreview: "No preview channel available in this host.",
|
|
3008
|
+
canvas: "OpenPencil canvas",
|
|
3009
|
+
zoomOut: "Zoom out",
|
|
3010
|
+
zoomIn: "Zoom in",
|
|
3011
|
+
fit: "Fit",
|
|
3012
|
+
close: "Close",
|
|
3013
|
+
readonlyCanvas: "Read-only OpenPencil design canvas",
|
|
3014
|
+
loadingCanvas: "Loading interactive canvas…",
|
|
3015
|
+
pngRemains: "PNG preview remains available underneath the dialog.",
|
|
3016
|
+
canvasUnavailable: "Interactive canvas unavailable",
|
|
3017
|
+
openPngFallback: "Open PNG fallback",
|
|
3018
|
+
panHint: "Drag to pan · scroll to pan · Ctrl/⌘ + scroll to zoom",
|
|
3019
|
+
snapshot: "snapshot",
|
|
3020
|
+
editorUnavailable: "Editable OpenPencil canvas is not available for this result."
|
|
3021
|
+
},
|
|
3022
|
+
zh: {
|
|
3023
|
+
designRender: "OpenPencil 渲染",
|
|
3024
|
+
error: "错误",
|
|
3025
|
+
rendering: "渲染中…",
|
|
3026
|
+
done: "完成",
|
|
3027
|
+
renderingDocument: "正在渲染设计文档…",
|
|
3028
|
+
renderFailed: "渲染失败。",
|
|
3029
|
+
frames: "页",
|
|
3030
|
+
openInteractiveCanvas: "打开交互画布",
|
|
3031
|
+
editCanvas: "编辑画布",
|
|
3032
|
+
editInSidebar: "在侧边栏编辑",
|
|
3033
|
+
openRenderedPng: "打开渲染 PNG",
|
|
3034
|
+
downloadPng: "下载 PNG",
|
|
3035
|
+
editSource: "编辑源文件 .op",
|
|
3036
|
+
downloadSource: "下载源文件 .op",
|
|
3037
|
+
inspectToolCall: "检查工具调用",
|
|
3038
|
+
recoveringPreview: "正在恢复 OpenPencil 预览…",
|
|
3039
|
+
noPreview: "当前宿主没有可用的预览通道。",
|
|
3040
|
+
canvas: "OpenPencil 画布",
|
|
3041
|
+
zoomOut: "缩小",
|
|
3042
|
+
zoomIn: "放大",
|
|
3043
|
+
fit: "适应窗口",
|
|
3044
|
+
close: "关闭",
|
|
3045
|
+
readonlyCanvas: "只读 OpenPencil 设计画布",
|
|
3046
|
+
loadingCanvas: "正在加载交互画布…",
|
|
3047
|
+
pngRemains: "对话框下方仍保留 PNG 预览。",
|
|
3048
|
+
canvasUnavailable: "交互画布不可用",
|
|
3049
|
+
openPngFallback: "打开 PNG 预览",
|
|
3050
|
+
panHint: "拖动平移 · 滚动平移 · Ctrl/⌘ + 滚动缩放",
|
|
3051
|
+
snapshot: "快照",
|
|
3052
|
+
editorUnavailable: "此渲染结果没有可用的 OpenPencil 编辑画布。"
|
|
3053
|
+
}
|
|
3054
|
+
};
|
|
3055
|
+
function designRenderCopy(locale) {
|
|
3056
|
+
return DESIGN_RENDER_COPY[locale];
|
|
3057
|
+
}
|
|
3058
|
+
function isRecord(value) {
|
|
3059
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3060
|
+
}
|
|
3061
|
+
function optionalString(record, key) {
|
|
3062
|
+
const value = record[key];
|
|
3063
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
3064
|
+
}
|
|
3065
|
+
function optionalFiniteNumber(record, key) {
|
|
3066
|
+
const value = record[key];
|
|
3067
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
3068
|
+
}
|
|
3069
|
+
function optionalStrings(record, key) {
|
|
3070
|
+
const value = record[key];
|
|
3071
|
+
if (!Array.isArray(value)) return void 0;
|
|
3072
|
+
const strings = value.filter((item) => typeof item === "string" && item.length > 0);
|
|
3073
|
+
return strings.length === 0 ? void 0 : strings;
|
|
3074
|
+
}
|
|
3075
|
+
function imageGrantOf(value) {
|
|
3076
|
+
if (!isRecord(value)) return void 0;
|
|
3077
|
+
const path = optionalString(value, "path");
|
|
3078
|
+
const previewUrl = optionalString(value, "previewUrl");
|
|
3079
|
+
const downloadUrl = optionalString(value, "downloadUrl");
|
|
3080
|
+
if (path === void 0 || previewUrl === void 0 || downloadUrl === void 0) return void 0;
|
|
3081
|
+
const id = optionalString(value, "id");
|
|
3082
|
+
const name = optionalString(value, "name");
|
|
3083
|
+
const index = optionalFiniteNumber(value, "index");
|
|
3084
|
+
return {
|
|
3085
|
+
path,
|
|
3086
|
+
previewUrl,
|
|
3087
|
+
downloadUrl,
|
|
3088
|
+
width: optionalFiniteNumber(value, "width"),
|
|
3089
|
+
height: optionalFiniteNumber(value, "height"),
|
|
3090
|
+
...id === void 0 ? {} : { id },
|
|
3091
|
+
...name === void 0 ? {} : { name },
|
|
3092
|
+
...index === void 0 || !Number.isSafeInteger(index) || index < 0 ? {} : { index }
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
function imageGrantsOf(value) {
|
|
3096
|
+
if (!Array.isArray(value)) return void 0;
|
|
3097
|
+
const frames = value.map(imageGrantOf).filter((frame) => frame !== void 0);
|
|
3098
|
+
return frames.length === 0 ? void 0 : frames;
|
|
3099
|
+
}
|
|
3100
|
+
function documentGrantOf(envelope, image) {
|
|
3101
|
+
const raw = isRecord(envelope.document) ? envelope.document : void 0;
|
|
3102
|
+
const legacyImage = isRecord(image) ? image : void 0;
|
|
3103
|
+
const url = raw === void 0 ? optionalString(envelope, "documentUrl") ?? optionalString(envelope, "sourceUrl") ?? (legacyImage === void 0 ? void 0 : optionalString(legacyImage, "documentUrl") ?? optionalString(legacyImage, "sourceUrl") ?? optionalString(legacyImage, "opUrl")) : optionalString(raw, "url") ?? optionalString(raw, "documentUrl");
|
|
3104
|
+
if (url === void 0) return void 0;
|
|
3105
|
+
return {
|
|
3106
|
+
url,
|
|
3107
|
+
path: raw === void 0 ? optionalString(envelope, "sourcePath") : optionalString(raw, "path"),
|
|
3108
|
+
downloadUrl: raw === void 0 ? optionalString(envelope, "documentDownloadUrl") : optionalString(raw, "downloadUrl"),
|
|
3109
|
+
bytes: raw === void 0 ? void 0 : optionalFiniteNumber(raw, "bytes"),
|
|
3110
|
+
sha256: raw === void 0 ? void 0 : optionalString(raw, "sha256"),
|
|
3111
|
+
mimeType: raw === void 0 ? void 0 : optionalString(raw, "mimeType")
|
|
3112
|
+
};
|
|
3113
|
+
}
|
|
3114
|
+
function viewerGrantOf(value) {
|
|
3115
|
+
if (!isRecord(value)) return void 0;
|
|
3116
|
+
const sdkUrl = optionalString(value, "sdkUrl");
|
|
3117
|
+
const wasmUrl = optionalString(value, "wasmUrl");
|
|
3118
|
+
const canvasKitBaseUrl = optionalString(value, "canvasKitBaseUrl") ?? optionalString(value, "assetBaseUrl");
|
|
3119
|
+
if (sdkUrl === void 0 || wasmUrl === void 0 || canvasKitBaseUrl === void 0) return void 0;
|
|
3120
|
+
return {
|
|
3121
|
+
sdkUrl,
|
|
3122
|
+
wasmUrl,
|
|
3123
|
+
canvasKitBaseUrl
|
|
3124
|
+
};
|
|
3125
|
+
}
|
|
3126
|
+
function editorGrantOf(value) {
|
|
3127
|
+
if (!isRecord(value) || value.enabled !== true) return void 0;
|
|
3128
|
+
const launchUrl = optionalString(value, "launchUrl");
|
|
3129
|
+
if (launchUrl === void 0) return void 0;
|
|
3130
|
+
const refreshUrl = optionalString(value, "refreshUrl");
|
|
3131
|
+
return {
|
|
3132
|
+
enabled: true,
|
|
3133
|
+
launchUrl,
|
|
3134
|
+
...refreshUrl === void 0 ? {} : { refreshUrl }
|
|
3135
|
+
};
|
|
3136
|
+
}
|
|
3137
|
+
/** Parse both the established v1 envelope and the additive v2 shape. */
|
|
3138
|
+
function presentationGrantOfMeta(metaValue) {
|
|
3139
|
+
const envelope = (isRecord(metaValue) ? metaValue : void 0)?.[PRESENTATION_META_KEY];
|
|
3140
|
+
if (!isRecord(envelope) || envelope.schemaVersion !== 1 && envelope.schemaVersion !== 2) return void 0;
|
|
3141
|
+
const frames = imageGrantsOf(envelope.frames);
|
|
3142
|
+
const image = imageGrantOf(envelope.image) ?? frames?.[0];
|
|
3143
|
+
const document = documentGrantOf(envelope, envelope.image);
|
|
3144
|
+
if (image === void 0 && document === void 0) return void 0;
|
|
3145
|
+
return {
|
|
3146
|
+
schemaVersion: envelope.schemaVersion,
|
|
3147
|
+
image,
|
|
3148
|
+
frames: frames ?? (image === void 0 ? void 0 : [image]),
|
|
3149
|
+
document,
|
|
3150
|
+
viewer: viewerGrantOf(envelope.viewer),
|
|
3151
|
+
editor: editorGrantOf(envelope.editor),
|
|
3152
|
+
renderer: optionalString(envelope, "renderer"),
|
|
3153
|
+
rendererBinary: optionalString(envelope, "rendererBinary"),
|
|
3154
|
+
fidelity: optionalString(envelope, "fidelity"),
|
|
3155
|
+
warnings: optionalStrings(envelope, "warnings"),
|
|
3156
|
+
...envelope.autoOpenEditor === true ? { autoOpenEditor: true } : {}
|
|
3157
|
+
};
|
|
3158
|
+
}
|
|
3159
|
+
function grantOf(block) {
|
|
3160
|
+
if (!("kind" in block) || block.isError) return void 0;
|
|
3161
|
+
return presentationGrantOfMeta(block.meta);
|
|
3162
|
+
}
|
|
3163
|
+
/** Flatten the durable result text for the fallback disclosure. */
|
|
3164
|
+
function resultText(block) {
|
|
3165
|
+
if (!("kind" in block)) return null;
|
|
3166
|
+
const parts = [];
|
|
3167
|
+
for (const item of block.content) parts.push(item.type === "text" ? item.text : JSON.stringify(item, null, 2));
|
|
3168
|
+
if (parts.length === 0 && block.error !== void 0) parts.push(`${block.error.name}: ${block.error.code}`);
|
|
3169
|
+
return parts.join("\n") || null;
|
|
3170
|
+
}
|
|
3171
|
+
const sdkLoads = /* @__PURE__ */ new Map();
|
|
3172
|
+
/** Load the host-served ESM core SDK without coupling the client bundle to React 19. */
|
|
3173
|
+
function loadOpenPencilSdk(url) {
|
|
3174
|
+
const absoluteUrl = new URL(url, window.location.href).href;
|
|
3175
|
+
let pending = sdkLoads.get(absoluteUrl);
|
|
3176
|
+
if (pending === void 0) {
|
|
3177
|
+
pending = import(
|
|
3178
|
+
/* @vite-ignore */
|
|
3179
|
+
absoluteUrl
|
|
3180
|
+
).then((module) => {
|
|
3181
|
+
if (!isRecord(module) || typeof module.createViewer !== "function") throw new Error("OpenPencil viewer SDK did not export createViewer");
|
|
3182
|
+
return module;
|
|
3183
|
+
});
|
|
3184
|
+
sdkLoads.set(absoluteUrl, pending);
|
|
3185
|
+
pending.catch(() => {
|
|
3186
|
+
sdkLoads.delete(absoluteUrl);
|
|
3187
|
+
});
|
|
3188
|
+
}
|
|
3189
|
+
return pending;
|
|
3190
|
+
}
|
|
3191
|
+
let activeCanvas;
|
|
3192
|
+
/** @internal Claim the page-wide SDK singleton; opening another canvas closes this one. */
|
|
3193
|
+
function claimCanvas(token, close) {
|
|
3194
|
+
const previous = activeCanvas;
|
|
3195
|
+
activeCanvas = {
|
|
3196
|
+
token,
|
|
3197
|
+
close
|
|
3198
|
+
};
|
|
3199
|
+
if (previous !== void 0 && previous.token !== token) previous.close();
|
|
3200
|
+
return () => {
|
|
3201
|
+
if (activeCanvas?.token === token) activeCanvas = void 0;
|
|
3202
|
+
};
|
|
3203
|
+
}
|
|
3204
|
+
const styles = {
|
|
3205
|
+
card: {
|
|
3206
|
+
border: "1px solid var(--ui-border, rgba(128,128,128,0.35))",
|
|
3207
|
+
borderRadius: 8,
|
|
3208
|
+
overflow: "hidden",
|
|
3209
|
+
background: "var(--ui-card-bg, transparent)",
|
|
3210
|
+
fontFamily: "inherit"
|
|
3211
|
+
},
|
|
3212
|
+
head: {
|
|
3213
|
+
display: "flex",
|
|
3214
|
+
alignItems: "center",
|
|
3215
|
+
gap: 8,
|
|
3216
|
+
padding: "8px 12px",
|
|
3217
|
+
fontSize: 13,
|
|
3218
|
+
fontWeight: 600,
|
|
3219
|
+
borderBottom: "1px solid var(--ui-border, rgba(128,128,128,0.2))"
|
|
3220
|
+
},
|
|
3221
|
+
badge: {
|
|
3222
|
+
fontSize: 11,
|
|
3223
|
+
padding: "1px 8px",
|
|
3224
|
+
borderRadius: 99,
|
|
3225
|
+
textTransform: "uppercase",
|
|
3226
|
+
letterSpacing: .4
|
|
3227
|
+
},
|
|
3228
|
+
badgeOk: {
|
|
3229
|
+
background: "rgba(34,197,94,0.15)",
|
|
3230
|
+
color: "#16a34a"
|
|
3231
|
+
},
|
|
3232
|
+
badgeError: {
|
|
3233
|
+
background: "rgba(239,68,68,0.15)",
|
|
3234
|
+
color: "#dc2626"
|
|
3235
|
+
},
|
|
3236
|
+
badgeRunning: {
|
|
3237
|
+
background: "rgba(100,116,139,0.15)",
|
|
3238
|
+
color: "#64748b"
|
|
3239
|
+
},
|
|
3240
|
+
body: { padding: 12 },
|
|
3241
|
+
imageViewport: {
|
|
3242
|
+
maxHeight: 560,
|
|
3243
|
+
overflow: "auto",
|
|
3244
|
+
overscrollBehavior: "contain",
|
|
3245
|
+
borderRadius: 4,
|
|
3246
|
+
border: "1px solid rgba(128,128,128,0.25)",
|
|
3247
|
+
background: "rgba(128,128,128,0.06)"
|
|
3248
|
+
},
|
|
3249
|
+
img: {
|
|
3250
|
+
display: "block",
|
|
3251
|
+
width: "auto",
|
|
3252
|
+
maxWidth: "100%",
|
|
3253
|
+
height: "auto",
|
|
3254
|
+
margin: "0 auto"
|
|
3255
|
+
},
|
|
3256
|
+
meta: {
|
|
3257
|
+
display: "flex",
|
|
3258
|
+
alignItems: "center",
|
|
3259
|
+
flexWrap: "wrap",
|
|
3260
|
+
gap: 10,
|
|
3261
|
+
marginTop: 10,
|
|
3262
|
+
fontSize: 12,
|
|
3263
|
+
color: "var(--ui-text-muted, #888)"
|
|
3264
|
+
},
|
|
3265
|
+
link: {
|
|
3266
|
+
color: "var(--ui-accent, #0ea5e9)",
|
|
3267
|
+
textDecoration: "none"
|
|
3268
|
+
},
|
|
3269
|
+
button: {
|
|
3270
|
+
color: "var(--ui-accent, #0ea5e9)",
|
|
3271
|
+
background: "none",
|
|
3272
|
+
border: "none",
|
|
3273
|
+
cursor: "pointer",
|
|
3274
|
+
padding: 0,
|
|
3275
|
+
font: "inherit",
|
|
3276
|
+
fontSize: 12
|
|
3277
|
+
},
|
|
3278
|
+
primaryButton: {
|
|
3279
|
+
border: "1px solid var(--ui-accent, #0ea5e9)",
|
|
3280
|
+
borderRadius: 6,
|
|
3281
|
+
color: "var(--ui-accent, #0ea5e9)",
|
|
3282
|
+
background: "transparent",
|
|
3283
|
+
padding: "4px 9px",
|
|
3284
|
+
cursor: "pointer",
|
|
3285
|
+
font: "inherit",
|
|
3286
|
+
fontSize: 12
|
|
3287
|
+
},
|
|
3288
|
+
pre: {
|
|
3289
|
+
whiteSpace: "pre-wrap",
|
|
3290
|
+
wordBreak: "break-all",
|
|
3291
|
+
fontSize: 12,
|
|
3292
|
+
margin: 0,
|
|
3293
|
+
maxHeight: "24em",
|
|
3294
|
+
overflow: "auto"
|
|
3295
|
+
},
|
|
3296
|
+
muted: {
|
|
3297
|
+
fontSize: 12,
|
|
3298
|
+
color: "var(--ui-text-muted, #888)"
|
|
3299
|
+
},
|
|
3300
|
+
warning: {
|
|
3301
|
+
margin: "10px 0 0",
|
|
3302
|
+
padding: "7px 9px",
|
|
3303
|
+
borderRadius: 6,
|
|
3304
|
+
color: "#b45309",
|
|
3305
|
+
background: "rgba(245,158,11,0.13)",
|
|
3306
|
+
fontSize: 12
|
|
3307
|
+
},
|
|
3308
|
+
backdrop: {
|
|
3309
|
+
position: "fixed",
|
|
3310
|
+
inset: 0,
|
|
3311
|
+
zIndex: 2147483e3,
|
|
3312
|
+
display: "flex",
|
|
3313
|
+
alignItems: "center",
|
|
3314
|
+
justifyContent: "center",
|
|
3315
|
+
padding: 20,
|
|
3316
|
+
background: "rgba(0,0,0,0.72)"
|
|
3317
|
+
},
|
|
3318
|
+
dialog: {
|
|
3319
|
+
width: "min(1120px, 94vw)",
|
|
3320
|
+
height: "min(820px, 92vh)",
|
|
3321
|
+
display: "flex",
|
|
3322
|
+
flexDirection: "column",
|
|
3323
|
+
overflow: "hidden",
|
|
3324
|
+
border: "1px solid var(--ui-border, rgba(128,128,128,0.5))",
|
|
3325
|
+
borderRadius: 10,
|
|
3326
|
+
background: "var(--ui-card-bg, #17171a)",
|
|
3327
|
+
color: "var(--ui-text, #eee)",
|
|
3328
|
+
boxShadow: "0 24px 80px rgba(0,0,0,0.45)"
|
|
3329
|
+
},
|
|
3330
|
+
toolbar: {
|
|
3331
|
+
display: "flex",
|
|
3332
|
+
alignItems: "center",
|
|
3333
|
+
flexWrap: "wrap",
|
|
3334
|
+
gap: 8,
|
|
3335
|
+
minHeight: 44,
|
|
3336
|
+
padding: "7px 10px",
|
|
3337
|
+
borderBottom: "1px solid var(--ui-border, rgba(128,128,128,0.3))"
|
|
3338
|
+
},
|
|
3339
|
+
canvasWrap: {
|
|
3340
|
+
position: "relative",
|
|
3341
|
+
flex: 1,
|
|
3342
|
+
minHeight: 0,
|
|
3343
|
+
overflow: "hidden",
|
|
3344
|
+
background: "#202124"
|
|
3345
|
+
},
|
|
3346
|
+
canvas: {
|
|
3347
|
+
display: "block",
|
|
3348
|
+
width: "100%",
|
|
3349
|
+
height: "100%",
|
|
3350
|
+
cursor: "grab",
|
|
3351
|
+
touchAction: "none"
|
|
3352
|
+
},
|
|
3353
|
+
overlay: {
|
|
3354
|
+
position: "absolute",
|
|
3355
|
+
inset: 0,
|
|
3356
|
+
display: "flex",
|
|
3357
|
+
alignItems: "center",
|
|
3358
|
+
justifyContent: "center",
|
|
3359
|
+
flexDirection: "column",
|
|
3360
|
+
gap: 10,
|
|
3361
|
+
padding: 24,
|
|
3362
|
+
textAlign: "center",
|
|
3363
|
+
background: "rgba(25,25,28,0.92)"
|
|
3364
|
+
}
|
|
3365
|
+
};
|
|
3366
|
+
function baseName(path) {
|
|
3367
|
+
const normalized = path.replaceAll("\\", "/");
|
|
3368
|
+
return normalized.slice(normalized.lastIndexOf("/") + 1) || path;
|
|
3369
|
+
}
|
|
3370
|
+
/** Size the canvas backing store to its CSS box before CanvasKit attaches. */
|
|
3371
|
+
function sizeCanvasForDisplay(canvas, devicePixelRatio = window.devicePixelRatio) {
|
|
3372
|
+
const cssWidth = Math.max(1, Math.round(canvas.clientWidth));
|
|
3373
|
+
const cssHeight = Math.max(1, Math.round(canvas.clientHeight));
|
|
3374
|
+
const dpr = Number.isFinite(devicePixelRatio) && devicePixelRatio > 0 ? devicePixelRatio : 1;
|
|
3375
|
+
canvas.width = Math.max(1, Math.round(cssWidth * dpr));
|
|
3376
|
+
canvas.height = Math.max(1, Math.round(cssHeight * dpr));
|
|
3377
|
+
return {
|
|
3378
|
+
cssWidth,
|
|
3379
|
+
cssHeight,
|
|
3380
|
+
dpr
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3383
|
+
function CanvasModal({ grant, onClose, locale }) {
|
|
3384
|
+
const canvasRef = (0, react.useRef)(null);
|
|
3385
|
+
const viewerRef = (0, react.useRef)();
|
|
3386
|
+
const dragRef = (0, react.useRef)();
|
|
3387
|
+
const [phase, setPhase] = (0, react.useState)("loading");
|
|
3388
|
+
const [failure, setFailure] = (0, react.useState)("");
|
|
3389
|
+
const [viewport, setViewport] = (0, react.useState)({
|
|
3390
|
+
panX: 0,
|
|
3391
|
+
panY: 0,
|
|
3392
|
+
zoom: 1
|
|
3393
|
+
});
|
|
3394
|
+
const documentGrant = grant.document;
|
|
3395
|
+
const viewerGrant = grant.viewer;
|
|
3396
|
+
const copy = designRenderCopy(locale);
|
|
3397
|
+
const fit = (0, react.useCallback)(() => {
|
|
3398
|
+
const viewer = viewerRef.current;
|
|
3399
|
+
const canvas = canvasRef.current;
|
|
3400
|
+
if (viewer === void 0 || canvas === null) return;
|
|
3401
|
+
viewer.zoomToFit(Math.max(1, canvas.clientWidth), Math.max(1, canvas.clientHeight));
|
|
3402
|
+
setViewport(viewer.viewport);
|
|
3403
|
+
}, []);
|
|
3404
|
+
const zoomBy = (0, react.useCallback)((factor) => {
|
|
3405
|
+
const viewer = viewerRef.current;
|
|
3406
|
+
if (viewer === void 0) return;
|
|
3407
|
+
viewer.setZoom(Math.min(16, Math.max(.05, viewer.viewport.zoom * factor)));
|
|
3408
|
+
setViewport(viewer.viewport);
|
|
3409
|
+
}, []);
|
|
3410
|
+
(0, react.useEffect)(() => {
|
|
3411
|
+
const onKeyDown = (event) => {
|
|
3412
|
+
if (event.key === "Escape") onClose();
|
|
3413
|
+
};
|
|
3414
|
+
window.addEventListener("keydown", onKeyDown);
|
|
3415
|
+
return () => {
|
|
3416
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
3417
|
+
};
|
|
3418
|
+
}, [onClose]);
|
|
3419
|
+
(0, react.useEffect)(() => {
|
|
3420
|
+
const canvas = canvasRef.current;
|
|
3421
|
+
if (canvas === null || documentGrant === void 0 || viewerGrant === void 0) return;
|
|
3422
|
+
sizeCanvasForDisplay(canvas);
|
|
3423
|
+
const abort = new AbortController();
|
|
3424
|
+
let cancelled = false;
|
|
3425
|
+
let created;
|
|
3426
|
+
setPhase("loading");
|
|
3427
|
+
setFailure("");
|
|
3428
|
+
const load = async () => {
|
|
3429
|
+
try {
|
|
3430
|
+
const [sdk, response] = await Promise.all([loadOpenPencilSdk(viewerGrant.sdkUrl), fetch(documentGrant.url, {
|
|
3431
|
+
signal: abort.signal,
|
|
3432
|
+
credentials: "same-origin"
|
|
3433
|
+
})]);
|
|
3434
|
+
if (!response.ok) throw new Error(`OpenPencil document request failed (${response.status})`);
|
|
3435
|
+
const source = await response.text();
|
|
3436
|
+
if (cancelled) return;
|
|
3437
|
+
created = await sdk.createViewer({
|
|
3438
|
+
canvas,
|
|
3439
|
+
doc: source,
|
|
3440
|
+
wasmUrl: viewerGrant.wasmUrl,
|
|
3441
|
+
canvasKitBaseUrl: viewerGrant.canvasKitBaseUrl
|
|
3442
|
+
});
|
|
3443
|
+
if (cancelled) {
|
|
3444
|
+
created.destroy();
|
|
3445
|
+
return;
|
|
3446
|
+
}
|
|
3447
|
+
viewerRef.current = created;
|
|
3448
|
+
const syncViewport = () => {
|
|
3449
|
+
if (!cancelled && created !== void 0) setViewport(created.viewport);
|
|
3450
|
+
};
|
|
3451
|
+
created.on("viewportchange", syncViewport);
|
|
3452
|
+
setPhase("ready");
|
|
3453
|
+
requestAnimationFrame(() => {
|
|
3454
|
+
if (!cancelled) fit();
|
|
3455
|
+
});
|
|
3456
|
+
} catch (error) {
|
|
3457
|
+
if (cancelled || abort.signal.aborted) return;
|
|
3458
|
+
setFailure(error instanceof Error ? error.message : String(error));
|
|
3459
|
+
setPhase("error");
|
|
3460
|
+
}
|
|
3461
|
+
};
|
|
3462
|
+
load();
|
|
3463
|
+
return () => {
|
|
3464
|
+
cancelled = true;
|
|
3465
|
+
abort.abort();
|
|
3466
|
+
viewerRef.current = void 0;
|
|
3467
|
+
created?.destroy();
|
|
3468
|
+
};
|
|
3469
|
+
}, [
|
|
3470
|
+
documentGrant?.url,
|
|
3471
|
+
fit,
|
|
3472
|
+
viewerGrant?.canvasKitBaseUrl,
|
|
3473
|
+
viewerGrant?.sdkUrl,
|
|
3474
|
+
viewerGrant?.wasmUrl
|
|
3475
|
+
]);
|
|
3476
|
+
const pointerDown = (event) => {
|
|
3477
|
+
const viewer = viewerRef.current;
|
|
3478
|
+
if (viewer === void 0) return;
|
|
3479
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
3480
|
+
const current = viewer.viewport;
|
|
3481
|
+
dragRef.current = {
|
|
3482
|
+
id: event.pointerId,
|
|
3483
|
+
x: event.clientX,
|
|
3484
|
+
y: event.clientY,
|
|
3485
|
+
panX: current.panX,
|
|
3486
|
+
panY: current.panY
|
|
3487
|
+
};
|
|
3488
|
+
};
|
|
3489
|
+
const pointerMove = (event) => {
|
|
3490
|
+
const drag = dragRef.current;
|
|
3491
|
+
const viewer = viewerRef.current;
|
|
3492
|
+
if (drag === void 0 || drag.id !== event.pointerId || viewer === void 0) return;
|
|
3493
|
+
viewer.panTo(drag.panX + event.clientX - drag.x, drag.panY + event.clientY - drag.y);
|
|
3494
|
+
setViewport(viewer.viewport);
|
|
3495
|
+
};
|
|
3496
|
+
const pointerUp = (event) => {
|
|
3497
|
+
if (dragRef.current?.id === event.pointerId) dragRef.current = void 0;
|
|
3498
|
+
};
|
|
3499
|
+
const title = documentGrant?.path === void 0 ? copy.canvas : baseName(documentGrant.path);
|
|
3500
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3501
|
+
style: styles.backdrop,
|
|
3502
|
+
role: "presentation",
|
|
3503
|
+
"data-openpencil-canvas-modal": "true",
|
|
3504
|
+
onMouseDown: (event) => {
|
|
3505
|
+
if (event.target === event.currentTarget) onClose();
|
|
3506
|
+
},
|
|
3507
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3508
|
+
style: styles.dialog,
|
|
3509
|
+
role: "dialog",
|
|
3510
|
+
"aria-modal": "true",
|
|
3511
|
+
"aria-label": `${copy.canvas}: ${title}`,
|
|
3512
|
+
children: [
|
|
3513
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3514
|
+
style: styles.toolbar,
|
|
3515
|
+
children: [
|
|
3516
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
3517
|
+
style: {
|
|
3518
|
+
marginRight: "auto",
|
|
3519
|
+
overflow: "hidden",
|
|
3520
|
+
textOverflow: "ellipsis",
|
|
3521
|
+
whiteSpace: "nowrap"
|
|
3522
|
+
},
|
|
3523
|
+
children: title
|
|
3524
|
+
}),
|
|
3525
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3526
|
+
type: "button",
|
|
3527
|
+
style: styles.primaryButton,
|
|
3528
|
+
disabled: phase !== "ready",
|
|
3529
|
+
onClick: () => {
|
|
3530
|
+
zoomBy(.8);
|
|
3531
|
+
},
|
|
3532
|
+
"aria-label": copy.zoomOut,
|
|
3533
|
+
children: "−"
|
|
3534
|
+
}),
|
|
3535
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3536
|
+
style: styles.muted,
|
|
3537
|
+
children: [Math.round(viewport.zoom * 100), "%"]
|
|
3538
|
+
}),
|
|
3539
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3540
|
+
type: "button",
|
|
3541
|
+
style: styles.primaryButton,
|
|
3542
|
+
disabled: phase !== "ready",
|
|
3543
|
+
onClick: () => {
|
|
3544
|
+
zoomBy(1.25);
|
|
3545
|
+
},
|
|
3546
|
+
"aria-label": copy.zoomIn,
|
|
3547
|
+
children: "+"
|
|
3548
|
+
}),
|
|
3549
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3550
|
+
type: "button",
|
|
3551
|
+
style: styles.primaryButton,
|
|
3552
|
+
disabled: phase !== "ready",
|
|
3553
|
+
onClick: fit,
|
|
3554
|
+
children: copy.fit
|
|
3555
|
+
}),
|
|
3556
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3557
|
+
type: "button",
|
|
3558
|
+
style: styles.primaryButton,
|
|
3559
|
+
onClick: onClose,
|
|
3560
|
+
children: copy.close
|
|
3561
|
+
})
|
|
3562
|
+
]
|
|
3563
|
+
}),
|
|
3564
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3565
|
+
style: styles.canvasWrap,
|
|
3566
|
+
children: [
|
|
3567
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
|
|
3568
|
+
ref: canvasRef,
|
|
3569
|
+
style: styles.canvas,
|
|
3570
|
+
onPointerDown: pointerDown,
|
|
3571
|
+
onPointerMove: pointerMove,
|
|
3572
|
+
onPointerUp: pointerUp,
|
|
3573
|
+
onPointerCancel: pointerUp,
|
|
3574
|
+
"aria-label": copy.readonlyCanvas
|
|
3575
|
+
}),
|
|
3576
|
+
phase === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3577
|
+
style: styles.overlay,
|
|
3578
|
+
role: "status",
|
|
3579
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: copy.loadingCanvas }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3580
|
+
style: styles.muted,
|
|
3581
|
+
children: copy.pngRemains
|
|
3582
|
+
})]
|
|
3583
|
+
}) : null,
|
|
3584
|
+
phase === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3585
|
+
style: styles.overlay,
|
|
3586
|
+
role: "alert",
|
|
3587
|
+
children: [
|
|
3588
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: copy.canvasUnavailable }),
|
|
3589
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3590
|
+
style: styles.muted,
|
|
3591
|
+
children: failure
|
|
3592
|
+
}),
|
|
3593
|
+
grant.image !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
3594
|
+
style: styles.link,
|
|
3595
|
+
href: grant.image.previewUrl,
|
|
3596
|
+
target: "_blank",
|
|
3597
|
+
rel: "noreferrer",
|
|
3598
|
+
children: copy.openPngFallback
|
|
3599
|
+
}) : null
|
|
3600
|
+
]
|
|
3601
|
+
}) : null
|
|
3602
|
+
]
|
|
3603
|
+
}),
|
|
3604
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3605
|
+
style: {
|
|
3606
|
+
...styles.meta,
|
|
3607
|
+
margin: 0,
|
|
3608
|
+
padding: "7px 10px"
|
|
3609
|
+
},
|
|
3610
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: copy.panHint }), documentGrant?.sha256 !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3611
|
+
title: documentGrant.sha256,
|
|
3612
|
+
children: [
|
|
3613
|
+
copy.snapshot,
|
|
3614
|
+
" ",
|
|
3615
|
+
documentGrant.sha256.slice(0, 10)
|
|
3616
|
+
]
|
|
3617
|
+
}) : null]
|
|
3618
|
+
})
|
|
3619
|
+
]
|
|
3620
|
+
})
|
|
3621
|
+
});
|
|
3622
|
+
}
|
|
3623
|
+
/** Render one OpenPencil render tool call as a PNG-first card. */
|
|
3624
|
+
function DesignRenderView({ block, callId, toolName, openDetails, openFile, inspect, locale = "en", sessionId, openEditorWorkbench, autoOpenEditorWorkbench }) {
|
|
3625
|
+
const settled = "kind" in block;
|
|
3626
|
+
const error = settled && block.isError;
|
|
3627
|
+
const running = !settled;
|
|
3628
|
+
const embeddedGrant = grantOf(block);
|
|
3629
|
+
const hydrationRequest = !running && !error ? presentationHydrationRequestOf({
|
|
3630
|
+
block,
|
|
3631
|
+
toolName,
|
|
3632
|
+
sessionId: String(sessionId),
|
|
3633
|
+
callId,
|
|
3634
|
+
embeddedGrant
|
|
3635
|
+
}) : void 0;
|
|
3636
|
+
const hydrationKey = hydrationRequest === void 0 ? void 0 : `${hydrationRequest.sessionId}\n${hydrationRequest.callId}\n${hydrationRequest.documentSha256}`;
|
|
3637
|
+
const [hydrated, setHydrated] = (0, react.useState)();
|
|
3638
|
+
const [hydrationFailedKey, setHydrationFailedKey] = (0, react.useState)();
|
|
3639
|
+
const grant = embeddedGrant ?? (hydrated !== void 0 && hydrated.key === hydrationKey ? hydrated.grant : void 0);
|
|
3640
|
+
const hydrationPending = hydrationKey !== void 0 && hydrationFailedKey !== hydrationKey;
|
|
3641
|
+
const copy = designRenderCopy(locale);
|
|
3642
|
+
const text = resultText(block);
|
|
3643
|
+
const frames = grant?.frames ?? [];
|
|
3644
|
+
const [selectedFrameIndex, setSelectedFrameIndex] = (0, react.useState)(0);
|
|
3645
|
+
const currentFrameIndex = normalizeFrameIndex(selectedFrameIndex, frames.length);
|
|
3646
|
+
const selectedFrame = frames[currentFrameIndex] ?? grant?.image;
|
|
3647
|
+
const [modalToken, setModalToken] = (0, react.useState)();
|
|
3648
|
+
const releaseRef = (0, react.useRef)();
|
|
3649
|
+
const liveAutoOpenCallKey = liveAutoOpenKey(String(sessionId), callId);
|
|
3650
|
+
(0, react.useEffect)(() => {
|
|
3651
|
+
if (hydrationKey === void 0 || hydrationRequest === void 0) return;
|
|
3652
|
+
const controller = new AbortController();
|
|
3653
|
+
requestPresentationGrant(hydrationRequest, presentationGrantOfMeta, { signal: controller.signal }).then((nextGrant) => {
|
|
3654
|
+
if (nextGrant !== void 0 && !controller.signal.aborted) setHydrated({
|
|
3655
|
+
key: hydrationKey,
|
|
3656
|
+
grant: nextGrant
|
|
3657
|
+
});
|
|
3658
|
+
else if (!controller.signal.aborted) setHydrationFailedKey(hydrationKey);
|
|
3659
|
+
});
|
|
3660
|
+
return () => {
|
|
3661
|
+
controller.abort();
|
|
3662
|
+
};
|
|
3663
|
+
}, [hydrationKey]);
|
|
3664
|
+
const closeCanvas = (0, react.useCallback)(() => {
|
|
3665
|
+
releaseRef.current?.();
|
|
3666
|
+
releaseRef.current = void 0;
|
|
3667
|
+
setModalToken(void 0);
|
|
3668
|
+
}, []);
|
|
3669
|
+
const openCanvas = (0, react.useCallback)(() => {
|
|
3670
|
+
const token = Symbol("openpencil-canvas");
|
|
3671
|
+
releaseRef.current?.();
|
|
3672
|
+
releaseRef.current = claimCanvas(token, () => {
|
|
3673
|
+
setModalToken((current) => current === token ? void 0 : current);
|
|
3674
|
+
});
|
|
3675
|
+
setModalToken(token);
|
|
3676
|
+
}, []);
|
|
3677
|
+
const openEditor = (0, react.useCallback)(() => {
|
|
3678
|
+
requestOpenPencilEditor(embeddedGrant === void 0 ? void 0 : openDetails, () => {
|
|
3679
|
+
if (grant === void 0) return;
|
|
3680
|
+
openEditorWorkbench?.({
|
|
3681
|
+
grant,
|
|
3682
|
+
sessionId: String(sessionId)
|
|
3683
|
+
});
|
|
3684
|
+
});
|
|
3685
|
+
}, [
|
|
3686
|
+
embeddedGrant,
|
|
3687
|
+
grant,
|
|
3688
|
+
openDetails,
|
|
3689
|
+
openEditorWorkbench,
|
|
3690
|
+
sessionId
|
|
3691
|
+
]);
|
|
3692
|
+
(0, react.useEffect)(() => {
|
|
3693
|
+
if (running && block.time >= liveAutoOpenActivatedAt) rememberLiveAutoOpenCall(liveAutoOpenCallKey);
|
|
3694
|
+
else if (error) forgetLiveAutoOpenCall(liveAutoOpenCallKey);
|
|
3695
|
+
}, [
|
|
3696
|
+
error,
|
|
3697
|
+
liveAutoOpenCallKey,
|
|
3698
|
+
running
|
|
3699
|
+
]);
|
|
3700
|
+
(0, react.useEffect)(() => {
|
|
3701
|
+
if (running || error || grant?.autoOpenEditor !== true || grant.editor?.enabled !== true || autoOpenEditorWorkbench === void 0) return;
|
|
3702
|
+
if (!takeLiveAutoOpenCall(liveAutoOpenCallKey)) return;
|
|
3703
|
+
autoOpenEditorWorkbench({
|
|
3704
|
+
grant,
|
|
3705
|
+
sessionId: String(sessionId)
|
|
3706
|
+
});
|
|
3707
|
+
}, [
|
|
3708
|
+
autoOpenEditorWorkbench,
|
|
3709
|
+
error,
|
|
3710
|
+
grant,
|
|
3711
|
+
liveAutoOpenCallKey,
|
|
3712
|
+
running,
|
|
3713
|
+
sessionId
|
|
3714
|
+
]);
|
|
3715
|
+
(0, react.useEffect)(() => () => {
|
|
3716
|
+
releaseRef.current?.();
|
|
3717
|
+
}, []);
|
|
3718
|
+
(0, react.useEffect)(() => {
|
|
3719
|
+
setSelectedFrameIndex(0);
|
|
3720
|
+
}, [frames.map((frame) => frame.previewUrl).join("\n")]);
|
|
3721
|
+
const badge = error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3722
|
+
style: {
|
|
3723
|
+
...styles.badge,
|
|
3724
|
+
...styles.badgeError
|
|
3725
|
+
},
|
|
3726
|
+
children: copy.error
|
|
3727
|
+
}) : running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3728
|
+
style: {
|
|
3729
|
+
...styles.badge,
|
|
3730
|
+
...styles.badgeRunning
|
|
3731
|
+
},
|
|
3732
|
+
children: copy.rendering
|
|
3733
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3734
|
+
style: {
|
|
3735
|
+
...styles.badge,
|
|
3736
|
+
...styles.badgeOk
|
|
3737
|
+
},
|
|
3738
|
+
children: copy.done
|
|
3739
|
+
});
|
|
3740
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
3741
|
+
style: styles.card,
|
|
3742
|
+
"data-tool": OPENPENCIL_RENDER_TOOL_NAME,
|
|
3743
|
+
"data-state": error ? "error" : running ? "running" : "success",
|
|
3744
|
+
children: [
|
|
3745
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3746
|
+
style: styles.head,
|
|
3747
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: copy.designRender }), badge]
|
|
3748
|
+
}),
|
|
3749
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3750
|
+
style: styles.body,
|
|
3751
|
+
children: [
|
|
3752
|
+
running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3753
|
+
style: styles.muted,
|
|
3754
|
+
children: copy.renderingDocument
|
|
3755
|
+
}) : null,
|
|
3756
|
+
error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3757
|
+
style: styles.muted,
|
|
3758
|
+
children: text ?? copy.renderFailed
|
|
3759
|
+
}) : null,
|
|
3760
|
+
!running && !error && frames.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FrameGallery, {
|
|
3761
|
+
frames,
|
|
3762
|
+
selectedIndex: currentFrameIndex,
|
|
3763
|
+
onSelect: setSelectedFrameIndex,
|
|
3764
|
+
locale
|
|
3765
|
+
}) : null,
|
|
3766
|
+
!running && !error && grant?.warnings !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3767
|
+
style: styles.warning,
|
|
3768
|
+
role: "status",
|
|
3769
|
+
children: grant.warnings.join(" ")
|
|
3770
|
+
}) : null,
|
|
3771
|
+
!running && !error && grant !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3772
|
+
style: styles.meta,
|
|
3773
|
+
children: [
|
|
3774
|
+
selectedFrame !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: selectedFrame.name ?? baseName(selectedFrame.path) }) : null,
|
|
3775
|
+
frames.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
3776
|
+
frames.length,
|
|
3777
|
+
" ",
|
|
3778
|
+
copy.frames
|
|
3779
|
+
] }) : null,
|
|
3780
|
+
grant.renderer !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3781
|
+
title: grant.rendererBinary,
|
|
3782
|
+
children: [grant.renderer, grant.fidelity === void 0 ? "" : ` · ${grant.fidelity}`]
|
|
3783
|
+
}) : null,
|
|
3784
|
+
grant.document !== void 0 && grant.viewer !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3785
|
+
type: "button",
|
|
3786
|
+
style: styles.primaryButton,
|
|
3787
|
+
onClick: openCanvas,
|
|
3788
|
+
children: copy.openInteractiveCanvas
|
|
3789
|
+
}) : null,
|
|
3790
|
+
grant.document !== void 0 && grant.editor?.enabled === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3791
|
+
type: "button",
|
|
3792
|
+
style: styles.primaryButton,
|
|
3793
|
+
onClick: openEditor,
|
|
3794
|
+
children: openDetails === void 0 || embeddedGrant === void 0 ? copy.editCanvas : copy.editInSidebar
|
|
3795
|
+
}) : null,
|
|
3796
|
+
selectedFrame !== void 0 && openFile !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3797
|
+
type: "button",
|
|
3798
|
+
style: styles.button,
|
|
3799
|
+
onClick: () => {
|
|
3800
|
+
openFile(selectedFrame.path);
|
|
3801
|
+
},
|
|
3802
|
+
children: copy.openRenderedPng
|
|
3803
|
+
}) : null,
|
|
3804
|
+
selectedFrame !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
3805
|
+
style: styles.link,
|
|
3806
|
+
href: selectedFrame.downloadUrl,
|
|
3807
|
+
download: true,
|
|
3808
|
+
children: copy.downloadPng
|
|
3809
|
+
}) : null,
|
|
3810
|
+
grant.document?.path !== void 0 && openFile !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3811
|
+
type: "button",
|
|
3812
|
+
style: styles.button,
|
|
3813
|
+
onClick: () => {
|
|
3814
|
+
openFile(grant.document?.path ?? "");
|
|
3815
|
+
},
|
|
3816
|
+
children: copy.editSource
|
|
3817
|
+
}) : null,
|
|
3818
|
+
grant.document?.downloadUrl !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
3819
|
+
style: styles.link,
|
|
3820
|
+
href: grant.document.downloadUrl,
|
|
3821
|
+
download: true,
|
|
3822
|
+
children: copy.downloadSource
|
|
3823
|
+
}) : null,
|
|
3824
|
+
inspect !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3825
|
+
type: "button",
|
|
3826
|
+
style: styles.button,
|
|
3827
|
+
onClick: inspect,
|
|
3828
|
+
children: copy.inspectToolCall
|
|
3829
|
+
}) : null
|
|
3830
|
+
]
|
|
3831
|
+
}) : null,
|
|
3832
|
+
!running && !error && grant === void 0 && hydrationPending ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3833
|
+
style: styles.muted,
|
|
3834
|
+
role: "status",
|
|
3835
|
+
children: copy.recoveringPreview
|
|
3836
|
+
}) : null,
|
|
3837
|
+
!running && !error && grant === void 0 && !hydrationPending ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3838
|
+
style: styles.muted,
|
|
3839
|
+
children: copy.noPreview
|
|
3840
|
+
}), text !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
3841
|
+
style: {
|
|
3842
|
+
...styles.pre,
|
|
3843
|
+
marginTop: 8
|
|
3844
|
+
},
|
|
3845
|
+
children: text
|
|
3846
|
+
}) : null] }) : null
|
|
3847
|
+
]
|
|
3848
|
+
}),
|
|
3849
|
+
modalToken !== void 0 && grant?.document !== void 0 && grant.viewer !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CanvasModal, {
|
|
3850
|
+
grant,
|
|
3851
|
+
onClose: closeCanvas,
|
|
3852
|
+
locale
|
|
3853
|
+
}) : null
|
|
3854
|
+
]
|
|
3855
|
+
});
|
|
3856
|
+
}
|
|
3857
|
+
/** Render the selected editable design inside DSH's resident details column. */
|
|
3858
|
+
function OpenPencilEditorPanel({ block, colorScheme, locale, sessionId }) {
|
|
3859
|
+
const grant = grantOf(block);
|
|
3860
|
+
if (grant?.editor === void 0 || grant.document === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3861
|
+
style: styles.overlay,
|
|
3862
|
+
children: editorPanelCopy(locale).unavailable
|
|
3863
|
+
});
|
|
3864
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ManagedOpenPencilEditor, {
|
|
3865
|
+
grant,
|
|
3866
|
+
colorScheme,
|
|
3867
|
+
locale,
|
|
3868
|
+
sessionId: String(sessionId)
|
|
3869
|
+
}, editorWorkbenchEditorKey(grant, String(sessionId)));
|
|
3870
|
+
}
|
|
3871
|
+
/** Required client services. */
|
|
3872
|
+
const inject = [
|
|
3873
|
+
"slots",
|
|
3874
|
+
"theme",
|
|
3875
|
+
"locale"
|
|
3876
|
+
];
|
|
3877
|
+
/** Register canonical views plus a presentation-only alias for replaying historical cards. */
|
|
3878
|
+
function apply(ctx) {
|
|
3879
|
+
const subscribeTheme = (notify) => ctx.on("theme/change", notify);
|
|
3880
|
+
const getColorScheme = () => ctx.theme.getTheme().active.colorScheme;
|
|
3881
|
+
const subscribeLocale = (notify) => ctx.on("locale/change", notify);
|
|
3882
|
+
const getLocale = () => ctx.locale.getLocale().active;
|
|
3883
|
+
const getEditorLocale = () => editorLocaleFromDsh(getLocale());
|
|
3884
|
+
let editorWorkbenchHost;
|
|
3885
|
+
if (typeof document !== "undefined") ctx.effect(() => {
|
|
3886
|
+
const host = mountEditorWorkbenchHost({
|
|
3887
|
+
subscribeTheme,
|
|
3888
|
+
getColorScheme,
|
|
3889
|
+
subscribeLocale,
|
|
3890
|
+
getLocale
|
|
3891
|
+
});
|
|
3892
|
+
editorWorkbenchHost = host;
|
|
3893
|
+
return () => {
|
|
3894
|
+
if (editorWorkbenchHost === host) editorWorkbenchHost = void 0;
|
|
3895
|
+
return host.dispose();
|
|
3896
|
+
};
|
|
3897
|
+
}, "dsh-openpencil: fallback editor workbench host");
|
|
3898
|
+
const HostSyncedDesignRenderView = (props) => {
|
|
3899
|
+
const locale = (0, react.useSyncExternalStore)(subscribeLocale, getLocale, getLocale);
|
|
3900
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DesignRenderView, {
|
|
3901
|
+
...props,
|
|
3902
|
+
locale,
|
|
3903
|
+
openEditorWorkbench: (request) => editorWorkbenchHost?.open(request) ?? false,
|
|
3904
|
+
autoOpenEditorWorkbench: (request) => editorWorkbenchHost?.openIfIdle(request) ?? false
|
|
3905
|
+
});
|
|
3906
|
+
};
|
|
3907
|
+
const HostSyncedOpenPencilEditorPanel = (props) => {
|
|
3908
|
+
const colorScheme = (0, react.useSyncExternalStore)(subscribeTheme, getColorScheme, getColorScheme);
|
|
3909
|
+
const locale = (0, react.useSyncExternalStore)(subscribeLocale, getEditorLocale, getEditorLocale);
|
|
3910
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OpenPencilEditorPanel, {
|
|
3911
|
+
...props,
|
|
3912
|
+
colorScheme,
|
|
3913
|
+
locale
|
|
3914
|
+
});
|
|
3915
|
+
};
|
|
3916
|
+
const HostSyncedOpenPencilSelectionDock = (props) => {
|
|
3917
|
+
const locale = (0, react.useSyncExternalStore)(subscribeLocale, getLocale, getLocale);
|
|
3918
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OpenPencilSelectionDock, {
|
|
3919
|
+
...props,
|
|
3920
|
+
locale
|
|
3921
|
+
});
|
|
3922
|
+
};
|
|
3923
|
+
for (const toolName of [OPENPENCIL_RENDER_TOOL_NAME, LEGACY_DESIGN_RENDER_TOOL_NAME]) {
|
|
3924
|
+
ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
|
|
3925
|
+
name: "tool.call.toolview",
|
|
3926
|
+
key: toolName
|
|
3927
|
+
}, HostSyncedDesignRenderView));
|
|
3928
|
+
ctx.slots.inject("tool.details.toolview", () => ctx.slots.register({
|
|
3929
|
+
name: "tool.details.toolview",
|
|
3930
|
+
key: toolName
|
|
3931
|
+
}, HostSyncedOpenPencilEditorPanel));
|
|
3932
|
+
}
|
|
3933
|
+
ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
|
|
3934
|
+
name: "conversation.input.dock",
|
|
3935
|
+
id: "openpencil-selection",
|
|
3936
|
+
order: 30
|
|
3937
|
+
}, HostSyncedOpenPencilSelectionDock));
|
|
3938
|
+
}
|
|
3939
|
+
//#endregion
|
|
3940
|
+
exports.DesignRenderView = DesignRenderView;
|
|
3941
|
+
exports.EDITOR_WORKBENCH_FULLSCREEN_BREAKPOINT = EDITOR_WORKBENCH_FULLSCREEN_BREAKPOINT;
|
|
3942
|
+
exports.EDITOR_WORKBENCH_LEFT_CLEARANCE = EDITOR_WORKBENCH_LEFT_CLEARANCE;
|
|
3943
|
+
exports.EDITOR_WORKBENCH_MAX_WIDTH = EDITOR_WORKBENCH_MAX_WIDTH;
|
|
3944
|
+
exports.EDITOR_WORKBENCH_MIN_WIDTH = EDITOR_WORKBENCH_MIN_WIDTH;
|
|
3945
|
+
exports.EDITOR_WORKBENCH_RESIZE_STEP = EDITOR_WORKBENCH_RESIZE_STEP;
|
|
3946
|
+
exports.GALLERY_COMPACT_MAX_HEIGHT = GALLERY_COMPACT_MAX_HEIGHT;
|
|
3947
|
+
exports.GALLERY_TOOLBAR_CONTROL_CONTENT_LAYOUT = GALLERY_TOOLBAR_CONTROL_CONTENT_LAYOUT;
|
|
3948
|
+
exports.GALLERY_TOOLBAR_CONTROL_HEIGHT = GALLERY_TOOLBAR_CONTROL_HEIGHT;
|
|
3949
|
+
exports.GALLERY_TOOLBAR_CONTROL_LAYOUT = GALLERY_TOOLBAR_CONTROL_LAYOUT;
|
|
3950
|
+
exports.GALLERY_ZOOM_MAX = GALLERY_ZOOM_MAX;
|
|
3951
|
+
exports.GALLERY_ZOOM_MIN = GALLERY_ZOOM_MIN;
|
|
3952
|
+
exports.GALLERY_ZOOM_STEP = GALLERY_ZOOM_STEP;
|
|
3953
|
+
exports.LEGACY_DESIGN_RENDER_TOOL_NAME = LEGACY_DESIGN_RENDER_TOOL_NAME;
|
|
3954
|
+
exports.OPENPENCIL_RENDER_TOOL_NAME = OPENPENCIL_RENDER_TOOL_NAME;
|
|
3955
|
+
exports.OPENPENCIL_SELECTION_DOCK_LAYOUT = OPENPENCIL_SELECTION_DOCK_LAYOUT;
|
|
3956
|
+
exports.OPENPENCIL_WORKBENCH_DOCK_ATTRIBUTE = OPENPENCIL_WORKBENCH_DOCK_ATTRIBUTE;
|
|
3957
|
+
exports.OpenPencilEditorPanel = OpenPencilEditorPanel;
|
|
3958
|
+
exports.PRESENTATION_HYDRATION_ENDPOINT = PRESENTATION_HYDRATION_ENDPOINT;
|
|
3959
|
+
exports.PRESENTATION_META_KEY = PRESENTATION_META_KEY;
|
|
3960
|
+
exports.apply = apply;
|
|
3961
|
+
exports.applyManagedEditorUnmountPolicy = applyManagedEditorUnmountPolicy;
|
|
3962
|
+
exports.beginEditorInitRetry = beginEditorInitRetry;
|
|
3963
|
+
exports.calculateGalleryFitViewZoom = calculateGalleryFitViewZoom;
|
|
3964
|
+
exports.captureManagedEditorRecovery = captureManagedEditorRecovery;
|
|
3965
|
+
exports.claimCanvas = claimCanvas;
|
|
3966
|
+
exports.claimEditor = claimEditor;
|
|
3967
|
+
exports.claimEditorWorkbenchDock = claimEditorWorkbenchDock;
|
|
3968
|
+
exports.clampEditorWorkbenchWidth = clampEditorWorkbenchWidth;
|
|
3969
|
+
exports.clampGalleryZoom = clampGalleryZoom;
|
|
3970
|
+
exports.clearOpenPencilSelection = clearOpenPencilSelection;
|
|
3971
|
+
exports.closeManagedEditorLaunch = closeManagedEditorLaunch;
|
|
3972
|
+
exports.confirmEditorClose = confirmEditorClose;
|
|
3973
|
+
exports.confirmEditorModalClose = confirmEditorModalClose;
|
|
3974
|
+
exports.createEditorWorkbenchStore = createEditorWorkbenchStore;
|
|
3975
|
+
exports.designRenderCopy = designRenderCopy;
|
|
3976
|
+
exports.discardManagedEditorRecovery = discardManagedEditorRecovery;
|
|
3977
|
+
exports.documentSha256FromCanonicalResult = documentSha256FromCanonicalResult;
|
|
3978
|
+
exports.editorControlUrl = editorControlUrl;
|
|
3979
|
+
exports.editorGrantForBoot = editorGrantForBoot;
|
|
3980
|
+
exports.editorIframeUrlWithLocale = editorIframeUrlWithLocale;
|
|
3981
|
+
exports.editorIframeUrlWithTheme = editorIframeUrlWithTheme;
|
|
3982
|
+
exports.editorLocaleFromDsh = editorLocaleFromDsh;
|
|
3983
|
+
exports.editorMessageFrom = editorMessageFrom;
|
|
3984
|
+
exports.editorModalCopy = editorModalCopy;
|
|
3985
|
+
exports.editorOrigin = editorOrigin;
|
|
3986
|
+
exports.editorPanelCopy = editorPanelCopy;
|
|
3987
|
+
exports.editorRecoveryCopy = editorRecoveryCopy;
|
|
3988
|
+
exports.editorRecoveryItemUrl = editorRecoveryItemUrl;
|
|
3989
|
+
exports.editorRecoverySummaryOf = editorRecoverySummaryOf;
|
|
3990
|
+
exports.editorSuccessorFromSave = editorSuccessorFromSave;
|
|
3991
|
+
exports.editorSuccessorStorageKey = editorSuccessorStorageKey;
|
|
3992
|
+
exports.editorWorkbenchEditorKey = editorWorkbenchEditorKey;
|
|
3993
|
+
exports.editorWorkbenchFocusTargetIndex = editorWorkbenchFocusTargetIndex;
|
|
3994
|
+
exports.editorWorkbenchShouldHandleEscape = editorWorkbenchShouldHandleEscape;
|
|
3995
|
+
exports.editorWorkbenchUsesFullscreen = editorWorkbenchUsesFullscreen;
|
|
3996
|
+
exports.editorWorkbenchWidthBounds = editorWorkbenchWidthBounds;
|
|
3997
|
+
exports.encodeEditorOutbound = encodeEditorOutbound;
|
|
3998
|
+
exports.frameGalleryCopy = frameGalleryCopy;
|
|
3999
|
+
exports.frameLabel = frameLabel;
|
|
4000
|
+
exports.galleryViewportMaxHeight = galleryViewportMaxHeight;
|
|
4001
|
+
exports.galleryZoomCommandTarget = galleryZoomCommandTarget;
|
|
4002
|
+
exports.galleryZoomPercent = galleryZoomPercent;
|
|
4003
|
+
exports.galleryZoomShortcut = galleryZoomShortcut;
|
|
4004
|
+
exports.getOpenPencilSelectionSnapshot = getOpenPencilSelectionSnapshot;
|
|
4005
|
+
exports.grantOf = grantOf;
|
|
4006
|
+
exports.hasOpenPencilSelection = hasOpenPencilSelection;
|
|
4007
|
+
exports.inject = inject;
|
|
4008
|
+
exports.isTerminalEditorSelectionStatus = isTerminalEditorSelectionStatus;
|
|
4009
|
+
exports.launchManagedEditor = launchManagedEditor;
|
|
4010
|
+
exports.liveSelectionOf = liveSelectionOf;
|
|
4011
|
+
exports.loadOpenPencilSdk = loadOpenPencilSdk;
|
|
4012
|
+
exports.mountEditorWorkbenchHost = mountEditorWorkbenchHost;
|
|
4013
|
+
exports.nextGalleryZoom = nextGalleryZoom;
|
|
4014
|
+
exports.normalizeFrameIndex = normalizeFrameIndex;
|
|
4015
|
+
exports.parseEditorInbound = parseEditorInbound;
|
|
4016
|
+
exports.prepareManagedEditor = prepareManagedEditor;
|
|
4017
|
+
exports.prepareManagedEditorForMount = prepareManagedEditorForMount;
|
|
4018
|
+
exports.presentationGrantOfMeta = presentationGrantOfMeta;
|
|
4019
|
+
exports.presentationHydrationRequestOf = presentationHydrationRequestOf;
|
|
4020
|
+
exports.preserveEditorBeforeWorkbenchDispose = preserveEditorBeforeWorkbenchDispose;
|
|
4021
|
+
exports.publishOpenPencilSelection = publishOpenPencilSelection;
|
|
4022
|
+
exports.rememberEditorSuccessor = rememberEditorSuccessor;
|
|
4023
|
+
exports.requestOpenPencilEditor = requestOpenPencilEditor;
|
|
4024
|
+
exports.requestPresentationGrant = requestPresentationGrant;
|
|
4025
|
+
exports.resizedEditorWorkbenchWidth = resizedEditorWorkbenchWidth;
|
|
4026
|
+
exports.restoreManagedEditorRecovery = restoreManagedEditorRecovery;
|
|
4027
|
+
exports.selectionNodeDetail = selectionNodeDetail;
|
|
4028
|
+
exports.selectionNodeLabel = selectionNodeLabel;
|
|
4029
|
+
exports.sizeCanvasForDisplay = sizeCanvasForDisplay;
|
|
4030
|
+
exports.startEditorSelectionPolling = startEditorSelectionPolling;
|
|
4031
|
+
exports.subscribeOpenPencilSelection = subscribeOpenPencilSelection;
|
|
4032
|
+
|
|
4033
|
+
return module.exports; } });
|