dsh-image-viewer 0.1.0-beta.3
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/AGENTS.md +40 -0
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/README.zh-CN.md +76 -0
- package/SECURITY.md +3 -0
- package/THIRD_PARTY_NOTICES.md +3 -0
- package/compatibility.json +4 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +801 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +5 -0
- package/package.json +105 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Agent installation guide
|
|
2
|
+
|
|
3
|
+
Use this guide only when the user asks to install, update, verify, or remove `dsh-image-viewer` in a selected DeepSeek Harness Web profile.
|
|
4
|
+
|
|
5
|
+
## Safety
|
|
6
|
+
|
|
7
|
+
- Confirm the target DSH installation and profile.
|
|
8
|
+
- Install the exact requested version; do not install a moving branch.
|
|
9
|
+
- Preserve conversations, attachments, unrelated plugins, settings, and credentials.
|
|
10
|
+
- Do not start, stop, or restart DSH without permission.
|
|
11
|
+
- Never copy, upload, or expose the user's images as part of installation verification.
|
|
12
|
+
|
|
13
|
+
## Install or update
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
dsh plugin --profile web add dsh-image-viewer@0.1.0-beta.3
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
DSH-Portable exposes the same standard `dsh plugin` command. Do not use a private executable path in public instructions.
|
|
20
|
+
|
|
21
|
+
## Verify
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
dsh plugin --profile web list dsh-image-viewer --depth 0
|
|
25
|
+
dsh --profile web --dump-config
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Confirm the package and `wsl043-native-image-viewer` bundle each appear exactly once. With permission to restart DSH, use a non-sensitive local test image to verify message and composer entry points, pointer-centered wheel zoom, drag pan, download, Escape, focus restoration, and the collapsed region-note sidebar.
|
|
29
|
+
|
|
30
|
+
## Uninstall
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
dsh plugin --profile web remove dsh-image-viewer
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Verify the package and bundle row are absent. Uninstall must preserve images, sessions, credentials, settings, and unrelated plugins.
|
|
37
|
+
|
|
38
|
+
## Failure handling
|
|
39
|
+
|
|
40
|
+
Separate package installation, DSH composition, unsupported image markup, browser capability, and file-download failures. Unsupported image markup must retain DSH's built-in behavior. Do not patch installed DSH files, disable browser security, or upload a user's image to reproduce a viewer issue.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WSL043
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# DSH Image Viewer
|
|
4
|
+
|
|
5
|
+
A compact, provider-neutral image viewer for DeepSeek Harness. It upgrades images already shown by DSH without replacing the conversation, attachment, or model workflows.
|
|
6
|
+
|
|
7
|
+
[](https://github.com/WSL043/dsh-image-viewer/actions/workflows/ci.yml)
|
|
8
|
+
[](https://www.npmjs.com/package/dsh-image-viewer)
|
|
9
|
+
[](https://www.npmjs.com/package/dsh-image-viewer)
|
|
10
|
+
[](#install)
|
|
11
|
+
[](LICENSE)
|
|
12
|
+
[](https://github.com/WSL043/dsh-image-viewer/stargazers)
|
|
13
|
+
|
|
14
|
+
[Install](#install) · [Privacy](#privacy) · [简体中文](README.zh-CN.md)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
> Beta: the viewer targets the latest public DSH release. Unsupported image markup is left untouched instead of being guessed.
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<img src="https://raw.githubusercontent.com/WSL043/dsh-image-viewer/main/docs/assets/image-viewer-dark.png" width="900" alt="DSH Image Viewer with fit, original-size, zoom, and download controls">
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="https://raw.githubusercontent.com/WSL043/dsh-image-viewer/main/docs/assets/image-viewer-note.png" width="900" alt="A numbered region note edited directly beside its image marker">
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
- Wheel zoom centered on the pointer, drag-to-pan, touch pinch, and double-click 100% view.
|
|
31
|
+
- Fit and original-size controls, original-file download, keyboard navigation, and multi-image galleries.
|
|
32
|
+
- One-shot region marking with each note edited beside its numbered image marker.
|
|
33
|
+
- Light and dark themes through DSH design tokens, responsive layout, focus containment, and reduced-motion support.
|
|
34
|
+
- Optional `nativeImageViewer` client service so image-producing plugins can add their own continuation action.
|
|
35
|
+
|
|
36
|
+
DSH keeps working when the plugin is absent or cannot recognize a newer image surface. Other plugins may use the service when present, but must retain their own basic fallback.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
Install the Beta:
|
|
41
|
+
|
|
42
|
+
```powershell
|
|
43
|
+
irm 'https://github.com/WSL043/dsh-image-viewer/releases/download/v0.1.0-beta.3/install.ps1' | iex
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or use the official DSH command directly:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
dsh plugin --profile web add dsh-image-viewer@0.1.0-beta.3
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Restart DSH manually after saving active work. The installer never needs access to provider credentials or image-generation accounts.
|
|
53
|
+
|
|
54
|
+
## Update
|
|
55
|
+
|
|
56
|
+
Run the same version-pinned PowerShell helper or official `add` command for the version you want to install.
|
|
57
|
+
|
|
58
|
+
## Uninstall
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
dsh plugin --profile web remove dsh-image-viewer
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Uninstalling restores DSH's built-in image lightbox. It does not remove conversations, attachments, generated images, provider plugins, or credentials.
|
|
65
|
+
|
|
66
|
+
## Privacy
|
|
67
|
+
|
|
68
|
+
Image bytes stay in the current browser profile. The plugin reads only URLs already rendered by DSH, does not upload images, does not call a model, and does not read provider credentials. Region notes remain transient unless another plugin explicitly uses them for a user-requested action.
|
|
69
|
+
|
|
70
|
+
## Support
|
|
71
|
+
|
|
72
|
+
Open the [bug report form](https://github.com/WSL043/dsh-image-viewer/issues/new?template=bug-report.yml) with the exact plugin version, DSH version, operating system, image location (message or composer), and the action that failed. Do not include private images, credentials, or full session logs. Report sensitive security problems privately through [GitHub Security Advisories](https://github.com/WSL043/dsh-image-viewer/security/advisories/new).
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
[简体中文](README.zh-CN.md) · [Report a bug](https://github.com/WSL043/dsh-image-viewer/issues/new?template=bug-report.yml) · [Request a feature](https://github.com/WSL043/dsh-image-viewer/issues/new?template=feature-request.yml) · [MIT](LICENSE)
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# DSH 图片查看器
|
|
4
|
+
|
|
5
|
+
面向 DeepSeek Harness 的简洁、与模型和供应商无关的图片查看器。它只增强 DSH 已经显示的图片,不替换会话、附件或模型流程。
|
|
6
|
+
|
|
7
|
+
[](https://github.com/WSL043/dsh-image-viewer/actions/workflows/ci.yml)
|
|
8
|
+
[](https://www.npmjs.com/package/dsh-image-viewer)
|
|
9
|
+
[](https://www.npmjs.com/package/dsh-image-viewer)
|
|
10
|
+
[](#安装)
|
|
11
|
+
[](LICENSE)
|
|
12
|
+
[](https://github.com/WSL043/dsh-image-viewer/stargazers)
|
|
13
|
+
|
|
14
|
+
[安装](#安装) · [隐私](#隐私) · [English](README.md)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
> Beta:当前适配最新公开版 DSH。遇到无法识别的新图片结构时会保留 DSH 原生行为,不会猜测接管。
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<img src="https://raw.githubusercontent.com/WSL043/dsh-image-viewer/main/docs/assets/image-viewer-dark.png" width="900" alt="DSH 图片查看器的适应窗口、原始大小、缩放和下载操作">
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="https://raw.githubusercontent.com/WSL043/dsh-image-viewer/main/docs/assets/image-viewer-note.png" width="900" alt="直接在图片编号附近编辑区域备注">
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
## 功能
|
|
29
|
+
|
|
30
|
+
- 以鼠标位置为中心的滚轮缩放、拖动查看、触控双指缩放和双击原始大小。
|
|
31
|
+
- 适应窗口、原始大小、原图下载、键盘切换和多图浏览。
|
|
32
|
+
- 一次点击标记一个区域,备注直接显示在图片编号附近并自动获得输入焦点。
|
|
33
|
+
- 使用 DSH 设计令牌适配亮色、暗色、响应式布局、焦点约束和减少动态效果设置。
|
|
34
|
+
- 提供可选的 `nativeImageViewer` 客户端服务,图片生成插件可以按需增加自己的后续操作。
|
|
35
|
+
|
|
36
|
+
未安装本插件或插件无法识别更新后的图片界面时,DSH 仍保持原有查看能力。其他插件可以在检测到服务时使用高级查看,但仍应保留自己的基础回退。
|
|
37
|
+
|
|
38
|
+
## 安装
|
|
39
|
+
|
|
40
|
+
安装 Beta:
|
|
41
|
+
|
|
42
|
+
```powershell
|
|
43
|
+
irm 'https://github.com/WSL043/dsh-image-viewer/releases/download/v0.1.0-beta.3/install.ps1' | iex
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
也可以直接使用 DSH 官方命令:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
dsh plugin --profile web add dsh-image-viewer@0.1.0-beta.3
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
保存正在进行的工作后手动重启 DSH。本插件不需要供应商凭据或图片生成账户权限。
|
|
53
|
+
|
|
54
|
+
## 更新
|
|
55
|
+
|
|
56
|
+
使用目标版本对应的固定 PowerShell 助手或官方 `add` 命令更新。
|
|
57
|
+
|
|
58
|
+
## 卸载
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
dsh plugin --profile web remove dsh-image-viewer
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
卸载后恢复 DSH 内置图片灯箱,不会删除会话、附件、生成图片、供应商插件或凭据。
|
|
65
|
+
|
|
66
|
+
## 隐私
|
|
67
|
+
|
|
68
|
+
图片数据保留在当前浏览器配置中。插件只读取 DSH 已经渲染的图片 URL,不上传图片、不调用模型,也不读取供应商凭据。区域备注默认只在当前查看过程中存在;只有其他插件在用户主动操作时才可以使用它们。
|
|
69
|
+
|
|
70
|
+
## 反馈
|
|
71
|
+
|
|
72
|
+
请通过[问题反馈表单](https://github.com/WSL043/dsh-image-viewer/issues/new?template=bug-report.yml)提供准确的插件版本、DSH 版本、操作系统、图片所在位置(消息或输入框)以及失败的操作。不要提交私人图片、凭据或完整会话日志。敏感安全问题请通过 [GitHub Security Advisories](https://github.com/WSL043/dsh-image-viewer/security/advisories/new) 私下报告。
|
|
73
|
+
|
|
74
|
+
## 许可证
|
|
75
|
+
|
|
76
|
+
[English](README.md) · [反馈问题](https://github.com/WSL043/dsh-image-viewer/issues/new?template=bug-report.yml) · [功能建议](https://github.com/WSL043/dsh-image-viewer/issues/new?template=feature-request.yml) · [MIT](LICENSE)
|
package/SECURITY.md
ADDED
package/cordis.patch.yml
ADDED
package/lib/client.js
ADDED
|
@@ -0,0 +1,801 @@
|
|
|
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
|
+
function normalizeViewerRequest(request) {
|
|
13
|
+
const items = (Array.isArray(request?.items) ? request.items : []).flatMap((item, position) => {
|
|
14
|
+
if (typeof item?.src !== "string" || item.src === "") return [];
|
|
15
|
+
return [{
|
|
16
|
+
id: typeof item.id === "string" && item.id !== "" ? item.id : `image-${position + 1}`,
|
|
17
|
+
src: item.src,
|
|
18
|
+
name: typeof item.name === "string" && item.name !== "" ? item.name : `Image ${position + 1}`,
|
|
19
|
+
width: boundedNumber(item.width, void 0),
|
|
20
|
+
height: boundedNumber(item.height, void 0),
|
|
21
|
+
bytes: boundedNumber(item.bytes, void 0)
|
|
22
|
+
}];
|
|
23
|
+
});
|
|
24
|
+
if (items.length === 0) return void 0;
|
|
25
|
+
const requestedIndex = Number.isInteger(request?.index) ? request.index : 0;
|
|
26
|
+
return {
|
|
27
|
+
items,
|
|
28
|
+
index: Math.max(0, Math.min(items.length - 1, requestedIndex)),
|
|
29
|
+
opener: typeof HTMLElement !== "undefined" && request?.opener instanceof HTMLElement ? request.opener : void 0,
|
|
30
|
+
source: typeof request?.source === "string" ? request.source : "dsh",
|
|
31
|
+
annotations: request?.annotations !== false,
|
|
32
|
+
editor: request?.editor
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
var NativeImageViewerService = class {
|
|
36
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
37
|
+
#revision = 0;
|
|
38
|
+
#snapshot;
|
|
39
|
+
constructor() {
|
|
40
|
+
this.subscribe = (listener) => {
|
|
41
|
+
this.#listeners.add(listener);
|
|
42
|
+
return () => {
|
|
43
|
+
this.#listeners.delete(listener);
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
this.getSnapshot = () => this.#snapshot;
|
|
47
|
+
}
|
|
48
|
+
open(request) {
|
|
49
|
+
const normalized = normalizeViewerRequest(request);
|
|
50
|
+
if (normalized === void 0) return false;
|
|
51
|
+
this.#revision += 1;
|
|
52
|
+
this.#snapshot = {
|
|
53
|
+
...normalized,
|
|
54
|
+
revision: this.#revision
|
|
55
|
+
};
|
|
56
|
+
this.#emit();
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
close() {
|
|
60
|
+
if (this.#snapshot === void 0) return;
|
|
61
|
+
const opener = this.#snapshot.opener;
|
|
62
|
+
this.#snapshot = void 0;
|
|
63
|
+
this.#emit();
|
|
64
|
+
if (typeof window === "undefined") opener?.focus();
|
|
65
|
+
else window.requestAnimationFrame(() => {
|
|
66
|
+
opener?.focus();
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
#emit() {
|
|
70
|
+
for (const listener of this.#listeners) listener();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
function nativeImageButton(target) {
|
|
74
|
+
if (!(target instanceof Element)) return void 0;
|
|
75
|
+
const button = target.closest("button");
|
|
76
|
+
if (!(button instanceof HTMLButtonElement)) return void 0;
|
|
77
|
+
const image = button.querySelector(":scope > img");
|
|
78
|
+
if (!(image instanceof HTMLImageElement) || image.currentSrc === "") return void 0;
|
|
79
|
+
if (button.matches("[data-variant=\"single\"],[data-variant=\"tile\"]")) return {
|
|
80
|
+
button,
|
|
81
|
+
image,
|
|
82
|
+
group: button.parentElement
|
|
83
|
+
};
|
|
84
|
+
const rail = button.closest("[role=\"group\"]");
|
|
85
|
+
if (rail !== null && button.hasAttribute("title")) return {
|
|
86
|
+
button,
|
|
87
|
+
image,
|
|
88
|
+
group: rail
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function imageItemsForButton(match) {
|
|
92
|
+
const candidates = [...match.group?.querySelectorAll("button[data-variant]") ?? match.group?.querySelectorAll("button[title]") ?? []].flatMap((button, index) => {
|
|
93
|
+
const image = button.querySelector(":scope > img");
|
|
94
|
+
if (!(image instanceof HTMLImageElement) || image.currentSrc === "") return [];
|
|
95
|
+
return [{
|
|
96
|
+
id: image.currentSrc,
|
|
97
|
+
src: image.currentSrc,
|
|
98
|
+
name: image.alt || `Image ${index + 1}`,
|
|
99
|
+
width: image.naturalWidth || void 0,
|
|
100
|
+
height: image.naturalHeight || void 0,
|
|
101
|
+
button
|
|
102
|
+
}];
|
|
103
|
+
});
|
|
104
|
+
const items = candidates.length > 0 ? candidates : [{
|
|
105
|
+
id: match.image.currentSrc,
|
|
106
|
+
src: match.image.currentSrc,
|
|
107
|
+
name: match.image.alt || "Image",
|
|
108
|
+
width: match.image.naturalWidth || void 0,
|
|
109
|
+
height: match.image.naturalHeight || void 0,
|
|
110
|
+
button: match.button
|
|
111
|
+
}];
|
|
112
|
+
const index = Math.max(0, items.findIndex((item) => item.button === match.button));
|
|
113
|
+
return {
|
|
114
|
+
items: items.map(({ button: _button, ...item }) => item),
|
|
115
|
+
index
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/styles.js
|
|
120
|
+
const CSS$1 = String.raw`
|
|
121
|
+
.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)}
|
|
122
|
+
.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}
|
|
123
|
+
.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)}
|
|
124
|
+
.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{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}
|
|
125
|
+
.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)}
|
|
126
|
+
.niv-workspace{position:absolute;inset:0;display:grid;min-width:0;min-height:0;padding:68px 24px 72px;box-sizing:border-box}
|
|
127
|
+
.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}
|
|
128
|
+
.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}
|
|
129
|
+
.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)}
|
|
130
|
+
.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}
|
|
131
|
+
.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)}
|
|
132
|
+
.niv-hint{display:none}
|
|
133
|
+
.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)}
|
|
134
|
+
.niv-editor{position:absolute;right:50%;bottom:70px;z-index:6;display:flex;width:min(620px,calc(100vw - 40px));align-items:center;gap:8px;padding:8px;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-base)) 92%,transparent);box-shadow:0 16px 44px rgba(0,0,0,.32);color:var(--dsw-alias-label-primary);transform:translateX(50%);backdrop-filter:blur(20px)}.niv-editor input{min-width:0;flex:1}.niv-editor-error{color:var(--dsw-alias-state-error-primary);font-size:11px}.niv-workspace[data-editor=true]{padding-bottom:132px}
|
|
135
|
+
@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}.niv-editor{bottom:60px;align-items:stretch;flex-direction:column}.niv-editor button{width:100%}.niv-workspace[data-editor=true]{padding-bottom:176px}}
|
|
136
|
+
@media(prefers-reduced-motion:reduce){.niv-surface{transition:none}}
|
|
137
|
+
`;
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/client.jsx
|
|
140
|
+
const name = "dsh-image-viewer";
|
|
141
|
+
const inject = ["locale", "slots"];
|
|
142
|
+
const LOCALES = {
|
|
143
|
+
en: {
|
|
144
|
+
dialog: "Image viewer",
|
|
145
|
+
close: "Close",
|
|
146
|
+
fit: "Fit",
|
|
147
|
+
actual: "100%",
|
|
148
|
+
download: "Download",
|
|
149
|
+
annotate: "Mark region",
|
|
150
|
+
cancelAnnotate: "Cancel marking",
|
|
151
|
+
regions: "Region notes",
|
|
152
|
+
regionHint: "Click a point on the image, then add a note.",
|
|
153
|
+
note: "Region {value}",
|
|
154
|
+
notePlaceholder: "Describe what should change here…",
|
|
155
|
+
removeNote: "Remove region note",
|
|
156
|
+
hideNotes: "Hide notes",
|
|
157
|
+
copyNotes: "Copy notes",
|
|
158
|
+
copied: "Copied",
|
|
159
|
+
previous: "Previous image",
|
|
160
|
+
next: "Next image",
|
|
161
|
+
zoomHint: "Wheel to zoom · drag to pan · double-click for 100%",
|
|
162
|
+
preparing: "Preparing…",
|
|
163
|
+
failed: "Could not prepare this image."
|
|
164
|
+
},
|
|
165
|
+
zh: {
|
|
166
|
+
dialog: "图片查看器",
|
|
167
|
+
close: "关闭",
|
|
168
|
+
fit: "适应窗口",
|
|
169
|
+
actual: "原始大小",
|
|
170
|
+
download: "下载",
|
|
171
|
+
annotate: "标记区域",
|
|
172
|
+
cancelAnnotate: "取消标记",
|
|
173
|
+
regions: "区域备注",
|
|
174
|
+
regionHint: "点击图片中的位置,然后填写备注。",
|
|
175
|
+
note: "区域 {value}",
|
|
176
|
+
notePlaceholder: "描述这里需要怎样调整…",
|
|
177
|
+
removeNote: "删除区域备注",
|
|
178
|
+
hideNotes: "收起备注",
|
|
179
|
+
copyNotes: "复制备注",
|
|
180
|
+
copied: "已复制",
|
|
181
|
+
previous: "上一张图片",
|
|
182
|
+
next: "下一张图片",
|
|
183
|
+
zoomHint: "滚轮缩放 · 拖动查看 · 双击切换原始大小",
|
|
184
|
+
preparing: "正在准备…",
|
|
185
|
+
failed: "暂时无法准备这张图片。"
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const fill = (value, variables) => Object.entries(variables).reduce((text, [key, replacement]) => text.replaceAll(`{${key}}`, String(replacement)), value);
|
|
189
|
+
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
190
|
+
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`;
|
|
191
|
+
const downloadName = (name) => {
|
|
192
|
+
const cleaned = String(name || "image.png").replace(/[<>:"/\\|?*\u0000-\u001f]/gu, "-").replace(/[. ]+$/u, "").trim();
|
|
193
|
+
return cleaned === "" ? "image.png" : cleaned;
|
|
194
|
+
};
|
|
195
|
+
const noteText = (annotations, t) => annotations.map((annotation, index) => {
|
|
196
|
+
return `${fill(t("note"), { value: index + 1 })} (${Math.round(annotation.x * 100)}%, ${Math.round(annotation.y * 100)}%): ${annotation.note.trim()}`;
|
|
197
|
+
}).filter((line) => !line.endsWith(": ")).join("\n");
|
|
198
|
+
function ViewerOverlay({ service, t }) {
|
|
199
|
+
const request = (0, react.useSyncExternalStore)(service.subscribe, service.getSnapshot);
|
|
200
|
+
const [index, setIndex] = (0, react.useState)(0);
|
|
201
|
+
const [transform, setTransform] = (0, react.useState)({
|
|
202
|
+
zoom: 1,
|
|
203
|
+
x: 0,
|
|
204
|
+
y: 0
|
|
205
|
+
});
|
|
206
|
+
const [dragging, setDragging] = (0, react.useState)(false);
|
|
207
|
+
const [annotating, setAnnotating] = (0, react.useState)(false);
|
|
208
|
+
const [annotationsByImage, setAnnotationsByImage] = (0, react.useState)({});
|
|
209
|
+
const [selected, setSelected] = (0, react.useState)();
|
|
210
|
+
const [focusNote, setFocusNote] = (0, react.useState)();
|
|
211
|
+
const [prompt, setPrompt] = (0, react.useState)("");
|
|
212
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
213
|
+
const [editorError, setEditorError] = (0, react.useState)(false);
|
|
214
|
+
const [copied, setCopied] = (0, react.useState)(false);
|
|
215
|
+
const rootRef = (0, react.useRef)(null);
|
|
216
|
+
const stageRef = (0, react.useRef)(null);
|
|
217
|
+
const surfaceRef = (0, react.useRef)(null);
|
|
218
|
+
const imageRef = (0, react.useRef)(null);
|
|
219
|
+
const pointersRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
220
|
+
const gestureRef = (0, react.useRef)();
|
|
221
|
+
const transformRef = (0, react.useRef)(transform);
|
|
222
|
+
transformRef.current = transform;
|
|
223
|
+
(0, react.useEffect)(() => {
|
|
224
|
+
if (request === void 0) return;
|
|
225
|
+
setIndex(request.index);
|
|
226
|
+
setTransform({
|
|
227
|
+
zoom: 1,
|
|
228
|
+
x: 0,
|
|
229
|
+
y: 0
|
|
230
|
+
});
|
|
231
|
+
setDragging(false);
|
|
232
|
+
setAnnotating(false);
|
|
233
|
+
setAnnotationsByImage({});
|
|
234
|
+
setSelected(void 0);
|
|
235
|
+
setPrompt("");
|
|
236
|
+
setBusy(false);
|
|
237
|
+
setEditorError(false);
|
|
238
|
+
setCopied(false);
|
|
239
|
+
}, [request?.revision]);
|
|
240
|
+
const item = request?.items[index];
|
|
241
|
+
const annotations = item === void 0 ? [] : annotationsByImage[item.id] ?? [];
|
|
242
|
+
const setAnnotations = (0, react.useCallback)((update) => {
|
|
243
|
+
if (item === void 0) return;
|
|
244
|
+
setAnnotationsByImage((current) => {
|
|
245
|
+
const previous = current[item.id] ?? [];
|
|
246
|
+
const next = typeof update === "function" ? update(previous) : update;
|
|
247
|
+
return {
|
|
248
|
+
...current,
|
|
249
|
+
[item.id]: next
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
}, [item?.id]);
|
|
253
|
+
const boundedPan = (0, react.useCallback)((zoom, x, y) => {
|
|
254
|
+
const stage = stageRef.current;
|
|
255
|
+
const surface = surfaceRef.current;
|
|
256
|
+
if (stage === null || surface === null || zoom <= 1) return {
|
|
257
|
+
x: 0,
|
|
258
|
+
y: 0
|
|
259
|
+
};
|
|
260
|
+
const limitX = Math.max(0, (surface.offsetWidth * zoom - stage.clientWidth) / 2) + 28;
|
|
261
|
+
const limitY = Math.max(0, (surface.offsetHeight * zoom - stage.clientHeight) / 2) + 28;
|
|
262
|
+
return {
|
|
263
|
+
x: clamp(x, -limitX, limitX),
|
|
264
|
+
y: clamp(y, -limitY, limitY)
|
|
265
|
+
};
|
|
266
|
+
}, []);
|
|
267
|
+
const setZoomAt = (0, react.useCallback)((nextZoom, clientX, clientY) => {
|
|
268
|
+
const stage = stageRef.current;
|
|
269
|
+
if (stage === null) return;
|
|
270
|
+
setTransform((current) => {
|
|
271
|
+
const next = clamp(nextZoom, .5, 8);
|
|
272
|
+
const box = stage.getBoundingClientRect();
|
|
273
|
+
const px = clientX - box.left - box.width / 2;
|
|
274
|
+
const py = clientY - box.top - box.height / 2;
|
|
275
|
+
const ratio = next / current.zoom;
|
|
276
|
+
return {
|
|
277
|
+
zoom: next,
|
|
278
|
+
...boundedPan(next, px - (px - current.x) * ratio, py - (py - current.y) * ratio)
|
|
279
|
+
};
|
|
280
|
+
});
|
|
281
|
+
}, [boundedPan]);
|
|
282
|
+
const fit = (0, react.useCallback)(() => {
|
|
283
|
+
setTransform({
|
|
284
|
+
zoom: 1,
|
|
285
|
+
x: 0,
|
|
286
|
+
y: 0
|
|
287
|
+
});
|
|
288
|
+
}, []);
|
|
289
|
+
const actual = (0, react.useCallback)(() => {
|
|
290
|
+
const image = imageRef.current;
|
|
291
|
+
const surface = surfaceRef.current;
|
|
292
|
+
if (image === null || surface === null || image.naturalWidth === 0) return;
|
|
293
|
+
const zoom = clamp(image.naturalWidth / Math.max(1, surface.offsetWidth), 1, 8);
|
|
294
|
+
setTransform({
|
|
295
|
+
zoom,
|
|
296
|
+
x: 0,
|
|
297
|
+
y: 0
|
|
298
|
+
});
|
|
299
|
+
}, []);
|
|
300
|
+
(0, react.useEffect)(() => {
|
|
301
|
+
if (request === void 0) return void 0;
|
|
302
|
+
const previousOverflow = document.body.style.overflow;
|
|
303
|
+
document.body.style.overflow = "hidden";
|
|
304
|
+
rootRef.current?.focus();
|
|
305
|
+
const onKeyDown = (event) => {
|
|
306
|
+
if (event.key === "Escape") {
|
|
307
|
+
event.preventDefault();
|
|
308
|
+
service.close();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const editing = event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement;
|
|
312
|
+
if (!editing && event.key === "ArrowLeft" && request.items.length > 1) {
|
|
313
|
+
event.preventDefault();
|
|
314
|
+
setIndex((value) => (value - 1 + request.items.length) % request.items.length);
|
|
315
|
+
} else if (!editing && event.key === "ArrowRight" && request.items.length > 1) {
|
|
316
|
+
event.preventDefault();
|
|
317
|
+
setIndex((value) => (value + 1) % request.items.length);
|
|
318
|
+
} else if (!editing && (event.key === "+" || event.key === "=")) {
|
|
319
|
+
event.preventDefault();
|
|
320
|
+
const box = stageRef.current?.getBoundingClientRect();
|
|
321
|
+
if (box) setZoomAt(transformRef.current.zoom * 1.2, box.left + box.width / 2, box.top + box.height / 2);
|
|
322
|
+
} else if (!editing && event.key === "-") {
|
|
323
|
+
event.preventDefault();
|
|
324
|
+
const box = stageRef.current?.getBoundingClientRect();
|
|
325
|
+
if (box) setZoomAt(transformRef.current.zoom / 1.2, box.left + box.width / 2, box.top + box.height / 2);
|
|
326
|
+
} else if (!editing && event.key.toLowerCase() === "f") {
|
|
327
|
+
event.preventDefault();
|
|
328
|
+
fit();
|
|
329
|
+
} else if (event.key === "Tab") {
|
|
330
|
+
const controls = [...rootRef.current.querySelectorAll("button:not(:disabled),a[href],input:not(:disabled),textarea:not(:disabled),[tabindex]:not([tabindex=\"-1\"])")];
|
|
331
|
+
const first = controls[0];
|
|
332
|
+
const last = controls.at(-1);
|
|
333
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
334
|
+
event.preventDefault();
|
|
335
|
+
last?.focus();
|
|
336
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
337
|
+
event.preventDefault();
|
|
338
|
+
first?.focus();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
document.addEventListener("keydown", onKeyDown);
|
|
343
|
+
return () => {
|
|
344
|
+
document.body.style.overflow = previousOverflow;
|
|
345
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
346
|
+
};
|
|
347
|
+
}, [
|
|
348
|
+
request,
|
|
349
|
+
service,
|
|
350
|
+
fit,
|
|
351
|
+
setZoomAt
|
|
352
|
+
]);
|
|
353
|
+
(0, react.useEffect)(() => {
|
|
354
|
+
if (focusNote === void 0) return;
|
|
355
|
+
(rootRef.current?.querySelector(`[data-note-id="${CSS.escape(focusNote)}"] textarea`))?.focus();
|
|
356
|
+
setFocusNote(void 0);
|
|
357
|
+
}, [
|
|
358
|
+
focusNote,
|
|
359
|
+
selected,
|
|
360
|
+
annotations.length
|
|
361
|
+
]);
|
|
362
|
+
(0, react.useEffect)(() => {
|
|
363
|
+
setTransform({
|
|
364
|
+
zoom: 1,
|
|
365
|
+
x: 0,
|
|
366
|
+
y: 0
|
|
367
|
+
});
|
|
368
|
+
setDragging(false);
|
|
369
|
+
setAnnotating(false);
|
|
370
|
+
setSelected(void 0);
|
|
371
|
+
setEditorError(false);
|
|
372
|
+
}, [item?.id]);
|
|
373
|
+
const onWheel = (0, react.useCallback)((event) => {
|
|
374
|
+
event.preventDefault();
|
|
375
|
+
setZoomAt(transformRef.current.zoom * Math.exp(-event.deltaY * .0015), event.clientX, event.clientY);
|
|
376
|
+
}, [setZoomAt]);
|
|
377
|
+
(0, react.useEffect)(() => {
|
|
378
|
+
const stage = stageRef.current;
|
|
379
|
+
if (stage === null || request === void 0) return void 0;
|
|
380
|
+
stage.addEventListener("wheel", onWheel, { passive: false });
|
|
381
|
+
return () => stage.removeEventListener("wheel", onWheel);
|
|
382
|
+
}, [onWheel, request]);
|
|
383
|
+
const onPointerDown = (event) => {
|
|
384
|
+
if (event.button !== 0 || annotating) return;
|
|
385
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
386
|
+
pointersRef.current.set(event.pointerId, {
|
|
387
|
+
x: event.clientX,
|
|
388
|
+
y: event.clientY
|
|
389
|
+
});
|
|
390
|
+
if (pointersRef.current.size === 2) {
|
|
391
|
+
const [a, b] = [...pointersRef.current.values()];
|
|
392
|
+
gestureRef.current = {
|
|
393
|
+
kind: "pinch",
|
|
394
|
+
distance: Math.hypot(a.x - b.x, a.y - b.y),
|
|
395
|
+
transform
|
|
396
|
+
};
|
|
397
|
+
} else if (!annotating && transform.zoom > 1) {
|
|
398
|
+
gestureRef.current = {
|
|
399
|
+
kind: "pan",
|
|
400
|
+
x: event.clientX,
|
|
401
|
+
y: event.clientY,
|
|
402
|
+
transform
|
|
403
|
+
};
|
|
404
|
+
setDragging(true);
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
const onPointerMove = (event) => {
|
|
408
|
+
if (!pointersRef.current.has(event.pointerId)) return;
|
|
409
|
+
pointersRef.current.set(event.pointerId, {
|
|
410
|
+
x: event.clientX,
|
|
411
|
+
y: event.clientY
|
|
412
|
+
});
|
|
413
|
+
const gesture = gestureRef.current;
|
|
414
|
+
if (gesture?.kind === "pinch" && pointersRef.current.size >= 2) {
|
|
415
|
+
const [a, b] = [...pointersRef.current.values()];
|
|
416
|
+
const distance = Math.max(1, Math.hypot(a.x - b.x, a.y - b.y));
|
|
417
|
+
setZoomAt(gesture.transform.zoom * distance / Math.max(1, gesture.distance), (a.x + b.x) / 2, (a.y + b.y) / 2);
|
|
418
|
+
} else if (gesture?.kind === "pan") {
|
|
419
|
+
const pan = boundedPan(gesture.transform.zoom, gesture.transform.x + event.clientX - gesture.x, gesture.transform.y + event.clientY - gesture.y);
|
|
420
|
+
setTransform({
|
|
421
|
+
zoom: gesture.transform.zoom,
|
|
422
|
+
...pan
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
const endPointer = (event) => {
|
|
427
|
+
pointersRef.current.delete(event.pointerId);
|
|
428
|
+
if (pointersRef.current.size === 0) {
|
|
429
|
+
gestureRef.current = void 0;
|
|
430
|
+
setDragging(false);
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
const addAnnotation = (event) => {
|
|
434
|
+
if (!annotating || event.target.closest(".niv-annotation")) return;
|
|
435
|
+
const bounds = surfaceRef.current?.getBoundingClientRect();
|
|
436
|
+
if (bounds === void 0) return;
|
|
437
|
+
const annotation = {
|
|
438
|
+
id: crypto.randomUUID(),
|
|
439
|
+
x: clamp((event.clientX - bounds.left) / bounds.width, 0, 1),
|
|
440
|
+
y: clamp((event.clientY - bounds.top) / bounds.height, 0, 1),
|
|
441
|
+
note: ""
|
|
442
|
+
};
|
|
443
|
+
setAnnotations((current) => [...current, annotation]);
|
|
444
|
+
setAnnotating(false);
|
|
445
|
+
setSelected(annotation.id);
|
|
446
|
+
setFocusNote(annotation.id);
|
|
447
|
+
};
|
|
448
|
+
const submit = async () => {
|
|
449
|
+
if (busy || request?.editor?.onSubmit === void 0 || item === void 0) return;
|
|
450
|
+
setBusy(true);
|
|
451
|
+
setEditorError(false);
|
|
452
|
+
try {
|
|
453
|
+
await request.editor.onSubmit({
|
|
454
|
+
prompt,
|
|
455
|
+
annotations,
|
|
456
|
+
item,
|
|
457
|
+
src: item.src
|
|
458
|
+
});
|
|
459
|
+
service.close();
|
|
460
|
+
} catch {
|
|
461
|
+
setEditorError(true);
|
|
462
|
+
} finally {
|
|
463
|
+
setBusy(false);
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
const copyNotes = async () => {
|
|
467
|
+
const text = noteText(annotations, t);
|
|
468
|
+
if (text === "") return;
|
|
469
|
+
await navigator.clipboard.writeText(text);
|
|
470
|
+
setCopied(true);
|
|
471
|
+
window.setTimeout(() => {
|
|
472
|
+
setCopied(false);
|
|
473
|
+
}, 1200);
|
|
474
|
+
};
|
|
475
|
+
if (request === void 0 || item === void 0) return null;
|
|
476
|
+
const meta = [item.width && item.height ? `${item.width} × ${item.height}` : void 0, bytesLabel(item.bytes)].filter(Boolean).join(" · ");
|
|
477
|
+
const showCounter = request.items.length > 1;
|
|
478
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
479
|
+
ref: rootRef,
|
|
480
|
+
className: "niv-root",
|
|
481
|
+
role: "dialog",
|
|
482
|
+
"aria-modal": "true",
|
|
483
|
+
"aria-label": t("dialog"),
|
|
484
|
+
tabIndex: -1,
|
|
485
|
+
children: [
|
|
486
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
487
|
+
className: "niv-title niv-sr-only",
|
|
488
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: item.name }), meta !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: meta }) : null]
|
|
489
|
+
}),
|
|
490
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("header", {
|
|
491
|
+
className: "niv-topbar",
|
|
492
|
+
role: "toolbar",
|
|
493
|
+
"aria-label": t("dialog"),
|
|
494
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
495
|
+
className: "niv-actions",
|
|
496
|
+
children: [
|
|
497
|
+
request.annotations ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
498
|
+
type: "button",
|
|
499
|
+
className: "niv-button",
|
|
500
|
+
"data-active": annotating,
|
|
501
|
+
"aria-label": annotating ? t("cancelAnnotate") : t("annotate"),
|
|
502
|
+
"aria-pressed": annotating,
|
|
503
|
+
onClick: () => setAnnotating((value) => !value),
|
|
504
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
505
|
+
className: "niv-label",
|
|
506
|
+
children: annotating ? t("cancelAnnotate") : t("annotate")
|
|
507
|
+
})]
|
|
508
|
+
}) : null,
|
|
509
|
+
annotations.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
510
|
+
type: "button",
|
|
511
|
+
className: "niv-button",
|
|
512
|
+
"data-active": selected !== void 0,
|
|
513
|
+
onClick: () => {
|
|
514
|
+
const first = annotations[0];
|
|
515
|
+
setSelected((current) => current === void 0 ? first.id : void 0);
|
|
516
|
+
if (selected === void 0) setFocusNote(first.id);
|
|
517
|
+
},
|
|
518
|
+
children: [
|
|
519
|
+
annotations.length,
|
|
520
|
+
" ",
|
|
521
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
522
|
+
className: "niv-label",
|
|
523
|
+
children: t("regions")
|
|
524
|
+
})
|
|
525
|
+
]
|
|
526
|
+
}) : null,
|
|
527
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
528
|
+
type: "button",
|
|
529
|
+
className: "niv-button",
|
|
530
|
+
"aria-label": t("fit"),
|
|
531
|
+
onClick: fit,
|
|
532
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFullscreenOutline16, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
533
|
+
className: "niv-label",
|
|
534
|
+
children: t("fit")
|
|
535
|
+
})]
|
|
536
|
+
}),
|
|
537
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
538
|
+
type: "button",
|
|
539
|
+
className: "niv-button",
|
|
540
|
+
onClick: actual,
|
|
541
|
+
children: t("actual")
|
|
542
|
+
}),
|
|
543
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
544
|
+
className: "niv-zoom",
|
|
545
|
+
children: [Math.round(transform.zoom * 100), "%"]
|
|
546
|
+
}),
|
|
547
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
|
|
548
|
+
className: "niv-download",
|
|
549
|
+
href: item.src,
|
|
550
|
+
download: downloadName(item.name),
|
|
551
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutline16, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
552
|
+
className: "niv-label",
|
|
553
|
+
children: t("download")
|
|
554
|
+
})]
|
|
555
|
+
})
|
|
556
|
+
]
|
|
557
|
+
})
|
|
558
|
+
}),
|
|
559
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
560
|
+
type: "button",
|
|
561
|
+
className: "niv-close-floating",
|
|
562
|
+
"aria-label": t("close"),
|
|
563
|
+
onClick: () => service.close(),
|
|
564
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, {})
|
|
565
|
+
}),
|
|
566
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
567
|
+
className: "niv-workspace",
|
|
568
|
+
"data-editor": request.editor !== void 0,
|
|
569
|
+
children: [
|
|
570
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
|
|
571
|
+
ref: stageRef,
|
|
572
|
+
className: "niv-stage",
|
|
573
|
+
"data-dragging": dragging,
|
|
574
|
+
"data-annotating": annotating,
|
|
575
|
+
onClick: (event) => {
|
|
576
|
+
if (event.target === event.currentTarget && !annotating && transform.zoom === 1) service.close();
|
|
577
|
+
},
|
|
578
|
+
onPointerDown,
|
|
579
|
+
onPointerMove,
|
|
580
|
+
onPointerUp: endPointer,
|
|
581
|
+
onPointerCancel: endPointer,
|
|
582
|
+
onDoubleClick: () => {
|
|
583
|
+
if (transform.zoom === 1) actual();
|
|
584
|
+
else fit();
|
|
585
|
+
},
|
|
586
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
587
|
+
ref: surfaceRef,
|
|
588
|
+
className: "niv-surface",
|
|
589
|
+
onClick: addAnnotation,
|
|
590
|
+
style: { transform: `translate3d(${transform.x}px,${transform.y}px,0) scale(${transform.zoom})` },
|
|
591
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
592
|
+
ref: imageRef,
|
|
593
|
+
className: "niv-image",
|
|
594
|
+
src: item.src,
|
|
595
|
+
alt: item.name,
|
|
596
|
+
draggable: "false"
|
|
597
|
+
}), annotations.map((annotation, position) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
598
|
+
className: "niv-annotation",
|
|
599
|
+
"data-x": annotation.x < .38 ? "right" : annotation.x > .62 ? "left" : "center",
|
|
600
|
+
"data-y": annotation.y < .28 ? "down" : "up",
|
|
601
|
+
style: {
|
|
602
|
+
left: `${annotation.x * 100}%`,
|
|
603
|
+
top: `${annotation.y * 100}%`,
|
|
604
|
+
transform: `translate(-50%,-50%) scale(${1 / transform.zoom})`
|
|
605
|
+
},
|
|
606
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
607
|
+
type: "button",
|
|
608
|
+
className: "niv-pin",
|
|
609
|
+
"data-active": selected === annotation.id,
|
|
610
|
+
"aria-label": fill(t("note"), { value: position + 1 }),
|
|
611
|
+
onClick: (event) => {
|
|
612
|
+
event.stopPropagation();
|
|
613
|
+
const opening = selected !== annotation.id;
|
|
614
|
+
setSelected(opening ? annotation.id : void 0);
|
|
615
|
+
if (opening) setFocusNote(annotation.id);
|
|
616
|
+
},
|
|
617
|
+
children: position + 1
|
|
618
|
+
}), selected === annotation.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
619
|
+
className: "niv-inline-note",
|
|
620
|
+
"data-note-id": annotation.id,
|
|
621
|
+
onClick: (event) => event.stopPropagation(),
|
|
622
|
+
children: [
|
|
623
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
624
|
+
className: "niv-inline-index",
|
|
625
|
+
children: position + 1
|
|
626
|
+
}),
|
|
627
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
628
|
+
value: annotation.note,
|
|
629
|
+
rows: 1,
|
|
630
|
+
"aria-label": fill(t("note"), { value: position + 1 }),
|
|
631
|
+
placeholder: t("notePlaceholder"),
|
|
632
|
+
onChange: (event) => {
|
|
633
|
+
const note = event.target.value;
|
|
634
|
+
setAnnotations((current) => current.map((entry) => entry.id === annotation.id ? {
|
|
635
|
+
...entry,
|
|
636
|
+
note
|
|
637
|
+
} : entry));
|
|
638
|
+
},
|
|
639
|
+
onKeyDown: (event) => {
|
|
640
|
+
if (event.key === "Escape") {
|
|
641
|
+
event.preventDefault();
|
|
642
|
+
setSelected(void 0);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}),
|
|
646
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
647
|
+
type: "button",
|
|
648
|
+
className: "niv-note-remove",
|
|
649
|
+
"aria-label": t("removeNote"),
|
|
650
|
+
onClick: (event) => {
|
|
651
|
+
event.stopPropagation();
|
|
652
|
+
setAnnotations((current) => current.filter((entry) => entry.id !== annotation.id));
|
|
653
|
+
setSelected(void 0);
|
|
654
|
+
},
|
|
655
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, {})
|
|
656
|
+
})
|
|
657
|
+
]
|
|
658
|
+
}) : null]
|
|
659
|
+
}, annotation.id))]
|
|
660
|
+
}), showCounter ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
661
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
662
|
+
type: "button",
|
|
663
|
+
className: "niv-button niv-icon-only niv-nav niv-prev",
|
|
664
|
+
"aria-label": t("previous"),
|
|
665
|
+
onClick: (event) => {
|
|
666
|
+
event.stopPropagation();
|
|
667
|
+
setIndex((value) => (value - 1 + request.items.length) % request.items.length);
|
|
668
|
+
},
|
|
669
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronLeftOutline14, {})
|
|
670
|
+
}),
|
|
671
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
672
|
+
type: "button",
|
|
673
|
+
className: "niv-button niv-icon-only niv-nav niv-next",
|
|
674
|
+
"aria-label": t("next"),
|
|
675
|
+
onClick: (event) => {
|
|
676
|
+
event.stopPropagation();
|
|
677
|
+
setIndex((value) => (value + 1) % request.items.length);
|
|
678
|
+
},
|
|
679
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, {})
|
|
680
|
+
}),
|
|
681
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
682
|
+
className: "niv-counter",
|
|
683
|
+
children: [
|
|
684
|
+
index + 1,
|
|
685
|
+
" / ",
|
|
686
|
+
request.items.length
|
|
687
|
+
]
|
|
688
|
+
})
|
|
689
|
+
] }) : annotating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
690
|
+
className: "niv-hint",
|
|
691
|
+
children: t("regionHint")
|
|
692
|
+
}) : transform.zoom === 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
693
|
+
className: "niv-hint",
|
|
694
|
+
children: t("zoomHint")
|
|
695
|
+
}) : null]
|
|
696
|
+
}),
|
|
697
|
+
annotations.some((annotation) => annotation.note.trim() !== "") ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
698
|
+
type: "button",
|
|
699
|
+
className: "niv-copy-notes",
|
|
700
|
+
onClick: () => {
|
|
701
|
+
copyNotes();
|
|
702
|
+
},
|
|
703
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutline16, {}), copied ? t("copied") : t("copyNotes")]
|
|
704
|
+
}) : null,
|
|
705
|
+
request.editor !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
|
|
706
|
+
className: "niv-editor",
|
|
707
|
+
children: [
|
|
708
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
709
|
+
value: prompt,
|
|
710
|
+
placeholder: request.editor.placeholder,
|
|
711
|
+
onChange: (event) => setPrompt(event.target.value),
|
|
712
|
+
onKeyDown: (event) => {
|
|
713
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
714
|
+
event.preventDefault();
|
|
715
|
+
submit();
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}),
|
|
719
|
+
editorError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
720
|
+
className: "niv-editor-error",
|
|
721
|
+
role: "alert",
|
|
722
|
+
children: request.editor.errorLabel ?? t("failed")
|
|
723
|
+
}) : null,
|
|
724
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
725
|
+
type: "button",
|
|
726
|
+
disabled: busy,
|
|
727
|
+
onClick: () => {
|
|
728
|
+
submit();
|
|
729
|
+
},
|
|
730
|
+
children: busy ? request.editor.busyLabel ?? t("preparing") : request.editor.label
|
|
731
|
+
})
|
|
732
|
+
]
|
|
733
|
+
}) : null
|
|
734
|
+
]
|
|
735
|
+
})
|
|
736
|
+
]
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
function installOfficialImageBridge(service) {
|
|
740
|
+
const onClick = (event) => {
|
|
741
|
+
const match = nativeImageButton(event.target);
|
|
742
|
+
if (match === void 0) return;
|
|
743
|
+
const { items, index } = imageItemsForButton(match);
|
|
744
|
+
event.preventDefault();
|
|
745
|
+
event.stopPropagation();
|
|
746
|
+
event.stopImmediatePropagation();
|
|
747
|
+
service.open({
|
|
748
|
+
items,
|
|
749
|
+
index,
|
|
750
|
+
opener: match.button,
|
|
751
|
+
source: "dsh-native",
|
|
752
|
+
annotations: true
|
|
753
|
+
});
|
|
754
|
+
};
|
|
755
|
+
document.addEventListener("click", onClick, true);
|
|
756
|
+
return () => {
|
|
757
|
+
document.removeEventListener("click", onClick, true);
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
function apply(ctx) {
|
|
761
|
+
const service = new NativeImageViewerService();
|
|
762
|
+
ctx.effect(() => ctx.locale.register(name, LOCALES), `${name}: dictionaries`);
|
|
763
|
+
ctx.effect(() => {
|
|
764
|
+
const style = document.createElement("style");
|
|
765
|
+
style.dataset.plugin = name;
|
|
766
|
+
style.textContent = CSS$1;
|
|
767
|
+
document.head.appendChild(style);
|
|
768
|
+
return () => {
|
|
769
|
+
style.remove();
|
|
770
|
+
};
|
|
771
|
+
}, `${name}: styles`);
|
|
772
|
+
ctx.effect(() => {
|
|
773
|
+
const disposeService = ctx.reflect.provide("nativeImageViewer", service);
|
|
774
|
+
const disposeBridge = installOfficialImageBridge(service);
|
|
775
|
+
return () => {
|
|
776
|
+
disposeBridge();
|
|
777
|
+
disposeService();
|
|
778
|
+
};
|
|
779
|
+
}, `${name}: optional viewer service and native image bridge`);
|
|
780
|
+
const t = ctx.locale.bind(name);
|
|
781
|
+
ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
782
|
+
name: "shell.overlay",
|
|
783
|
+
id: name,
|
|
784
|
+
order: 20,
|
|
785
|
+
inject: () => ({
|
|
786
|
+
service,
|
|
787
|
+
t
|
|
788
|
+
})
|
|
789
|
+
}, ViewerOverlay));
|
|
790
|
+
}
|
|
791
|
+
//#endregion
|
|
792
|
+
exports.NativeImageViewerService = NativeImageViewerService;
|
|
793
|
+
exports.apply = apply;
|
|
794
|
+
exports.inject = inject;
|
|
795
|
+
exports.name = name;
|
|
796
|
+
exports.normalizeViewerRequest = normalizeViewerRequest;
|
|
797
|
+
return module.exports;
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","names":["#listeners","#snapshot","#revision","#emit","CSS","IconEditOutline16","IconFullscreenOutline16","IconDownloadOutline16","IconCloseOutline16","IconChevronLeftOutline14","IconChevronRightOutline14","IconCopyOutline16","Input","Button","VIEWER_CSS"],"sources":["../src/viewer.js","../src/styles.js","../src/client.jsx"],"sourcesContent":["const boundedNumber = (value, fallback) => Number.isFinite(value) && value > 0 ? value : fallback\r\n\r\nexport function normalizeViewerRequest(request) {\r\n const rawItems = Array.isArray(request?.items) ? request.items : []\r\n const items = rawItems.flatMap((item, position) => {\r\n if (typeof item?.src !== 'string' || item.src === '') return []\r\n return [{\r\n id: typeof item.id === 'string' && item.id !== '' ? item.id : `image-${position + 1}`,\r\n src: item.src,\r\n name: typeof item.name === 'string' && item.name !== '' ? item.name : `Image ${position + 1}`,\r\n width: boundedNumber(item.width, undefined),\r\n height: boundedNumber(item.height, undefined),\r\n bytes: boundedNumber(item.bytes, undefined),\r\n }]\r\n })\r\n if (items.length === 0) return undefined\r\n const requestedIndex = Number.isInteger(request?.index) ? request.index : 0\r\n return {\r\n items,\r\n index: Math.max(0, Math.min(items.length - 1, requestedIndex)),\r\n opener: typeof HTMLElement !== 'undefined' && request?.opener instanceof HTMLElement ? request.opener : undefined,\r\n source: typeof request?.source === 'string' ? request.source : 'dsh',\r\n annotations: request?.annotations !== false,\r\n editor: request?.editor,\r\n }\r\n}\r\n\r\nexport class NativeImageViewerService {\r\n #listeners = new Set()\r\n #revision = 0\r\n #snapshot\r\n\r\n constructor() {\r\n this.subscribe = listener => {\r\n this.#listeners.add(listener)\r\n return () => { this.#listeners.delete(listener) }\r\n }\r\n this.getSnapshot = () => this.#snapshot\r\n }\r\n\r\n open(request) {\r\n const normalized = normalizeViewerRequest(request)\r\n if (normalized === undefined) return false\r\n this.#revision += 1\r\n this.#snapshot = { ...normalized, revision: this.#revision }\r\n this.#emit()\r\n return true\r\n }\r\n\r\n close() {\r\n if (this.#snapshot === undefined) return\r\n const opener = this.#snapshot.opener\r\n this.#snapshot = undefined\r\n this.#emit()\r\n if (typeof window === 'undefined') opener?.focus()\r\n else window.requestAnimationFrame(() => { opener?.focus() })\r\n }\r\n\r\n #emit() {\r\n for (const listener of this.#listeners) listener()\r\n }\r\n}\r\n\r\nexport function nativeImageButton(target) {\r\n if (!(target instanceof Element)) return undefined\r\n const button = target.closest('button')\r\n if (!(button instanceof HTMLButtonElement)) return undefined\r\n const image = button.querySelector(':scope > img')\r\n if (!(image instanceof HTMLImageElement) || image.currentSrc === '') return undefined\r\n if (button.matches('[data-variant=\"single\"],[data-variant=\"tile\"]')) return { button, image, group: button.parentElement }\r\n const rail = button.closest('[role=\"group\"]')\r\n if (rail !== null && button.hasAttribute('title')) return { button, image, group: rail }\r\n return undefined\r\n}\r\n\r\nexport function imageItemsForButton(match) {\r\n const buttons = match.group?.querySelectorAll('button[data-variant]')\r\n ?? match.group?.querySelectorAll('button[title]')\r\n ?? []\r\n const candidates = [...buttons].flatMap((button, index) => {\r\n const image = button.querySelector(':scope > img')\r\n if (!(image instanceof HTMLImageElement) || image.currentSrc === '') return []\r\n return [{\r\n id: image.currentSrc,\r\n src: image.currentSrc,\r\n name: image.alt || `Image ${index + 1}`,\r\n width: image.naturalWidth || undefined,\r\n height: image.naturalHeight || undefined,\r\n button,\r\n }]\r\n })\r\n const items = candidates.length > 0 ? candidates : [{\r\n id: match.image.currentSrc,\r\n src: match.image.currentSrc,\r\n name: match.image.alt || 'Image',\r\n width: match.image.naturalWidth || undefined,\r\n height: match.image.naturalHeight || undefined,\r\n button: match.button,\r\n }]\r\n const index = Math.max(0, items.findIndex(item => item.button === match.button))\r\n return { items: items.map(({ button: _button, ...item }) => item), index }\r\n}\r\n","export const CSS = String.raw`\r\n.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)}\r\n.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}\r\n.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)}\r\n.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{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}\r\n.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)}\r\n.niv-workspace{position:absolute;inset:0;display:grid;min-width:0;min-height:0;padding:68px 24px 72px;box-sizing:border-box}\r\n.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}\r\n.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}\r\n.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)}\r\n.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}\r\n.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)}\r\n.niv-hint{display:none}\r\n.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)}\r\n.niv-editor{position:absolute;right:50%;bottom:70px;z-index:6;display:flex;width:min(620px,calc(100vw - 40px));align-items:center;gap:8px;padding:8px;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-base)) 92%,transparent);box-shadow:0 16px 44px rgba(0,0,0,.32);color:var(--dsw-alias-label-primary);transform:translateX(50%);backdrop-filter:blur(20px)}.niv-editor input{min-width:0;flex:1}.niv-editor-error{color:var(--dsw-alias-state-error-primary);font-size:11px}.niv-workspace[data-editor=true]{padding-bottom:132px}\r\n@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}.niv-editor{bottom:60px;align-items:stretch;flex-direction:column}.niv-editor button{width:100%}.niv-workspace[data-editor=true]{padding-bottom:176px}}\r\n@media(prefers-reduced-motion:reduce){.niv-surface{transition:none}}\r\n`\r\n","import {\r\n useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore,\r\n} from 'react'\r\nimport {\r\n Button, IconChevronLeftOutline14, IconChevronRightOutline14, IconCloseOutline16,\r\n IconCopyOutline16, IconDownloadOutline16, IconEditOutline16, IconFullscreenOutline16,\r\n Input,\r\n} from '@deepseek-ai/dsh-client-ui-primitives'\r\nimport {\r\n imageItemsForButton, NativeImageViewerService, nativeImageButton,\r\n} from './viewer.js'\r\nimport { CSS as VIEWER_CSS } from './styles.js'\r\n\r\nexport const name = 'dsh-image-viewer'\r\nexport const inject = ['locale', 'slots']\r\n\r\nconst LOCALES = {\r\n en: {\r\n dialog: 'Image viewer', close: 'Close', fit: 'Fit', actual: '100%', download: 'Download',\r\n annotate: 'Mark region', cancelAnnotate: 'Cancel marking', regions: 'Region notes', regionHint: 'Click a point on the image, then add a note.',\r\n note: 'Region {value}', notePlaceholder: 'Describe what should change here…', removeNote: 'Remove region note',\r\n hideNotes: 'Hide notes', copyNotes: 'Copy notes', copied: 'Copied', previous: 'Previous image', next: 'Next image',\r\n zoomHint: 'Wheel to zoom · drag to pan · double-click for 100%', preparing: 'Preparing…', failed: 'Could not prepare this image.',\r\n },\r\n zh: {\r\n dialog: '图片查看器', close: '关闭', fit: '适应窗口', actual: '原始大小', download: '下载',\r\n annotate: '标记区域', cancelAnnotate: '取消标记', regions: '区域备注', regionHint: '点击图片中的位置,然后填写备注。',\r\n note: '区域 {value}', notePlaceholder: '描述这里需要怎样调整…', removeNote: '删除区域备注',\r\n hideNotes: '收起备注', copyNotes: '复制备注', copied: '已复制', previous: '上一张图片', next: '下一张图片',\r\n zoomHint: '滚轮缩放 · 拖动查看 · 双击切换原始大小', preparing: '正在准备…', failed: '暂时无法准备这张图片。',\r\n },\r\n}\r\n\r\nconst fill = (value, variables) => Object.entries(variables).reduce(\r\n (text, [key, replacement]) => text.replaceAll(`{${key}}`, String(replacement)),\r\n value,\r\n)\r\nconst clamp = (value, min, max) => Math.min(max, Math.max(min, value))\r\nconst bytesLabel = bytes => bytes === undefined ? undefined : bytes < 1024 * 1024\r\n ? `${Math.max(0.1, bytes / 1024).toLocaleString(undefined, { maximumFractionDigits: 1 })} KB`\r\n : `${(bytes / 1024 / 1024).toLocaleString(undefined, { maximumFractionDigits: 1 })} MB`\r\nconst downloadName = name => {\r\n const cleaned = String(name || 'image.png').replace(/[<>:\"/\\\\|?*\\u0000-\\u001f]/gu, '-').replace(/[. ]+$/u, '').trim()\r\n return cleaned === '' ? 'image.png' : cleaned\r\n}\r\nconst noteText = (annotations, t) => annotations.map((annotation, index) => {\r\n const label = fill(t('note'), { value: index + 1 })\r\n return `${label} (${Math.round(annotation.x * 100)}%, ${Math.round(annotation.y * 100)}%): ${annotation.note.trim()}`\r\n}).filter(line => !line.endsWith(': ')).join('\\n')\r\n\r\nfunction ViewerOverlay({ service, t }) {\r\n const request = useSyncExternalStore(service.subscribe, service.getSnapshot)\r\n const [index, setIndex] = useState(0)\r\n const [transform, setTransform] = useState({ zoom: 1, x: 0, y: 0 })\r\n const [dragging, setDragging] = useState(false)\r\n const [annotating, setAnnotating] = useState(false)\r\n const [annotationsByImage, setAnnotationsByImage] = useState({})\r\n const [selected, setSelected] = useState()\r\n const [focusNote, setFocusNote] = useState()\r\n const [prompt, setPrompt] = useState('')\r\n const [busy, setBusy] = useState(false)\r\n const [editorError, setEditorError] = useState(false)\r\n const [copied, setCopied] = useState(false)\r\n const rootRef = useRef(null)\r\n const stageRef = useRef(null)\r\n const surfaceRef = useRef(null)\r\n const imageRef = useRef(null)\r\n const pointersRef = useRef(new Map())\r\n const gestureRef = useRef()\r\n const transformRef = useRef(transform)\r\n transformRef.current = transform\r\n\r\n useEffect(() => {\r\n if (request === undefined) return\r\n setIndex(request.index)\r\n setTransform({ zoom: 1, x: 0, y: 0 })\r\n setDragging(false)\r\n setAnnotating(false)\r\n setAnnotationsByImage({})\r\n setSelected(undefined)\r\n setPrompt('')\r\n setBusy(false)\r\n setEditorError(false)\r\n setCopied(false)\r\n }, [request?.revision])\r\n\r\n const item = request?.items[index]\r\n const annotations = item === undefined ? [] : annotationsByImage[item.id] ?? []\r\n const setAnnotations = useCallback((update) => {\r\n if (item === undefined) return\r\n setAnnotationsByImage(current => {\r\n const previous = current[item.id] ?? []\r\n const next = typeof update === 'function' ? update(previous) : update\r\n return { ...current, [item.id]: next }\r\n })\r\n }, [item?.id])\r\n\r\n const boundedPan = useCallback((zoom, x, y) => {\r\n const stage = stageRef.current\r\n const surface = surfaceRef.current\r\n if (stage === null || surface === null || zoom <= 1) return { x: 0, y: 0 }\r\n const limitX = Math.max(0, (surface.offsetWidth * zoom - stage.clientWidth) / 2) + 28\r\n const limitY = Math.max(0, (surface.offsetHeight * zoom - stage.clientHeight) / 2) + 28\r\n return { x: clamp(x, -limitX, limitX), y: clamp(y, -limitY, limitY) }\r\n }, [])\r\n\r\n const setZoomAt = useCallback((nextZoom, clientX, clientY) => {\r\n const stage = stageRef.current\r\n if (stage === null) return\r\n setTransform(current => {\r\n const next = clamp(nextZoom, 0.5, 8)\r\n const box = stage.getBoundingClientRect()\r\n const px = clientX - box.left - box.width / 2\r\n const py = clientY - box.top - box.height / 2\r\n const ratio = next / current.zoom\r\n const pan = boundedPan(next, px - (px - current.x) * ratio, py - (py - current.y) * ratio)\r\n return { zoom: next, ...pan }\r\n })\r\n }, [boundedPan])\r\n\r\n const fit = useCallback(() => { setTransform({ zoom: 1, x: 0, y: 0 }) }, [])\r\n const actual = useCallback(() => {\r\n const image = imageRef.current\r\n const surface = surfaceRef.current\r\n if (image === null || surface === null || image.naturalWidth === 0) return\r\n const zoom = clamp(image.naturalWidth / Math.max(1, surface.offsetWidth), 1, 8)\r\n setTransform({ zoom, x: 0, y: 0 })\r\n }, [])\r\n\r\n useEffect(() => {\r\n if (request === undefined) return undefined\r\n const previousOverflow = document.body.style.overflow\r\n document.body.style.overflow = 'hidden'\r\n rootRef.current?.focus()\r\n const onKeyDown = event => {\r\n if (event.key === 'Escape') {\r\n event.preventDefault()\r\n service.close()\r\n return\r\n }\r\n const editing = event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement\r\n if (!editing && event.key === 'ArrowLeft' && request.items.length > 1) {\r\n event.preventDefault(); setIndex(value => (value - 1 + request.items.length) % request.items.length)\r\n } else if (!editing && event.key === 'ArrowRight' && request.items.length > 1) {\r\n event.preventDefault(); setIndex(value => (value + 1) % request.items.length)\r\n } else if (!editing && (event.key === '+' || event.key === '=')) {\r\n event.preventDefault(); const box = stageRef.current?.getBoundingClientRect(); if (box) setZoomAt(transformRef.current.zoom * 1.2, box.left + box.width / 2, box.top + box.height / 2)\r\n } else if (!editing && event.key === '-') {\r\n event.preventDefault(); const box = stageRef.current?.getBoundingClientRect(); if (box) setZoomAt(transformRef.current.zoom / 1.2, box.left + box.width / 2, box.top + box.height / 2)\r\n } else if (!editing && event.key.toLowerCase() === 'f') {\r\n event.preventDefault(); fit()\r\n } else if (event.key === 'Tab') {\r\n const controls = [...rootRef.current.querySelectorAll('button:not(:disabled),a[href],input:not(:disabled),textarea:not(:disabled),[tabindex]:not([tabindex=\"-1\"])')]\r\n const first = controls[0]\r\n const last = controls.at(-1)\r\n if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus() }\r\n else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus() }\r\n }\r\n }\r\n document.addEventListener('keydown', onKeyDown)\r\n return () => {\r\n document.body.style.overflow = previousOverflow\r\n document.removeEventListener('keydown', onKeyDown)\r\n }\r\n }, [request, service, fit, setZoomAt])\r\n\r\n useEffect(() => {\r\n if (focusNote === undefined) return\r\n const field = rootRef.current?.querySelector(`[data-note-id=\"${CSS.escape(focusNote)}\"] textarea`)\r\n field?.focus()\r\n setFocusNote(undefined)\r\n }, [focusNote, selected, annotations.length])\r\n\r\n useEffect(() => {\r\n setTransform({ zoom: 1, x: 0, y: 0 })\r\n setDragging(false)\r\n setAnnotating(false)\r\n setSelected(undefined)\r\n setEditorError(false)\r\n }, [item?.id])\r\n\r\n const onWheel = useCallback(event => {\r\n event.preventDefault()\r\n setZoomAt(transformRef.current.zoom * Math.exp(-event.deltaY * 0.0015), event.clientX, event.clientY)\r\n }, [setZoomAt])\r\n useEffect(() => {\r\n const stage = stageRef.current\r\n if (stage === null || request === undefined) return undefined\r\n stage.addEventListener('wheel', onWheel, { passive: false })\r\n return () => stage.removeEventListener('wheel', onWheel)\r\n }, [onWheel, request])\r\n const onPointerDown = event => {\r\n if (event.button !== 0 || annotating) return\r\n event.currentTarget.setPointerCapture(event.pointerId)\r\n pointersRef.current.set(event.pointerId, { x: event.clientX, y: event.clientY })\r\n if (pointersRef.current.size === 2) {\r\n const [a, b] = [...pointersRef.current.values()]\r\n gestureRef.current = { kind: 'pinch', distance: Math.hypot(a.x - b.x, a.y - b.y), transform }\r\n } else if (!annotating && transform.zoom > 1) {\r\n gestureRef.current = { kind: 'pan', x: event.clientX, y: event.clientY, transform }\r\n setDragging(true)\r\n }\r\n }\r\n const onPointerMove = event => {\r\n if (!pointersRef.current.has(event.pointerId)) return\r\n pointersRef.current.set(event.pointerId, { x: event.clientX, y: event.clientY })\r\n const gesture = gestureRef.current\r\n if (gesture?.kind === 'pinch' && pointersRef.current.size >= 2) {\r\n const [a, b] = [...pointersRef.current.values()]\r\n const distance = Math.max(1, Math.hypot(a.x - b.x, a.y - b.y))\r\n setZoomAt(gesture.transform.zoom * distance / Math.max(1, gesture.distance), (a.x + b.x) / 2, (a.y + b.y) / 2)\r\n } else if (gesture?.kind === 'pan') {\r\n const pan = boundedPan(gesture.transform.zoom, gesture.transform.x + event.clientX - gesture.x, gesture.transform.y + event.clientY - gesture.y)\r\n setTransform({ zoom: gesture.transform.zoom, ...pan })\r\n }\r\n }\r\n const endPointer = event => {\r\n pointersRef.current.delete(event.pointerId)\r\n if (pointersRef.current.size === 0) { gestureRef.current = undefined; setDragging(false) }\r\n }\r\n const addAnnotation = event => {\r\n if (!annotating || event.target.closest('.niv-annotation')) return\r\n const bounds = surfaceRef.current?.getBoundingClientRect()\r\n if (bounds === undefined) return\r\n const annotation = {\r\n id: crypto.randomUUID(),\r\n x: clamp((event.clientX - bounds.left) / bounds.width, 0, 1),\r\n y: clamp((event.clientY - bounds.top) / bounds.height, 0, 1),\r\n note: '',\r\n }\r\n setAnnotations(current => [...current, annotation])\r\n setAnnotating(false)\r\n setSelected(annotation.id)\r\n setFocusNote(annotation.id)\r\n }\r\n const submit = async () => {\r\n if (busy || request?.editor?.onSubmit === undefined || item === undefined) return\r\n setBusy(true)\r\n setEditorError(false)\r\n try {\r\n await request.editor.onSubmit({ prompt, annotations, item, src: item.src })\r\n service.close()\r\n } catch {\r\n setEditorError(true)\r\n } finally {\r\n setBusy(false)\r\n }\r\n }\r\n const copyNotes = async () => {\r\n const text = noteText(annotations, t)\r\n if (text === '') return\r\n await navigator.clipboard.writeText(text)\r\n setCopied(true)\r\n window.setTimeout(() => { setCopied(false) }, 1200)\r\n }\r\n\r\n if (request === undefined || item === undefined) return null\r\n const meta = [item.width && item.height ? `${item.width} × ${item.height}` : undefined, bytesLabel(item.bytes)].filter(Boolean).join(' · ')\r\n const showCounter = request.items.length > 1\r\n return <div ref={rootRef} className=\"niv-root\" role=\"dialog\" aria-modal=\"true\" aria-label={t('dialog')} tabIndex={-1}>\r\n <div className=\"niv-title niv-sr-only\"><strong>{item.name}</strong>{meta !== '' ? <small>{meta}</small> : null}</div>\r\n <header className=\"niv-topbar\" role=\"toolbar\" aria-label={t('dialog')}>\r\n <div className=\"niv-actions\">\r\n {request.annotations ? <button type=\"button\" className=\"niv-button\" data-active={annotating} aria-label={annotating ? t('cancelAnnotate') : t('annotate')} aria-pressed={annotating} onClick={() => setAnnotating(value => !value)}><IconEditOutline16 /><span className=\"niv-label\">{annotating ? t('cancelAnnotate') : t('annotate')}</span></button> : null}\r\n {annotations.length > 0 ? <button type=\"button\" className=\"niv-button\" data-active={selected !== undefined} onClick={() => { const first = annotations[0]; setSelected(current => current === undefined ? first.id : undefined); if (selected === undefined) setFocusNote(first.id) }}>{annotations.length} <span className=\"niv-label\">{t('regions')}</span></button> : null}\r\n <button type=\"button\" className=\"niv-button\" aria-label={t('fit')} onClick={fit}><IconFullscreenOutline16 /><span className=\"niv-label\">{t('fit')}</span></button>\r\n <button type=\"button\" className=\"niv-button\" onClick={actual}>{t('actual')}</button>\r\n <span className=\"niv-zoom\">{Math.round(transform.zoom * 100)}%</span>\r\n <a className=\"niv-download\" href={item.src} download={downloadName(item.name)}><IconDownloadOutline16 /><span className=\"niv-label\">{t('download')}</span></a>\r\n </div>\r\n </header>\r\n <button type=\"button\" className=\"niv-close-floating\" aria-label={t('close')} onClick={() => service.close()}><IconCloseOutline16 /></button>\r\n <div className=\"niv-workspace\" data-editor={request.editor !== undefined}>\r\n <main ref={stageRef} className=\"niv-stage\" data-dragging={dragging} data-annotating={annotating} onClick={event => { if (event.target === event.currentTarget && !annotating && transform.zoom === 1) service.close() }} onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={endPointer} onPointerCancel={endPointer} onDoubleClick={() => { if (transform.zoom === 1) actual(); else fit() }}>\r\n <div ref={surfaceRef} className=\"niv-surface\" onClick={addAnnotation} style={{ transform: `translate3d(${transform.x}px,${transform.y}px,0) scale(${transform.zoom})` }}>\r\n <img ref={imageRef} className=\"niv-image\" src={item.src} alt={item.name} draggable=\"false\" />\r\n {annotations.map((annotation, position) => <div className=\"niv-annotation\" data-x={annotation.x < 0.38 ? 'right' : annotation.x > 0.62 ? 'left' : 'center'} data-y={annotation.y < 0.28 ? 'down' : 'up'} style={{ left: `${annotation.x * 100}%`, top: `${annotation.y * 100}%`, transform: `translate(-50%,-50%) scale(${1 / transform.zoom})` }} key={annotation.id}>\r\n <button type=\"button\" className=\"niv-pin\" data-active={selected === annotation.id} aria-label={fill(t('note'), { value: position + 1 })} onClick={event => { event.stopPropagation(); const opening = selected !== annotation.id; setSelected(opening ? annotation.id : undefined); if (opening) setFocusNote(annotation.id) }}>{position + 1}</button>\r\n {selected === annotation.id ? <div className=\"niv-inline-note\" data-note-id={annotation.id} onClick={event => event.stopPropagation()}>\r\n <span className=\"niv-inline-index\">{position + 1}</span>\r\n <textarea value={annotation.note} rows={1} aria-label={fill(t('note'), { value: position + 1 })} placeholder={t('notePlaceholder')} onChange={event => { const note = event.target.value; setAnnotations(current => current.map(entry => entry.id === annotation.id ? { ...entry, note } : entry)) }} onKeyDown={event => { if (event.key === 'Escape') { event.preventDefault(); setSelected(undefined) } }} />\r\n <button type=\"button\" className=\"niv-note-remove\" aria-label={t('removeNote')} onClick={event => { event.stopPropagation(); setAnnotations(current => current.filter(entry => entry.id !== annotation.id)); setSelected(undefined) }}><IconCloseOutline16 /></button>\r\n </div> : null}\r\n </div>)}\r\n </div>\r\n {showCounter ? <>\r\n <button type=\"button\" className=\"niv-button niv-icon-only niv-nav niv-prev\" aria-label={t('previous')} onClick={event => { event.stopPropagation(); setIndex(value => (value - 1 + request.items.length) % request.items.length) }}><IconChevronLeftOutline14 /></button>\r\n <button type=\"button\" className=\"niv-button niv-icon-only niv-nav niv-next\" aria-label={t('next')} onClick={event => { event.stopPropagation(); setIndex(value => (value + 1) % request.items.length) }}><IconChevronRightOutline14 /></button>\r\n <span className=\"niv-counter\">{index + 1} / {request.items.length}</span>\r\n </> : annotating ? <span className=\"niv-hint\">{t('regionHint')}</span> : transform.zoom === 1 ? <span className=\"niv-hint\">{t('zoomHint')}</span> : null}\r\n </main>\r\n {annotations.some(annotation => annotation.note.trim() !== '') ? <button type=\"button\" className=\"niv-copy-notes\" onClick={() => { void copyNotes() }}><IconCopyOutline16 />{copied ? t('copied') : t('copyNotes')}</button> : null}\r\n {request.editor !== undefined ? <footer className=\"niv-editor\">\r\n <Input value={prompt} placeholder={request.editor.placeholder} onChange={event => setPrompt(event.target.value)} onKeyDown={event => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); void submit() } }} />\r\n {editorError ? <span className=\"niv-editor-error\" role=\"alert\">{request.editor.errorLabel ?? t('failed')}</span> : null}\r\n <Button type=\"button\" disabled={busy} onClick={() => { void submit() }}>{busy ? request.editor.busyLabel ?? t('preparing') : request.editor.label}</Button>\r\n </footer> : null}\r\n </div>\r\n </div>\r\n}\r\n\r\nfunction installOfficialImageBridge(service) {\r\n const onClick = event => {\r\n const match = nativeImageButton(event.target)\r\n if (match === undefined) return\r\n const { items, index } = imageItemsForButton(match)\r\n event.preventDefault()\r\n event.stopPropagation()\r\n event.stopImmediatePropagation()\r\n service.open({ items, index, opener: match.button, source: 'dsh-native', annotations: true })\r\n }\r\n document.addEventListener('click', onClick, true)\r\n return () => { document.removeEventListener('click', onClick, true) }\r\n}\r\n\r\nexport function apply(ctx) {\r\n const service = new NativeImageViewerService()\r\n ctx.effect(() => ctx.locale.register(name, LOCALES), `${name}: dictionaries`)\r\n ctx.effect(() => {\r\n const style = document.createElement('style')\r\n style.dataset.plugin = name\r\n style.textContent = VIEWER_CSS\r\n document.head.appendChild(style)\r\n return () => { style.remove() }\r\n }, `${name}: styles`)\r\n ctx.effect(() => {\r\n const disposeService = ctx.reflect.provide('nativeImageViewer', service)\r\n const disposeBridge = installOfficialImageBridge(service)\r\n return () => { disposeBridge(); void disposeService() }\r\n }, `${name}: optional viewer service and native image bridge`)\r\n const t = ctx.locale.bind(name)\r\n ctx.slots.inject('shell.overlay', () => ctx.slots.register({\r\n name: 'shell.overlay', id: name, order: 20, inject: () => ({ service, t }),\r\n }, ViewerOverlay))\r\n}\r\n\r\nexport { NativeImageViewerService, normalizeViewerRequest } from './viewer.js'\r\n"],"mappings":"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"}
|
package/lib/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-image-viewer",
|
|
3
|
+
"version": "0.1.0-beta.3",
|
|
4
|
+
"description": "Zoom, pan, download, gallery, and region-note image viewer for DeepSeek Harness",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dsh-plugin",
|
|
7
|
+
"deepseek-harness",
|
|
8
|
+
"image-viewer",
|
|
9
|
+
"lightbox",
|
|
10
|
+
"zoom",
|
|
11
|
+
"annotation"
|
|
12
|
+
],
|
|
13
|
+
"author": "WSL043",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./lib/index.js",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./lib/index.js",
|
|
19
|
+
"./client": "./lib/client.js",
|
|
20
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"lib/*.js",
|
|
25
|
+
"lib/*.map",
|
|
26
|
+
"cordis.patch.yml",
|
|
27
|
+
"README.md",
|
|
28
|
+
"README.zh-CN.md",
|
|
29
|
+
"compatibility.json",
|
|
30
|
+
"AGENTS.md",
|
|
31
|
+
"LICENSE",
|
|
32
|
+
"SECURITY.md",
|
|
33
|
+
"THIRD_PARTY_NOTICES.md"
|
|
34
|
+
],
|
|
35
|
+
"dsh": {
|
|
36
|
+
"bundle": {
|
|
37
|
+
"patch": "./cordis.patch.yml"
|
|
38
|
+
},
|
|
39
|
+
"client": {
|
|
40
|
+
"inject": [
|
|
41
|
+
"@deepseek-ai/dsh-client-locale",
|
|
42
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
43
|
+
"@deepseek-ai/dsh-client-ui-primitives",
|
|
44
|
+
"@deepseek-ai/dsh-client-ui-slots"
|
|
45
|
+
],
|
|
46
|
+
"platform": "web"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/WSL043/dsh-image-viewer.git"
|
|
52
|
+
},
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/WSL043/dsh-image-viewer/issues"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/WSL043/dsh-image-viewer#readme",
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
},
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"@deepseek-ai/cordis": "4.0.1",
|
|
65
|
+
"@deepseek-ai/dsh-client-locale": "0.1.1-rc.2",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-layout": "0.1.1-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.1.1-rc.2",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.1-rc.2",
|
|
69
|
+
"react": "^18.2.0"
|
|
70
|
+
},
|
|
71
|
+
"peerDependenciesMeta": {
|
|
72
|
+
"@deepseek-ai/cordis": {
|
|
73
|
+
"optional": true
|
|
74
|
+
},
|
|
75
|
+
"@deepseek-ai/dsh-client-locale": {
|
|
76
|
+
"optional": true
|
|
77
|
+
},
|
|
78
|
+
"@deepseek-ai/dsh-client-ui-layout": {
|
|
79
|
+
"optional": true
|
|
80
|
+
},
|
|
81
|
+
"@deepseek-ai/dsh-client-ui-primitives": {
|
|
82
|
+
"optional": true
|
|
83
|
+
},
|
|
84
|
+
"@deepseek-ai/dsh-client-ui-slots": {
|
|
85
|
+
"optional": true
|
|
86
|
+
},
|
|
87
|
+
"react": {
|
|
88
|
+
"optional": true
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"devDependencies": {
|
|
92
|
+
"@deepseek-ai/cordis": "4.0.1",
|
|
93
|
+
"@deepseek-ai/dsh-client-locale": "0.1.1-rc.2",
|
|
94
|
+
"@deepseek-ai/dsh-client-ui-layout": "0.1.1-rc.2",
|
|
95
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.1.1-rc.2",
|
|
96
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.1-rc.2",
|
|
97
|
+
"react": "18.3.1",
|
|
98
|
+
"tsdown": "0.22.2"
|
|
99
|
+
},
|
|
100
|
+
"scripts": {
|
|
101
|
+
"build": "tsdown --config tsdown.config.mjs",
|
|
102
|
+
"test": "node --test tests/*.test.mjs",
|
|
103
|
+
"check": "pnpm run test && pnpm run build && pnpm pack --pack-destination .artifacts"
|
|
104
|
+
}
|
|
105
|
+
}
|