dsh-image-viewer 0.1.0-beta.10

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/lib/client.js ADDED
@@ -0,0 +1,846 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-image-viewer",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ //#region src/viewer.js
11
+ const boundedNumber = (value, fallback) => Number.isFinite(value) && value > 0 ? value : fallback;
12
+ const downloadOf = (value) => typeof value?.onInvoke === "function" ? {
13
+ pendingLabel: typeof value.pendingLabel === "string" && value.pendingLabel !== "" ? value.pendingLabel : void 0,
14
+ errorLabel: typeof value.errorLabel === "string" && value.errorLabel !== "" ? value.errorLabel : void 0,
15
+ onInvoke: value.onInvoke
16
+ } : void 0;
17
+ const actionsOf = (value) => Array.isArray(value) ? value.flatMap((action, position) => {
18
+ if (typeof action?.onInvoke !== "function" || typeof action?.label !== "string" || action.label.trim() === "") return [];
19
+ return [{
20
+ id: typeof action.id === "string" && action.id !== "" ? action.id : `action-${position + 1}`,
21
+ label: action.label,
22
+ pendingLabel: typeof action.pendingLabel === "string" && action.pendingLabel !== "" ? action.pendingLabel : action.label,
23
+ errorLabel: typeof action.errorLabel === "string" && action.errorLabel !== "" ? action.errorLabel : action.label,
24
+ closeOnSuccess: action.closeOnSuccess === true,
25
+ onInvoke: action.onInvoke
26
+ }];
27
+ }) : [];
28
+ function normalizeViewerRequest(request) {
29
+ const items = (Array.isArray(request?.items) ? request.items : []).flatMap((item, position) => {
30
+ if (typeof item?.src !== "string" || item.src === "") return [];
31
+ return [{
32
+ id: typeof item.id === "string" && item.id !== "" ? item.id : `image-${position + 1}`,
33
+ src: item.src,
34
+ name: typeof item.name === "string" && item.name !== "" ? item.name : `Image ${position + 1}`,
35
+ width: boundedNumber(item.width, void 0),
36
+ height: boundedNumber(item.height, void 0),
37
+ bytes: boundedNumber(item.bytes, void 0),
38
+ download: downloadOf(item.download),
39
+ actions: actionsOf(item.actions)
40
+ }];
41
+ });
42
+ if (items.length === 0) return void 0;
43
+ const requestedIndex = Number.isInteger(request?.index) ? request.index : 0;
44
+ return {
45
+ items,
46
+ index: Math.max(0, Math.min(items.length - 1, requestedIndex)),
47
+ opener: typeof HTMLElement !== "undefined" && request?.opener instanceof HTMLElement ? request.opener : void 0,
48
+ source: typeof request?.source === "string" ? request.source : "dsh",
49
+ annotations: request?.annotations !== false
50
+ };
51
+ }
52
+ var NativeImageViewerService = class {
53
+ #listeners = /* @__PURE__ */ new Set();
54
+ #revision = 0;
55
+ #snapshot;
56
+ #annotationsByImage = /* @__PURE__ */ new Map();
57
+ constructor() {
58
+ this.subscribe = (listener) => {
59
+ this.#listeners.add(listener);
60
+ return () => {
61
+ this.#listeners.delete(listener);
62
+ };
63
+ };
64
+ this.getSnapshot = () => this.#snapshot;
65
+ this.getAnnotationsSnapshot = () => Object.fromEntries([...this.#annotationsByImage].map(([id, annotations]) => [id, annotations.map((annotation) => ({ ...annotation }))]));
66
+ }
67
+ setAnnotations(imageId, annotations) {
68
+ if (typeof imageId !== "string" || imageId === "" || !Array.isArray(annotations)) return;
69
+ if (annotations.length === 0) this.#annotationsByImage.delete(imageId);
70
+ else this.#annotationsByImage.set(imageId, annotations.map((annotation) => ({ ...annotation })));
71
+ }
72
+ open(request) {
73
+ const normalized = normalizeViewerRequest(request);
74
+ if (normalized === void 0) return false;
75
+ this.#revision += 1;
76
+ this.#snapshot = {
77
+ ...normalized,
78
+ revision: this.#revision
79
+ };
80
+ this.#emit();
81
+ return true;
82
+ }
83
+ close() {
84
+ if (this.#snapshot === void 0) return;
85
+ const opener = this.#snapshot.opener;
86
+ this.#snapshot = void 0;
87
+ this.#emit();
88
+ if (typeof window === "undefined") opener?.focus();
89
+ else window.requestAnimationFrame(() => {
90
+ opener?.focus();
91
+ });
92
+ }
93
+ #emit() {
94
+ for (const listener of this.#listeners) listener();
95
+ }
96
+ };
97
+ function nativeImageButton(target) {
98
+ if (!(target instanceof Element)) return void 0;
99
+ const button = target.closest("button");
100
+ if (!(button instanceof HTMLButtonElement)) return void 0;
101
+ const image = button.querySelector(":scope > img");
102
+ if (!(image instanceof HTMLImageElement) || image.currentSrc === "") return void 0;
103
+ if (button.matches("[data-variant=\"single\"],[data-variant=\"tile\"]")) return {
104
+ button,
105
+ image,
106
+ group: button.parentElement
107
+ };
108
+ const rail = button.closest("[role=\"group\"]");
109
+ if (rail !== null && button.hasAttribute("title")) return {
110
+ button,
111
+ image,
112
+ group: rail
113
+ };
114
+ }
115
+ function imageItemsForButton(match) {
116
+ const candidates = [...match.group?.querySelectorAll("button[data-variant=\"single\"],button[data-variant=\"tile\"],button[title]") ?? []].flatMap((button, index) => {
117
+ const image = button.querySelector(":scope > img");
118
+ if (!(image instanceof HTMLImageElement) || image.currentSrc === "") return [];
119
+ return [{
120
+ id: image.currentSrc,
121
+ src: image.currentSrc,
122
+ name: image.alt || `Image ${index + 1}`,
123
+ width: image.naturalWidth || void 0,
124
+ height: image.naturalHeight || void 0,
125
+ button
126
+ }];
127
+ });
128
+ const items = candidates.length > 0 ? candidates : [{
129
+ id: match.image.currentSrc,
130
+ src: match.image.currentSrc,
131
+ name: match.image.alt || "Image",
132
+ width: match.image.naturalWidth || void 0,
133
+ height: match.image.naturalHeight || void 0,
134
+ button: match.button
135
+ }];
136
+ const index = Math.max(0, items.findIndex((item) => item.button === match.button));
137
+ return {
138
+ items: items.map(({ button: _button, ...item }) => item),
139
+ index
140
+ };
141
+ }
142
+ //#endregion
143
+ //#region src/styles.js
144
+ const CSS$1 = String.raw`
145
+ .niv-root{position:fixed;inset:0;z-index:1000;pointer-events:auto;overflow:hidden;background:rgba(7,8,10,.68);color:var(--dsw-alias-label-primary-inverted,#fff);outline:0;backdrop-filter:blur(13px) saturate(.72);-webkit-backdrop-filter:blur(13px) saturate(.72)}
146
+ .niv-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}
147
+ .niv-topbar{position:absolute;right:50%;bottom:22px;z-index:6;max-width:calc(100vw - 36px);padding:5px;border:1px solid rgba(255,255,255,.12);border-radius:999px;background:rgba(38,39,43,.86);box-shadow:0 10px 34px rgba(0,0,0,.3);transform:translateX(50%);backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px)}
148
+ .niv-actions{display:flex;align-items:center;gap:2px;overflow-x:auto;scrollbar-width:none}.niv-actions::-webkit-scrollbar{display:none}.niv-button,.niv-download{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:6px;height:32px;padding:0 10px;border:0;border-radius:999px;background:transparent;color:rgba(255,255,255,.9);font:inherit;font-size:12px;text-decoration:none;white-space:nowrap;cursor:pointer}.niv-button:hover,.niv-download:hover,.niv-button[data-active=true]{background:rgba(255,255,255,.12)}.niv-button:focus-visible,.niv-download:focus-visible,.niv-close-floating:focus-visible{outline:2px solid rgba(255,255,255,.9);outline-offset:2px}.niv-button:disabled,.niv-download:disabled{opacity:.38;cursor:default}.niv-icon-only{width:32px;padding:0}.niv-zoom{min-width:44px;color:rgba(255,255,255,.68);font-size:12px;font-variant-numeric:tabular-nums;text-align:center}
149
+ .niv-close-floating{position:absolute;top:20px;right:20px;z-index:8;display:grid;place-items:center;width:42px;height:42px;padding:0;border:1px solid rgba(255,255,255,.1);border-radius:50%;background:rgba(48,49,53,.82);box-shadow:0 8px 24px rgba(0,0,0,.28);color:#fff;cursor:pointer;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px)}.niv-close-floating:hover{background:rgba(66,67,72,.92)}
150
+ .niv-workspace{position:absolute;inset:0;display:grid;min-width:0;min-height:0;padding:68px 24px 72px;box-sizing:border-box}
151
+ .niv-stage{position:relative;display:grid;place-items:center;min-width:0;min-height:0;overflow:hidden;padding:0;touch-action:none;user-select:none}.niv-stage[data-dragging=true]{cursor:grabbing}.niv-stage[data-annotating=true]{cursor:crosshair}
152
+ .niv-surface{position:relative;display:inline-flex;max-width:100%;max-height:100%;transform-origin:center;will-change:transform}.niv-image{display:block;max-width:calc(100vw - 72px);max-height:calc(100vh - 154px);border-radius:12px;object-fit:contain;box-shadow:0 22px 60px rgba(0,0,0,.46);user-select:none;-webkit-user-drag:none}
153
+ .niv-annotation{position:absolute;z-index:5;width:24px;height:24px;transform-origin:center;pointer-events:none}.niv-pin{position:absolute;inset:0;display:grid;place-items:center;width:24px;height:24px;padding:0;border:2px solid #fff;border-radius:50%;background:rgba(23,24,27,.94);box-shadow:0 4px 16px rgba(0,0,0,.35);color:#fff;font:inherit;font-size:11px;font-weight:700;cursor:pointer;pointer-events:auto}.niv-pin[data-active=true]{background:var(--dsw-alias-state-business-primary,#3964fe)}
154
+ .niv-inline-note{position:absolute;bottom:34px;box-sizing:border-box;display:grid;width:min(300px,calc(100vw - 40px));grid-template-columns:24px minmax(0,1fr) 24px;align-items:center;gap:7px;padding:7px 8px;border:1px solid rgba(255,255,255,.12);border-radius:18px;background:rgba(32,33,37,.94);box-shadow:0 14px 38px rgba(0,0,0,.38);color:#fff;pointer-events:auto;backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px)}.niv-annotation[data-x=right] .niv-inline-note{left:-8px}.niv-annotation[data-x=left] .niv-inline-note{right:-8px}.niv-annotation[data-x=center] .niv-inline-note{left:50%;transform:translateX(-50%)}.niv-annotation[data-y=down] .niv-inline-note{top:34px;bottom:auto}.niv-inline-note::after{position:absolute;width:9px;height:9px;background:rgba(32,33,37,.94);content:'';transform:rotate(45deg)}.niv-annotation[data-y=up] .niv-inline-note::after{bottom:-5px}.niv-annotation[data-y=down] .niv-inline-note::after{top:-5px}.niv-annotation[data-x=right] .niv-inline-note::after{left:13px}.niv-annotation[data-x=left] .niv-inline-note::after{right:13px}.niv-annotation[data-x=center] .niv-inline-note::after{left:calc(50% - 4px)}.niv-inline-index{display:grid;place-items:center;width:22px;height:22px;border-radius:50%;background:var(--dsw-alias-state-business-primary,#3964fe);color:#fff;font-size:10px;font-weight:700}.niv-inline-note textarea{box-sizing:border-box;width:100%;min-height:24px;max-height:92px;resize:none;overflow:auto;border:0;outline:0;background:transparent;color:#fff;font:inherit;font-size:12px;line-height:18px}.niv-inline-note textarea::placeholder{color:rgba(255,255,255,.44)}.niv-note-remove{display:grid;place-items:center;width:24px;height:24px;padding:0;border:0;border-radius:50%;background:transparent;color:rgba(255,255,255,.68);cursor:pointer}.niv-note-remove:hover{background:rgba(255,255,255,.1);color:#fff}
155
+ .niv-nav{position:absolute;top:50%;z-index:4;width:42px;height:42px;padding:0;transform:translateY(-50%);background:rgba(48,49,53,.82);box-shadow:0 8px 24px rgba(0,0,0,.28);backdrop-filter:blur(16px)}.niv-prev{left:6px}.niv-next{right:6px}.niv-counter{position:absolute;bottom:8px;left:50%;padding:5px 10px;border-radius:999px;background:rgba(38,39,43,.86);color:rgba(255,255,255,.72);font-size:11px;transform:translateX(-50%);backdrop-filter:blur(16px)}
156
+ .niv-hint{display:none}
157
+ .niv-copy-notes{position:absolute;right:20px;bottom:22px;z-index:6;display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 11px;border:1px solid rgba(255,255,255,.1);border-radius:999px;background:rgba(38,39,43,.86);color:rgba(255,255,255,.82);font:inherit;font-size:12px;cursor:pointer;backdrop-filter:blur(16px)}
158
+ @media(max-width:760px){.niv-close-floating{top:12px;right:12px;width:40px;height:40px}.niv-workspace{padding:60px 12px 68px}.niv-image{max-width:calc(100vw - 24px);max-height:calc(100vh - 136px)}.niv-topbar{bottom:12px;max-width:calc(100vw - 24px)}.niv-button{padding:0 8px}.niv-button span.niv-label{display:none}.niv-inline-note{width:min(260px,calc(100vw - 40px))}.niv-copy-notes{display:none}}
159
+ @media(prefers-reduced-motion:reduce){.niv-surface{transition:none}}
160
+ `;
161
+ //#endregion
162
+ //#region src/client.jsx
163
+ const name = "dsh-image-viewer";
164
+ const inject = ["locale", "slots"];
165
+ const LOCALES = {
166
+ en: {
167
+ dialog: "Image viewer",
168
+ close: "Close",
169
+ fit: "Fit",
170
+ actual: "100%",
171
+ download: "Download",
172
+ annotate: "Mark region",
173
+ cancelAnnotate: "Cancel marking",
174
+ regions: "Region notes",
175
+ regionHint: "Click a point on the image, then add a note.",
176
+ note: "Region {value}",
177
+ notePlaceholder: "Describe what should change here…",
178
+ removeNote: "Remove region note",
179
+ hideNotes: "Hide notes",
180
+ copyNotes: "Copy notes",
181
+ copied: "Copied",
182
+ previous: "Previous image",
183
+ next: "Next image",
184
+ zoomHint: "Wheel to zoom · drag to pan · double-click for 100%",
185
+ preparing: "Preparing…",
186
+ failed: "Could not prepare this image."
187
+ },
188
+ zh: {
189
+ dialog: "图片查看器",
190
+ close: "关闭",
191
+ fit: "适应窗口",
192
+ actual: "原始大小",
193
+ download: "下载",
194
+ annotate: "标记区域",
195
+ cancelAnnotate: "取消标记",
196
+ regions: "区域备注",
197
+ regionHint: "点击图片中的位置,然后填写备注。",
198
+ note: "区域 {value}",
199
+ notePlaceholder: "描述这里需要怎样调整…",
200
+ removeNote: "删除区域备注",
201
+ hideNotes: "收起备注",
202
+ copyNotes: "复制备注",
203
+ copied: "已复制",
204
+ previous: "上一张图片",
205
+ next: "下一张图片",
206
+ zoomHint: "滚轮缩放 · 拖动查看 · 双击切换原始大小",
207
+ preparing: "正在准备…",
208
+ failed: "暂时无法准备这张图片。"
209
+ }
210
+ };
211
+ const fill = (value, variables) => Object.entries(variables).reduce((text, [key, replacement]) => text.replaceAll(`{${key}}`, String(replacement)), value);
212
+ const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
213
+ const bytesLabel = (bytes) => bytes === void 0 ? void 0 : bytes < 1024 * 1024 ? `${Math.max(.1, bytes / 1024).toLocaleString(void 0, { maximumFractionDigits: 1 })} KB` : `${(bytes / 1024 / 1024).toLocaleString(void 0, { maximumFractionDigits: 1 })} MB`;
214
+ const downloadName = (name) => {
215
+ const cleaned = String(name || "image.png").replace(/[<>:"/\\|?*\u0000-\u001f]/gu, "-").replace(/[. ]+$/u, "").trim();
216
+ return cleaned === "" ? "image.png" : cleaned;
217
+ };
218
+ const noteText = (annotations, t) => annotations.map((annotation, index) => {
219
+ return `${fill(t("note"), { value: index + 1 })} (${Math.round(annotation.x * 100)}%, ${Math.round(annotation.y * 100)}%): ${annotation.note.trim()}`;
220
+ }).filter((line) => !line.endsWith(": ")).join("\n");
221
+ function ViewerAction({ action, annotations, item, service }) {
222
+ const [state, setState] = (0, react.useState)("idle");
223
+ const invoke = async () => {
224
+ if (state === "pending") return;
225
+ setState("pending");
226
+ try {
227
+ await action.onInvoke({
228
+ annotations,
229
+ item,
230
+ src: item.src
231
+ });
232
+ setState("idle");
233
+ if (action.closeOnSuccess) service.close();
234
+ } catch {
235
+ setState("failed");
236
+ }
237
+ };
238
+ const label = state === "pending" ? action.pendingLabel : state === "failed" ? action.errorLabel : action.label;
239
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
240
+ type: "button",
241
+ className: "niv-button",
242
+ disabled: state === "pending",
243
+ onClick: () => {
244
+ invoke();
245
+ },
246
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
247
+ className: "niv-label",
248
+ children: label
249
+ })
250
+ });
251
+ }
252
+ function ViewerDownload({ download, item, t }) {
253
+ const [state, setState] = (0, react.useState)("idle");
254
+ const invoke = async () => {
255
+ if (state === "pending") return;
256
+ setState("pending");
257
+ try {
258
+ await download.onInvoke({
259
+ item,
260
+ src: item.src
261
+ });
262
+ setState("idle");
263
+ } catch {
264
+ setState("failed");
265
+ }
266
+ };
267
+ const label = state === "pending" ? download.pendingLabel ?? t("preparing") : state === "failed" ? download.errorLabel ?? t("failed") : t("download");
268
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
269
+ type: "button",
270
+ className: "niv-download",
271
+ disabled: state === "pending",
272
+ onClick: () => {
273
+ invoke();
274
+ },
275
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutline16, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
276
+ className: "niv-label",
277
+ children: label
278
+ })]
279
+ });
280
+ }
281
+ function ViewerOverlay({ service, t }) {
282
+ const request = (0, react.useSyncExternalStore)(service.subscribe, service.getSnapshot);
283
+ const [index, setIndex] = (0, react.useState)(0);
284
+ const [transform, setTransform] = (0, react.useState)({
285
+ zoom: 1,
286
+ x: 0,
287
+ y: 0
288
+ });
289
+ const [dragging, setDragging] = (0, react.useState)(false);
290
+ const [annotating, setAnnotating] = (0, react.useState)(false);
291
+ const [annotationsByImage, setAnnotationsByImage] = (0, react.useState)(service.getAnnotationsSnapshot);
292
+ const annotationsByImageRef = (0, react.useRef)(annotationsByImage);
293
+ const [selected, setSelected] = (0, react.useState)();
294
+ const [focusNote, setFocusNote] = (0, react.useState)();
295
+ const [copied, setCopied] = (0, react.useState)(false);
296
+ const rootRef = (0, react.useRef)(null);
297
+ const stageRef = (0, react.useRef)(null);
298
+ const surfaceRef = (0, react.useRef)(null);
299
+ const imageRef = (0, react.useRef)(null);
300
+ const pointersRef = (0, react.useRef)(/* @__PURE__ */ new Map());
301
+ const gestureRef = (0, react.useRef)();
302
+ const transformRef = (0, react.useRef)(transform);
303
+ transformRef.current = transform;
304
+ annotationsByImageRef.current = annotationsByImage;
305
+ (0, react.useEffect)(() => {
306
+ if (request === void 0) return;
307
+ setIndex(request.index);
308
+ setTransform({
309
+ zoom: 1,
310
+ x: 0,
311
+ y: 0
312
+ });
313
+ setDragging(false);
314
+ setAnnotating(false);
315
+ setSelected(void 0);
316
+ setCopied(false);
317
+ }, [request?.revision]);
318
+ const item = request?.items[index];
319
+ const annotations = item === void 0 ? [] : annotationsByImage[item.id] ?? [];
320
+ const setAnnotations = (0, react.useCallback)((update) => {
321
+ if (item === void 0) return;
322
+ const previous = annotationsByImageRef.current[item.id] ?? [];
323
+ const next = typeof update === "function" ? update(previous) : update;
324
+ const snapshot = {
325
+ ...annotationsByImageRef.current,
326
+ [item.id]: next
327
+ };
328
+ annotationsByImageRef.current = snapshot;
329
+ service.setAnnotations(item.id, next);
330
+ setAnnotationsByImage(snapshot);
331
+ }, [item?.id, service]);
332
+ const boundedPan = (0, react.useCallback)((zoom, x, y) => {
333
+ const stage = stageRef.current;
334
+ const surface = surfaceRef.current;
335
+ if (stage === null || surface === null || zoom <= 1) return {
336
+ x: 0,
337
+ y: 0
338
+ };
339
+ const limitX = Math.max(0, (surface.offsetWidth * zoom - stage.clientWidth) / 2) + 28;
340
+ const limitY = Math.max(0, (surface.offsetHeight * zoom - stage.clientHeight) / 2) + 28;
341
+ return {
342
+ x: clamp(x, -limitX, limitX),
343
+ y: clamp(y, -limitY, limitY)
344
+ };
345
+ }, []);
346
+ const setZoomAt = (0, react.useCallback)((nextZoom, clientX, clientY) => {
347
+ const stage = stageRef.current;
348
+ if (stage === null) return;
349
+ setTransform((current) => {
350
+ const next = clamp(nextZoom, .5, 8);
351
+ const box = stage.getBoundingClientRect();
352
+ const px = clientX - box.left - box.width / 2;
353
+ const py = clientY - box.top - box.height / 2;
354
+ const ratio = next / current.zoom;
355
+ return {
356
+ zoom: next,
357
+ ...boundedPan(next, px - (px - current.x) * ratio, py - (py - current.y) * ratio)
358
+ };
359
+ });
360
+ }, [boundedPan]);
361
+ const fit = (0, react.useCallback)(() => {
362
+ setTransform({
363
+ zoom: 1,
364
+ x: 0,
365
+ y: 0
366
+ });
367
+ }, []);
368
+ const actual = (0, react.useCallback)(() => {
369
+ const image = imageRef.current;
370
+ const surface = surfaceRef.current;
371
+ if (image === null || surface === null || image.naturalWidth === 0) return;
372
+ const zoom = clamp(image.naturalWidth / Math.max(1, surface.offsetWidth), 1, 8);
373
+ setTransform({
374
+ zoom,
375
+ x: 0,
376
+ y: 0
377
+ });
378
+ }, []);
379
+ (0, react.useEffect)(() => {
380
+ if (request === void 0) return void 0;
381
+ const previousOverflow = document.body.style.overflow;
382
+ document.body.style.overflow = "hidden";
383
+ rootRef.current?.focus();
384
+ const onKeyDown = (event) => {
385
+ if (event.key === "Escape") {
386
+ event.preventDefault();
387
+ if (event.target instanceof Element && event.target.closest(".niv-inline-note") !== null) {
388
+ setSelected(void 0);
389
+ return;
390
+ }
391
+ service.close();
392
+ return;
393
+ }
394
+ const editing = event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement;
395
+ if (!editing && event.key === "ArrowLeft" && request.items.length > 1) {
396
+ event.preventDefault();
397
+ setIndex((value) => (value - 1 + request.items.length) % request.items.length);
398
+ } else if (!editing && event.key === "ArrowRight" && request.items.length > 1) {
399
+ event.preventDefault();
400
+ setIndex((value) => (value + 1) % request.items.length);
401
+ } else if (!editing && (event.key === "+" || event.key === "=")) {
402
+ event.preventDefault();
403
+ const box = stageRef.current?.getBoundingClientRect();
404
+ if (box) setZoomAt(transformRef.current.zoom * 1.2, box.left + box.width / 2, box.top + box.height / 2);
405
+ } else if (!editing && event.key === "-") {
406
+ event.preventDefault();
407
+ const box = stageRef.current?.getBoundingClientRect();
408
+ if (box) setZoomAt(transformRef.current.zoom / 1.2, box.left + box.width / 2, box.top + box.height / 2);
409
+ } else if (!editing && event.key.toLowerCase() === "f") {
410
+ event.preventDefault();
411
+ fit();
412
+ } else if (event.key === "Tab") {
413
+ const controls = [...rootRef.current.querySelectorAll("button:not(:disabled),a[href],input:not(:disabled),textarea:not(:disabled),[tabindex]:not([tabindex=\"-1\"])")];
414
+ const first = controls[0];
415
+ const last = controls.at(-1);
416
+ if (event.shiftKey && document.activeElement === first) {
417
+ event.preventDefault();
418
+ last?.focus();
419
+ } else if (!event.shiftKey && document.activeElement === last) {
420
+ event.preventDefault();
421
+ first?.focus();
422
+ }
423
+ }
424
+ };
425
+ document.addEventListener("keydown", onKeyDown);
426
+ return () => {
427
+ document.body.style.overflow = previousOverflow;
428
+ document.removeEventListener("keydown", onKeyDown);
429
+ };
430
+ }, [
431
+ request,
432
+ service,
433
+ fit,
434
+ setZoomAt
435
+ ]);
436
+ (0, react.useEffect)(() => {
437
+ if (focusNote === void 0) return;
438
+ (rootRef.current?.querySelector(`[data-note-id="${CSS.escape(focusNote)}"] textarea`))?.focus();
439
+ setFocusNote(void 0);
440
+ }, [
441
+ focusNote,
442
+ selected,
443
+ annotations.length
444
+ ]);
445
+ (0, react.useEffect)(() => {
446
+ setTransform({
447
+ zoom: 1,
448
+ x: 0,
449
+ y: 0
450
+ });
451
+ setDragging(false);
452
+ setAnnotating(false);
453
+ setSelected(void 0);
454
+ }, [item?.id]);
455
+ const onWheel = (0, react.useCallback)((event) => {
456
+ event.preventDefault();
457
+ setZoomAt(transformRef.current.zoom * Math.exp(-event.deltaY * .0015), event.clientX, event.clientY);
458
+ }, [setZoomAt]);
459
+ (0, react.useEffect)(() => {
460
+ const stage = stageRef.current;
461
+ if (stage === null || request === void 0) return void 0;
462
+ stage.addEventListener("wheel", onWheel, { passive: false });
463
+ return () => stage.removeEventListener("wheel", onWheel);
464
+ }, [onWheel, request]);
465
+ const onPointerDown = (event) => {
466
+ if (event.button !== 0 || annotating) return;
467
+ if (event.target instanceof Element && event.target.closest("button,a,input,textarea,select")) return;
468
+ if (event.pointerType === "mouse" && transform.zoom <= 1) return;
469
+ event.currentTarget.setPointerCapture(event.pointerId);
470
+ pointersRef.current.set(event.pointerId, {
471
+ x: event.clientX,
472
+ y: event.clientY
473
+ });
474
+ if (pointersRef.current.size === 2) {
475
+ const [a, b] = [...pointersRef.current.values()];
476
+ gestureRef.current = {
477
+ kind: "pinch",
478
+ distance: Math.hypot(a.x - b.x, a.y - b.y),
479
+ transform
480
+ };
481
+ } else if (!annotating && transform.zoom > 1) {
482
+ gestureRef.current = {
483
+ kind: "pan",
484
+ x: event.clientX,
485
+ y: event.clientY,
486
+ transform
487
+ };
488
+ setDragging(true);
489
+ }
490
+ };
491
+ const onPointerMove = (event) => {
492
+ if (!pointersRef.current.has(event.pointerId)) return;
493
+ pointersRef.current.set(event.pointerId, {
494
+ x: event.clientX,
495
+ y: event.clientY
496
+ });
497
+ const gesture = gestureRef.current;
498
+ if (gesture?.kind === "pinch" && pointersRef.current.size >= 2) {
499
+ const [a, b] = [...pointersRef.current.values()];
500
+ const distance = Math.max(1, Math.hypot(a.x - b.x, a.y - b.y));
501
+ setZoomAt(gesture.transform.zoom * distance / Math.max(1, gesture.distance), (a.x + b.x) / 2, (a.y + b.y) / 2);
502
+ } else if (gesture?.kind === "pan") {
503
+ const pan = boundedPan(gesture.transform.zoom, gesture.transform.x + event.clientX - gesture.x, gesture.transform.y + event.clientY - gesture.y);
504
+ setTransform({
505
+ zoom: gesture.transform.zoom,
506
+ ...pan
507
+ });
508
+ }
509
+ };
510
+ const endPointer = (event) => {
511
+ pointersRef.current.delete(event.pointerId);
512
+ if (pointersRef.current.size === 0) {
513
+ gestureRef.current = void 0;
514
+ setDragging(false);
515
+ }
516
+ };
517
+ const addAnnotation = (event) => {
518
+ if (!annotating || event.target.closest(".niv-annotation")) return;
519
+ const bounds = surfaceRef.current?.getBoundingClientRect();
520
+ if (bounds === void 0) return;
521
+ const annotation = {
522
+ id: crypto.randomUUID(),
523
+ x: clamp((event.clientX - bounds.left) / bounds.width, 0, 1),
524
+ y: clamp((event.clientY - bounds.top) / bounds.height, 0, 1),
525
+ note: ""
526
+ };
527
+ setAnnotations((current) => [...current, annotation]);
528
+ setAnnotating(false);
529
+ setSelected(annotation.id);
530
+ setFocusNote(annotation.id);
531
+ };
532
+ const copyNotes = async () => {
533
+ const text = noteText(annotations, t);
534
+ if (text === "") return;
535
+ await navigator.clipboard.writeText(text);
536
+ setCopied(true);
537
+ window.setTimeout(() => {
538
+ setCopied(false);
539
+ }, 1200);
540
+ };
541
+ if (request === void 0 || item === void 0) return null;
542
+ const meta = [item.width && item.height ? `${item.width} × ${item.height}` : void 0, bytesLabel(item.bytes)].filter(Boolean).join(" · ");
543
+ const showCounter = request.items.length > 1;
544
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
545
+ ref: rootRef,
546
+ className: "niv-root",
547
+ role: "dialog",
548
+ "aria-modal": "true",
549
+ "aria-label": t("dialog"),
550
+ tabIndex: -1,
551
+ children: [
552
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
553
+ className: "niv-title niv-sr-only",
554
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: item.name }), meta !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: meta }) : null]
555
+ }),
556
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("header", {
557
+ className: "niv-topbar",
558
+ role: "toolbar",
559
+ "aria-label": t("dialog"),
560
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
561
+ className: "niv-actions",
562
+ children: [
563
+ request.annotations ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
564
+ type: "button",
565
+ className: "niv-button",
566
+ "data-active": annotating,
567
+ "aria-label": annotating ? t("cancelAnnotate") : t("annotate"),
568
+ "aria-pressed": annotating,
569
+ onClick: () => setAnnotating((value) => !value),
570
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
571
+ className: "niv-label",
572
+ children: annotating ? t("cancelAnnotate") : t("annotate")
573
+ })]
574
+ }) : null,
575
+ annotations.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
576
+ type: "button",
577
+ className: "niv-button",
578
+ "data-active": selected !== void 0,
579
+ onClick: () => {
580
+ const first = annotations[0];
581
+ setSelected((current) => current === void 0 ? first.id : void 0);
582
+ if (selected === void 0) setFocusNote(first.id);
583
+ },
584
+ children: [
585
+ annotations.length,
586
+ " ",
587
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
588
+ className: "niv-label",
589
+ children: t("regions")
590
+ })
591
+ ]
592
+ }) : null,
593
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
594
+ type: "button",
595
+ className: "niv-button",
596
+ "aria-label": t("fit"),
597
+ onClick: fit,
598
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFullscreenOutline16, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
599
+ className: "niv-label",
600
+ children: t("fit")
601
+ })]
602
+ }),
603
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
604
+ type: "button",
605
+ className: "niv-button",
606
+ onClick: actual,
607
+ children: t("actual")
608
+ }),
609
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
610
+ className: "niv-zoom",
611
+ children: [Math.round(transform.zoom * 100), "%"]
612
+ }),
613
+ item.download === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
614
+ className: "niv-download",
615
+ href: item.src,
616
+ download: downloadName(item.name),
617
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutline16, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
618
+ className: "niv-label",
619
+ children: t("download")
620
+ })]
621
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ViewerDownload, {
622
+ download: item.download,
623
+ item,
624
+ t
625
+ }),
626
+ item.actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ViewerAction, {
627
+ action,
628
+ annotations,
629
+ item,
630
+ service
631
+ }, action.id))
632
+ ]
633
+ })
634
+ }),
635
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
636
+ type: "button",
637
+ className: "niv-close-floating",
638
+ "aria-label": t("close"),
639
+ onClick: () => service.close(),
640
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, {})
641
+ }),
642
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
643
+ className: "niv-workspace",
644
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
645
+ ref: stageRef,
646
+ className: "niv-stage",
647
+ "data-dragging": dragging,
648
+ "data-annotating": annotating,
649
+ onClick: (event) => {
650
+ if (event.target === event.currentTarget && !annotating && transform.zoom === 1) service.close();
651
+ },
652
+ onPointerDown,
653
+ onPointerMove,
654
+ onPointerUp: endPointer,
655
+ onPointerCancel: endPointer,
656
+ onDoubleClick: () => {
657
+ if (transform.zoom === 1) actual();
658
+ else fit();
659
+ },
660
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
661
+ ref: surfaceRef,
662
+ className: "niv-surface",
663
+ onClick: addAnnotation,
664
+ style: { transform: `translate3d(${transform.x}px,${transform.y}px,0) scale(${transform.zoom})` },
665
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
666
+ ref: imageRef,
667
+ className: "niv-image",
668
+ src: item.src,
669
+ alt: item.name,
670
+ draggable: "false"
671
+ }), annotations.map((annotation, position) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
672
+ className: "niv-annotation",
673
+ "data-x": annotation.x < .38 ? "right" : annotation.x > .62 ? "left" : "center",
674
+ "data-y": annotation.y < .28 ? "down" : "up",
675
+ style: {
676
+ left: `${annotation.x * 100}%`,
677
+ top: `${annotation.y * 100}%`,
678
+ transform: `translate(-50%,-50%) scale(${1 / transform.zoom})`
679
+ },
680
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
681
+ type: "button",
682
+ className: "niv-pin",
683
+ "data-active": selected === annotation.id,
684
+ "aria-label": fill(t("note"), { value: position + 1 }),
685
+ onClick: (event) => {
686
+ event.stopPropagation();
687
+ const opening = selected !== annotation.id;
688
+ setSelected(opening ? annotation.id : void 0);
689
+ if (opening) setFocusNote(annotation.id);
690
+ },
691
+ children: position + 1
692
+ }), selected === annotation.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
693
+ className: "niv-inline-note",
694
+ "data-note-id": annotation.id,
695
+ onClick: (event) => event.stopPropagation(),
696
+ children: [
697
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
698
+ className: "niv-inline-index",
699
+ children: position + 1
700
+ }),
701
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
702
+ value: annotation.note,
703
+ rows: 1,
704
+ "aria-label": fill(t("note"), { value: position + 1 }),
705
+ placeholder: t("notePlaceholder"),
706
+ onChange: (event) => {
707
+ const note = event.target.value;
708
+ setAnnotations((current) => current.map((entry) => entry.id === annotation.id ? {
709
+ ...entry,
710
+ note
711
+ } : entry));
712
+ },
713
+ onKeyDown: (event) => {
714
+ if (event.key === "Enter" && !event.shiftKey || event.key === "Escape") {
715
+ event.preventDefault();
716
+ event.stopPropagation();
717
+ event.nativeEvent?.stopImmediatePropagation?.();
718
+ setSelected(void 0);
719
+ }
720
+ }
721
+ }),
722
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
723
+ type: "button",
724
+ className: "niv-note-remove",
725
+ "aria-label": t("removeNote"),
726
+ onClick: (event) => {
727
+ event.stopPropagation();
728
+ setAnnotations((current) => current.filter((entry) => entry.id !== annotation.id));
729
+ setSelected(void 0);
730
+ },
731
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, {})
732
+ })
733
+ ]
734
+ }) : null]
735
+ }, annotation.id))]
736
+ }), showCounter ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
737
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
738
+ type: "button",
739
+ className: "niv-button niv-icon-only niv-nav niv-prev",
740
+ "aria-label": t("previous"),
741
+ onClick: (event) => {
742
+ event.stopPropagation();
743
+ setIndex((value) => (value - 1 + request.items.length) % request.items.length);
744
+ },
745
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronLeftOutline14, {})
746
+ }),
747
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
748
+ type: "button",
749
+ className: "niv-button niv-icon-only niv-nav niv-next",
750
+ "aria-label": t("next"),
751
+ onClick: (event) => {
752
+ event.stopPropagation();
753
+ setIndex((value) => (value + 1) % request.items.length);
754
+ },
755
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, {})
756
+ }),
757
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
758
+ className: "niv-counter",
759
+ children: [
760
+ index + 1,
761
+ " / ",
762
+ request.items.length
763
+ ]
764
+ })
765
+ ] }) : annotating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
766
+ className: "niv-hint",
767
+ children: t("regionHint")
768
+ }) : transform.zoom === 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
769
+ className: "niv-hint",
770
+ children: t("zoomHint")
771
+ }) : null]
772
+ }), annotations.some((annotation) => annotation.note.trim() !== "") ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
773
+ type: "button",
774
+ className: "niv-copy-notes",
775
+ onClick: () => {
776
+ copyNotes();
777
+ },
778
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutline16, {}), copied ? t("copied") : t("copyNotes")]
779
+ }) : null]
780
+ })
781
+ ]
782
+ });
783
+ }
784
+ function installOfficialImageBridge(service) {
785
+ const onClick = (event) => {
786
+ const match = nativeImageButton(event.target);
787
+ if (match === void 0) return;
788
+ const { items, index } = imageItemsForButton(match);
789
+ event.preventDefault();
790
+ event.stopPropagation();
791
+ event.stopImmediatePropagation();
792
+ service.open({
793
+ items,
794
+ index,
795
+ opener: match.button,
796
+ source: "dsh-native",
797
+ annotations: true
798
+ });
799
+ };
800
+ document.addEventListener("click", onClick, true);
801
+ return () => {
802
+ document.removeEventListener("click", onClick, true);
803
+ };
804
+ }
805
+ function apply(ctx) {
806
+ const service = new NativeImageViewerService();
807
+ ctx.effect(() => ctx.locale.register(name, LOCALES), `${name}: dictionaries`);
808
+ ctx.effect(() => {
809
+ const style = document.createElement("style");
810
+ style.dataset.plugin = name;
811
+ style.textContent = CSS$1;
812
+ document.head.appendChild(style);
813
+ return () => {
814
+ style.remove();
815
+ };
816
+ }, `${name}: styles`);
817
+ ctx.effect(() => {
818
+ const disposeService = ctx.reflect.provide("nativeImageViewer", service);
819
+ const disposeBridge = installOfficialImageBridge(service);
820
+ return () => {
821
+ disposeBridge();
822
+ disposeService();
823
+ };
824
+ }, `${name}: optional viewer service and native image bridge`);
825
+ const t = ctx.locale.bind(name);
826
+ ctx.slots.inject("shell.overlay", () => ctx.slots.register({
827
+ name: "shell.overlay",
828
+ id: name,
829
+ order: 20,
830
+ inject: () => ({
831
+ service,
832
+ t
833
+ })
834
+ }, ViewerOverlay));
835
+ }
836
+ //#endregion
837
+ exports.NativeImageViewerService = NativeImageViewerService;
838
+ exports.apply = apply;
839
+ exports.inject = inject;
840
+ exports.name = name;
841
+ exports.normalizeViewerRequest = normalizeViewerRequest;
842
+ return module.exports;
843
+ }
844
+ });
845
+
846
+ //# sourceMappingURL=client.js.map