@kanyun-ai-infra/band-plugin-react 0.0.0-stage → 0.1.2

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.
@@ -0,0 +1,788 @@
1
+ // src/image-view-model.ts
2
+ var unit = (n) => typeof n === "number" && n >= 0 && n <= 1;
3
+ function unitRectFrom(value) {
4
+ if (!value || typeof value !== "object") return null;
5
+ const r = value;
6
+ if (!unit(r.x) || !unit(r.y) || !unit(r.w) || !unit(r.h)) return null;
7
+ if (r.x + r.w > 1.000001 || r.y + r.h > 1.000001) return null;
8
+ return { x: r.x, y: r.y, w: r.w, h: r.h };
9
+ }
10
+ var DRAG_PX = 4;
11
+ function unitRectOf(content, a, b = a) {
12
+ if (content.width <= 0 || content.height <= 0) return null;
13
+ const milli = (v, size) => Math.round(Math.min(1, Math.max(0, v / size)) * 1e3);
14
+ const x0 = milli(Math.min(a.x, b.x) - content.left, content.width);
15
+ const x1 = milli(Math.max(a.x, b.x) - content.left, content.width);
16
+ const y0 = milli(Math.min(a.y, b.y) - content.top, content.height);
17
+ const y1 = milli(Math.max(a.y, b.y) - content.top, content.height);
18
+ return { x: x0 / 1e3, y: y0 / 1e3, w: (x1 - x0) / 1e3, h: (y1 - y0) / 1e3 };
19
+ }
20
+ var MIN_SCALE = 1;
21
+ var MAX_SCALE = 8;
22
+ var SNAP_SCALE = 1.05;
23
+ var FIT = { scale: 1, x: 0, y: 0 };
24
+ function fitSize(natural, viewport) {
25
+ if (natural.width <= 0 || natural.height <= 0) return { width: 0, height: 0 };
26
+ const k = Math.min(1, viewport.width / natural.width, viewport.height / natural.height);
27
+ return { width: natural.width * k, height: natural.height * k };
28
+ }
29
+ function clampView(v, base, viewport, max = MAX_SCALE) {
30
+ const scale = Math.min(max, Math.max(MIN_SCALE, v.scale));
31
+ const room = (size, view) => Math.max(0, (size * scale - view) / 2);
32
+ const rx = room(base.width, viewport.width);
33
+ const ry = room(base.height, viewport.height);
34
+ return {
35
+ scale,
36
+ x: Math.min(rx, Math.max(-rx, v.x)),
37
+ y: Math.min(ry, Math.max(-ry, v.y))
38
+ };
39
+ }
40
+ function zoomAt(v, factor, at, base, viewport, max = MAX_SCALE) {
41
+ const scale = Math.min(Math.max(max, v.scale), Math.max(MIN_SCALE, v.scale * factor));
42
+ if (factor < 1 && scale < SNAP_SCALE) return FIT;
43
+ const k = scale / v.scale;
44
+ return clampView(
45
+ { scale, x: at.x - (at.x - v.x) * k, y: at.y - (at.y - v.y) * k },
46
+ base,
47
+ viewport,
48
+ Math.max(max, v.scale)
49
+ );
50
+ }
51
+
52
+ // src/image-lightbox.ts
53
+ import {
54
+ blockingDialog,
55
+ withImeGuard
56
+ } from "@kanyun-ai-infra/band-plugin-sdk";
57
+
58
+ // src/image-lightbox-model.ts
59
+ var LIGHTBOX_MAX_SCALE = Math.min(MAX_SCALE, 4);
60
+ var LIGHTBOX_DRAG_PX = Math.max(DRAG_PX, 6);
61
+ function lightboxSize(natural, viewport, narrow) {
62
+ const gap = narrow ? 12 : 40;
63
+ return fitSize(natural, {
64
+ width: Math.max(0, viewport.width - gap * 2),
65
+ height: Math.max(0, viewport.height - gap * 2)
66
+ });
67
+ }
68
+ function lightboxClamp(view, base, viewport) {
69
+ if (view.scale <= 1) return { scale: 1, x: 0, y: 0 };
70
+ return clampView(
71
+ view,
72
+ base,
73
+ {
74
+ width: Math.max(0, viewport.width - 80),
75
+ height: Math.max(0, viewport.height - 80)
76
+ },
77
+ LIGHTBOX_MAX_SCALE
78
+ );
79
+ }
80
+ function lightboxZoom(view, factor, at, base, viewport) {
81
+ const room = {
82
+ width: Math.max(0, viewport.width - 80),
83
+ height: Math.max(0, viewport.height - 80)
84
+ };
85
+ return lightboxClamp(zoomAt(view, factor, at, base, room, LIGHTBOX_MAX_SCALE), base, viewport);
86
+ }
87
+ function focusRegion(rect, base, viewport) {
88
+ return lightboxClamp(
89
+ {
90
+ scale: 2,
91
+ x: (0.5 - rect.x - rect.w / 2) * base.width * 2,
92
+ y: (0.5 - rect.y - rect.h / 2) * base.height * 2
93
+ },
94
+ base,
95
+ viewport
96
+ );
97
+ }
98
+ function nextImage(index, delta, count) {
99
+ return Math.max(0, Math.min(count - 1, index + delta));
100
+ }
101
+
102
+ // src/image-lightbox.ts
103
+ var LIGHTBOX_STYLE = `
104
+ .bs-frame { position: relative; min-height: 0; height: 100%; display: grid; }
105
+ .bs-frame .bs-lb [hidden] { display: none; }
106
+ .bs-picture { display: block; position: relative; padding: 0; border: 0; background: none; border-radius: var(--r-sm); cursor: zoom-in; justify-self: start; }
107
+ .bs-picture:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
108
+ .bs-wall .bs-picture { width: 100%; justify-self: stretch; }
109
+ .bs-wall .bs-thumb { max-width: 100%; }
110
+ .bs-magnify { position: absolute; right: 4px; bottom: 4px; width: 24px; height: 24px; display: grid; place-items: center; border-radius: 6px; background: rgba(0,0,0,.45); color: var(--media-ink); opacity: 0; }
111
+ .bs-magnify svg { width: 14px; height: 14px; }
112
+ .bs-picture:hover .bs-magnify, .bs-picture:focus-visible .bs-magnify { opacity: 1; }
113
+ .bs-count { position: absolute; top: 4px; left: 4px; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 8px; background: var(--accent); color: var(--accent-ink); font: 600 10px var(--mono); }
114
+ .bs-returned { outline: 2px solid var(--accent); outline-offset: -2px; }
115
+ .bs-lb { position: absolute; inset: 0; display: grid; grid-template-rows: auto minmax(0,1fr) auto; background: var(--media); color: var(--media-ink); min-width: 0; }
116
+ /* \u5168\u5C4F\u5C42\u7EA7\u7531\u771F\u5B9E\u6302\u8F7D\u70B9\u7BA1\u7406\uFF1B\u9501\u5B9A\u8BC4\u8BBA\u5BBF\u4E3B\u540C\u6863\u540E\u7F6E\uFF0C\u7EE7\u7EED\u753B\u5728\u5927\u56FE\u4E0A\u9762\u3002 */
117
+ .bs-lb[data-full="true"] { position: fixed; inset: 0; }
118
+ .bs-lb-bar { display: flex; align-items: center; gap: 4px; padding: 8px 12px; min-width: 0; }
119
+ .bs-lb-title { font-size: var(--fs-ui); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; margin: 0 6px; }
120
+ .bs-lb-title span { color: rgba(255,255,255,.6); font: var(--fs-meta) var(--mono); margin-left: 6px; }
121
+ .bs-lb-grow { flex: 1; }
122
+ .bs-lb .bs-lb-button { width: 32px; height: 32px; padding: 0; border: 0; border-radius: 8px; display: inline-grid; place-items: center; background: transparent; color: rgba(255,255,255,.85); cursor: pointer; flex: none; }
123
+ .bs-lb-button svg { width: 18px; height: 18px; }
124
+ .bs-lb .bs-lb-button:hover, .bs-lb-back:hover, .bs-lb-zoom:hover { background: rgba(255,255,255,.12); }
125
+ .bs-lb button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
126
+ .bs-lb button:disabled { opacity: .3; cursor: default; }
127
+ .bs-lb-back { display: inline-flex; align-items: center; gap: 4px; border: 0; background: transparent; color: rgba(255,255,255,.85); font: var(--fs-ui) var(--font); padding: 5px 8px 5px 4px; border-radius: var(--r); cursor: pointer; white-space: nowrap; }
128
+ .bs-lb-back svg { width: 16px; height: 16px; }
129
+ .bs-lb-zoom { font: 500 var(--fs-meta) var(--mono); color: rgba(255,255,255,.8); min-width: 48px; text-align: center; border: 0; background: transparent; cursor: pointer; border-radius: var(--r-sm); padding: 4px 2px; font-variant-numeric: tabular-nums; }
130
+ .bs-lb-main { display: grid; grid-template-columns: minmax(0,1fr) 300px; min-height: 0; }
131
+ .bs-lb-stage { position: relative; overflow: hidden; touch-action: none; user-select: none; min-height: 0; outline: none; }
132
+ .bs-lb-stage[data-pan="true"] { cursor: grab; }
133
+ .bs-lb-stage[data-pan="true"]:active { cursor: grabbing; }
134
+ .bs-lb-stage[data-mark="true"] { cursor: crosshair; }
135
+ .bs-lb-image { position: absolute; border-radius: 2px; box-shadow: 0 8px 30px rgba(0,0,0,.5); max-width: none; object-fit: fill; pointer-events: none; }
136
+ .bs-lb-regions { position: absolute; inset: 0; pointer-events: none; }
137
+ .bs-lb-region { position: absolute; border: 2px solid var(--accent); background: color-mix(in srgb,var(--accent) 13%,transparent); border-radius: 2px; }
138
+ .bs-lb-region.old { border-width: 1.5px; border-style: dashed; background: color-mix(in srgb,var(--accent) 8%,transparent); }
139
+ .bs-lb-point { position: absolute; width: 14px; height: 14px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb,var(--accent) 30%,transparent); translate: -50% -50%; }
140
+ .bs-lb-nav { position: absolute; top: 50%; translate: 0 -50%; width: 40px; height: 40px; border-radius: 20px; border: 0; background: rgba(0,0,0,.45); color: var(--media-ink); display: grid; place-items: center; cursor: pointer; }
141
+ .bs-lb-nav:hover { background: rgba(0,0,0,.7); }
142
+ .bs-lb-nav svg { width: 20px; height: 20px; }
143
+ .bs-lb-nav.prev { left: 12px; } .bs-lb-nav.next { right: 12px; }
144
+ .bs-lb-side { background: var(--panel); color: var(--ink); overflow: auto; overscroll-behavior: contain; padding: 16px; display: grid; gap: 12px; align-content: start; border-left: 1px solid var(--rule); min-width: 0; }
145
+ .bs-lb-side h3 { margin: 0; font-size: var(--fs-title); display: flex; align-items: baseline; gap: 8px; }
146
+ .bs-lb-side h3 span { font: var(--fs-meta) var(--mono); color: var(--muted); font-weight: 400; }
147
+ .bs-lb-side dl { margin: 0; display: grid; grid-template-columns: 40px minmax(0,1fr); gap: 6px 8px; font-size: var(--fs-ui); }
148
+ .bs-lb-side dt { color: var(--muted); font-size: var(--fs-meta); padding-top: 1px; }
149
+ .bs-lb-side dd { margin: 0; overflow-wrap: anywhere; }
150
+ .bs-lb-path { font-family: var(--mono); }
151
+ .bs-lb-cm-head { font-size: var(--fs-meta); color: var(--muted); border-top: 1px solid var(--rule); padding-top: 10px; }
152
+ .bs-lb-cm { border: 0; background: none; text-align: left; display: grid; grid-template-columns: 18px minmax(0,1fr); gap: 8px; padding: 6px; border-radius: var(--r); cursor: pointer; font: var(--fs-ui) var(--font); color: var(--ink); }
153
+ .bs-lb-cm:hover { background: var(--fill-2); }
154
+ .bs-lb-excerpt { overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
155
+ .bs-lb-hint { margin: 0; font-size: var(--fs-meta); color: var(--muted); }
156
+ .bs-lb-stage .bs-lb-hint { padding: 40px; color: var(--media-ink); }
157
+ .bs-lb-strip { display: flex; gap: 6px; padding: 8px 12px 10px; overflow-x: auto; overscroll-behavior: contain; justify-content: safe center; }
158
+ .bs-lb-strip button { flex: none; width: 88px; aspect-ratio: 16/9; border-radius: 4px; border: 2px solid transparent; padding: 0; position: relative; overflow: hidden; cursor: pointer; background: none; opacity: .65; }
159
+ .bs-lb-strip button[aria-current="true"] { border-color: rgba(255,255,255,1); opacity: 1; }
160
+ .bs-lb-strip button:hover { opacity: 1; }
161
+ .bs-lb-strip img { width: 100%; height: 100%; object-fit: cover; }
162
+ .bs-lb-strip span { position: absolute; left: 4px; bottom: 2px; font: 600 10px var(--mono); color: var(--media-ink); text-shadow: 0 1px 2px rgba(0,0,0,.6); }
163
+ @media (max-width: 600px) {
164
+ .bs-lb-main { grid-template-columns: minmax(0,1fr); grid-template-rows: minmax(0,1fr) minmax(0,1fr); }
165
+ .bs-lb-side { border-left: 0; border-top: 1px solid var(--rule); }
166
+ .bs-lb-strip { display: none; }
167
+ .bs-lb-nav { width: 34px; height: 34px; }
168
+ .bs-lb-title { margin: 0; }
169
+ .bs-lb-back { font-size: 0; padding: 5px 4px; }
170
+ }
171
+ `;
172
+ var ICONS = {
173
+ back: "M15 18l-6-6 6-6",
174
+ next: "M9 18l6-6-6-6",
175
+ minus: "M10 14h4 M21 21l-5-5 M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0",
176
+ plus: "M10 6v8 M6 10h8 M21 21l-5-5 M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0",
177
+ full: "M15 3h6v6 M21 3l-7 7 M9 21H3v-6 M3 21l7-7",
178
+ comment: "M21 11a9 9 0 0 1-9 9H3l2-5a9 9 0 1 1 16-4"
179
+ };
180
+ function lightboxIcon(doc, name) {
181
+ const svg = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
182
+ svg.setAttribute("viewBox", "0 0 24 24");
183
+ svg.setAttribute("fill", "none");
184
+ svg.setAttribute("stroke", "currentColor");
185
+ svg.setAttribute("stroke-width", "1.8");
186
+ svg.setAttribute("stroke-linecap", "round");
187
+ svg.setAttribute("stroke-linejoin", "round");
188
+ svg.setAttribute("aria-hidden", "true");
189
+ const path = doc.createElementNS(svg.namespaceURI, "path");
190
+ path.setAttribute("d", ICONS[name]);
191
+ svg.append(path);
192
+ return svg;
193
+ }
194
+ function createImageLightbox(ctx, root, frame, options) {
195
+ const doc = root.ownerDocument;
196
+ const win = doc.defaultView;
197
+ const el = (tag, cls, text) => {
198
+ const node = doc.createElement(tag);
199
+ if (cls) node.className = cls;
200
+ if (text !== void 0) node.textContent = text;
201
+ return node;
202
+ };
203
+ let layer = null;
204
+ let stage = null;
205
+ let image = null;
206
+ let regions = null;
207
+ let side = null;
208
+ let index = 0;
209
+ let view = FIT;
210
+ let full = false;
211
+ let space = false;
212
+ let alive = true;
213
+ let loaded = false;
214
+ let generation = 0;
215
+ let gesture = null;
216
+ let draft = null;
217
+ let ownAnchor = null;
218
+ let threads = [];
219
+ let focused = null;
220
+ let ready = Promise.resolve(false);
221
+ let settleImage = () => void 0;
222
+ const pointers = /* @__PURE__ */ new Map();
223
+ let savedFull = null;
224
+ const fullStyle = {
225
+ position: "fixed",
226
+ "z-index": "var(--layer-viewer-full)",
227
+ inset: "0",
228
+ margin: "0",
229
+ "overflow-x": "hidden",
230
+ "overflow-y": "hidden",
231
+ "overscroll-behavior-x": "contain",
232
+ "overscroll-behavior-y": "contain"
233
+ };
234
+ const applyFullStyle = (on) => {
235
+ if (on) {
236
+ savedFull = {
237
+ style: Object.keys(fullStyle).map((key) => [
238
+ key,
239
+ root.style.getPropertyValue(key),
240
+ root.style.getPropertyPriority(key)
241
+ ])
242
+ };
243
+ for (const [key, value] of Object.entries(fullStyle)) root.style.setProperty(key, value);
244
+ } else if (savedFull) {
245
+ for (const [key, value, priority] of savedFull.style) {
246
+ if (value) root.style.setProperty(key, value, priority);
247
+ else root.style.removeProperty(key);
248
+ }
249
+ savedFull = null;
250
+ }
251
+ };
252
+ const narrow = () => (win?.innerWidth ?? 1440) <= 600;
253
+ const marking = () => options.canComment() && ctx.anchors.annotating.get() && !narrow() && !space;
254
+ const viewport = () => ({
255
+ width: stage?.getBoundingClientRect().width ?? 0,
256
+ height: stage?.getBoundingClientRect().height ?? 0
257
+ });
258
+ const base = () => lightboxSize(
259
+ { width: image?.naturalWidth ?? 0, height: image?.naturalHeight ?? 0 },
260
+ viewport(),
261
+ narrow()
262
+ );
263
+ const path = () => options.items()[index]?.path ?? null;
264
+ const relative = (at) => {
265
+ const r = stage?.getBoundingClientRect();
266
+ return {
267
+ x: at.x - (r?.left ?? 0) - (r?.width ?? 0) / 2,
268
+ y: at.y - (r?.top ?? 0) - (r?.height ?? 0) / 2
269
+ };
270
+ };
271
+ const imageRect = () => {
272
+ const r = stage?.getBoundingClientRect();
273
+ const b = base();
274
+ const width = b.width * view.scale;
275
+ const height = b.height * view.scale;
276
+ return {
277
+ left: (r?.left ?? 0) + ((r?.width ?? 0) - width) / 2 + view.x,
278
+ top: (r?.top ?? 0) + ((r?.height ?? 0) - height) / 2 + view.y,
279
+ width,
280
+ height
281
+ };
282
+ };
283
+ const fits = (p) => {
284
+ const r = imageRect();
285
+ return p.x >= r.left && p.x <= r.left + r.width && p.y >= r.top && p.y <= r.top + r.height;
286
+ };
287
+ const clearSelection = () => {
288
+ const selected = ctx.selection.get();
289
+ if (ownAnchor && selected?.kind === "anchor" && selected.anchor.id === ownAnchor)
290
+ ctx.selection.set(null);
291
+ ownAnchor = null;
292
+ draft = null;
293
+ };
294
+ const cancel = () => {
295
+ gesture = null;
296
+ draft = null;
297
+ pointers.clear();
298
+ };
299
+ const addRegion = (rect, old) => {
300
+ if (!regions || !stage) return;
301
+ const r = imageRect();
302
+ const s = stage.getBoundingClientRect();
303
+ const point = rect.w === 0 && rect.h === 0;
304
+ const mark = el("div", point ? "bs-lb-point" : `bs-lb-region${old ? " old" : ""}`);
305
+ mark.style.left = `${r.left - s.left + rect.x * r.width}px`;
306
+ mark.style.top = `${r.top - s.top + rect.y * r.height}px`;
307
+ if (!point) {
308
+ mark.style.width = `${rect.w * r.width}px`;
309
+ mark.style.height = `${rect.h * r.height}px`;
310
+ }
311
+ regions.append(mark);
312
+ };
313
+ const drawRegions = () => {
314
+ regions?.replaceChildren();
315
+ if (!loaded || !ctx.anchors.annotating.get() || narrow()) return;
316
+ for (const t of threads) {
317
+ const target = options.region(t.anchor);
318
+ if (t.status !== "resolved" && target?.index === index) addRegion(target.rect, true);
319
+ }
320
+ if (draft) addRegion(draft, false);
321
+ };
322
+ const paint = () => {
323
+ if (!stage || !image) return;
324
+ view = lightboxClamp(view, base(), viewport());
325
+ const r = imageRect();
326
+ const s = stage.getBoundingClientRect();
327
+ Object.assign(image.style, {
328
+ left: `${r.left - s.left}px`,
329
+ top: `${r.top - s.top}px`,
330
+ width: `${r.width}px`,
331
+ height: `${r.height}px`
332
+ });
333
+ const zoom2 = layer?.querySelector('[aria-label="\u9002\u5E94\u7A97\u53E3"]');
334
+ if (zoom2) zoom2.textContent = `${Math.round(view.scale * 100)}%`;
335
+ stage.dataset.mark = String(marking());
336
+ stage.dataset.pan = String(view.scale > 1 && !marking());
337
+ drawRegions();
338
+ options.notify();
339
+ };
340
+ const zoom = (factor, at = { x: 0, y: 0 }) => {
341
+ view = lightboxZoom(view, factor, at, base(), viewport());
342
+ paint();
343
+ };
344
+ const setFull = () => {
345
+ full = !full;
346
+ applyFullStyle(full);
347
+ if (layer) layer.dataset.full = String(full);
348
+ drawFullButton();
349
+ paint();
350
+ };
351
+ const makeButton = (label, icon, action, cls = "bs-lb-button") => {
352
+ const button = el("button", cls);
353
+ button.type = "button";
354
+ button.setAttribute("aria-label", label);
355
+ button.title = label;
356
+ button.append(lightboxIcon(doc, icon));
357
+ button.addEventListener("click", action);
358
+ return button;
359
+ };
360
+ const drawFullButton = () => {
361
+ const toggle = layer?.querySelector("[data-full-comment]");
362
+ if (toggle) {
363
+ toggle.hidden = !full || !options.canComment() || narrow();
364
+ toggle.setAttribute("aria-pressed", String(ctx.anchors.annotating.get()));
365
+ }
366
+ };
367
+ const drawSide = () => {
368
+ if (!side) return;
369
+ side.replaceChildren();
370
+ options.renderSide(side, index);
371
+ };
372
+ const close = () => {
373
+ if (!layer) return;
374
+ clearSelection();
375
+ applyFullStyle(false);
376
+ cancel();
377
+ generation++;
378
+ settleImage(false);
379
+ layer.remove();
380
+ layer = stage = image = regions = side = null;
381
+ full = false;
382
+ loaded = false;
383
+ options.returned(index);
384
+ options.notify();
385
+ };
386
+ const pinch = () => {
387
+ const [a, b] = [...pointers.values()];
388
+ return a && b ? {
389
+ distance: Math.hypot(a.x - b.x, a.y - b.y),
390
+ mid: { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }
391
+ } : null;
392
+ };
393
+ const bindStage = (canvas) => {
394
+ canvas.addEventListener(
395
+ "wheel",
396
+ (e) => {
397
+ if (!loaded) return;
398
+ e.preventDefault();
399
+ zoom(
400
+ Math.exp(-Math.max(-100, Math.min(100, e.deltaY)) * 2e-3 * (e.ctrlKey ? 4 : 1)),
401
+ relative({ x: e.clientX, y: e.clientY })
402
+ );
403
+ },
404
+ { passive: false }
405
+ );
406
+ canvas.addEventListener("dblclick", (e) => {
407
+ if (!loaded || marking()) return;
408
+ if (view.scale > 1) {
409
+ view = FIT;
410
+ paint();
411
+ } else zoom(2, relative({ x: e.clientX, y: e.clientY }));
412
+ });
413
+ canvas.addEventListener("pointerdown", (e) => {
414
+ if (!loaded || e.pointerType === "mouse" && e.button !== 0 || e.target instanceof Element && e.target.closest("button"))
415
+ return;
416
+ const at = { x: e.clientX, y: e.clientY };
417
+ pointers.set(e.pointerId, at);
418
+ canvas.setPointerCapture?.(e.pointerId);
419
+ if (pointers.size === 2) {
420
+ const p = pinch();
421
+ draft = null;
422
+ gesture = p ? { kind: "pinch", ...p, view } : null;
423
+ paint();
424
+ return;
425
+ }
426
+ if (pointers.size !== 1) return;
427
+ if (marking() && fits(at))
428
+ gesture = { kind: "mark", id: e.pointerId, start: at, view, moved: false };
429
+ else if (!marking() && view.scale > 1)
430
+ gesture = { kind: "pan", id: e.pointerId, start: at, view, moved: false };
431
+ });
432
+ canvas.addEventListener("pointermove", (e) => {
433
+ if (!pointers.has(e.pointerId)) return;
434
+ const at = { x: e.clientX, y: e.clientY };
435
+ pointers.set(e.pointerId, at);
436
+ const g = gesture;
437
+ if (!g) return;
438
+ if (g.kind === "pinch") {
439
+ const p = pinch();
440
+ if (!p || g.distance <= 0) return;
441
+ const z = lightboxZoom(
442
+ g.view,
443
+ p.distance / g.distance,
444
+ relative(g.mid),
445
+ base(),
446
+ viewport()
447
+ );
448
+ view = lightboxClamp(
449
+ { ...z, x: z.x + p.mid.x - g.mid.x, y: z.y + p.mid.y - g.mid.y },
450
+ base(),
451
+ viewport()
452
+ );
453
+ paint();
454
+ return;
455
+ }
456
+ if (g.id !== e.pointerId) return;
457
+ if (g.kind === "pan") {
458
+ view = lightboxClamp(
459
+ { ...g.view, x: g.view.x + at.x - g.start.x, y: g.view.y + at.y - g.start.y },
460
+ base(),
461
+ viewport()
462
+ );
463
+ paint();
464
+ return;
465
+ }
466
+ if (!g.moved && Math.hypot(at.x - g.start.x, at.y - g.start.y) <= LIGHTBOX_DRAG_PX) return;
467
+ g.moved = true;
468
+ draft = unitRectOf(imageRect(), g.start, at);
469
+ paint();
470
+ });
471
+ const release = (e, commit) => {
472
+ if (!pointers.delete(e.pointerId)) return;
473
+ const g = gesture;
474
+ if (g?.kind === "pinch") {
475
+ if (pointers.size < 2) {
476
+ gesture = null;
477
+ pointers.clear();
478
+ }
479
+ return;
480
+ }
481
+ if (!g || g.id !== e.pointerId) return;
482
+ gesture = null;
483
+ draft = null;
484
+ paint();
485
+ if (!commit || g.kind !== "mark" || !marking()) return;
486
+ const rect = unitRectOf(
487
+ imageRect(),
488
+ g.start,
489
+ g.moved ? { x: e.clientX, y: e.clientY } : g.start
490
+ );
491
+ const p = path();
492
+ if (!rect || !p) return;
493
+ const anchor = options.makeAnchor?.(index, rect);
494
+ if (!anchor) return;
495
+ ownAnchor = anchor.id;
496
+ ctx.threads.openAt(anchor);
497
+ };
498
+ canvas.addEventListener("pointerup", (e) => release(e, true));
499
+ canvas.addEventListener("pointercancel", (e) => release(e, false));
500
+ };
501
+ const open = (next) => {
502
+ const items = options.items();
503
+ if (!items[next]) return;
504
+ clearSelection();
505
+ cancel();
506
+ generation++;
507
+ const seq = generation;
508
+ settleImage(false);
509
+ index = next;
510
+ view = FIT;
511
+ loaded = false;
512
+ focused = null;
513
+ layer?.remove();
514
+ layer = el("section", "bs-lb");
515
+ layer.setAttribute("aria-label", options.labels.layer);
516
+ layer.dataset.full = String(full);
517
+ const bar = el("div", "bs-lb-bar");
518
+ const back = makeButton(options.labels.back, "back", close, "bs-lb-back");
519
+ back.append(options.labels.back);
520
+ bar.append(back);
521
+ const heading = options.heading(index);
522
+ const title = el("div", "bs-lb-title", heading.title);
523
+ if (heading.meta) title.append(el("span", void 0, heading.meta));
524
+ const fit = el("button", "bs-lb-zoom", "100%");
525
+ fit.type = "button";
526
+ fit.setAttribute("aria-label", "\u9002\u5E94\u7A97\u53E3");
527
+ fit.addEventListener("click", () => {
528
+ view = FIT;
529
+ paint();
530
+ });
531
+ const comment = makeButton(
532
+ "\u8BC4\u8BBA\uFF08C\uFF09",
533
+ "comment",
534
+ () => ctx.anchors.annotating.set(!ctx.anchors.annotating.get())
535
+ );
536
+ comment.dataset.fullComment = "";
537
+ bar.append(
538
+ title,
539
+ el("span", "bs-lb-grow"),
540
+ makeButton("\u7F29\u5C0F", "minus", () => zoom(1 / 1.25)),
541
+ fit,
542
+ makeButton("\u653E\u5927", "plus", () => zoom(1.25)),
543
+ makeButton("\u5168\u5C4F\uFF08F\uFF09", "full", setFull),
544
+ comment
545
+ );
546
+ const main = el("div", "bs-lb-main");
547
+ stage = el("div", "bs-lb-stage");
548
+ stage.setAttribute("aria-label", options.labels.stage);
549
+ stage.tabIndex = 0;
550
+ image = el("img", "bs-lb-image");
551
+ image.alt = items[index]?.alt ?? items[index]?.title ?? "";
552
+ regions = el("div", "bs-lb-regions");
553
+ stage.append(image, regions);
554
+ bindStage(stage);
555
+ const go = (delta) => {
556
+ const n = nextImage(index, delta, items.length);
557
+ if (n !== index) open(n);
558
+ };
559
+ const prev = makeButton(options.labels.previous, "back", () => go(-1), "bs-lb-nav prev");
560
+ const nextButton = makeButton(options.labels.next, "next", () => go(1), "bs-lb-nav next");
561
+ prev.disabled = index === 0;
562
+ nextButton.disabled = index === items.length - 1;
563
+ stage.append(prev, nextButton);
564
+ side = el("aside", "bs-lb-side");
565
+ side.setAttribute("aria-label", options.labels.side);
566
+ main.append(stage, side);
567
+ const strip = el("nav", "bs-lb-strip");
568
+ strip.setAttribute("aria-label", options.labels.strip);
569
+ items.forEach((item, i) => {
570
+ const button = el("button");
571
+ button.type = "button";
572
+ button.setAttribute("aria-label", options.labels.item(i));
573
+ button.setAttribute("aria-current", String(i === index));
574
+ const p2 = item.path;
575
+ if (p2) {
576
+ const thumb = el("img");
577
+ thumb.alt = "";
578
+ button.append(thumb);
579
+ void options.imageUrl(p2).then(
580
+ (url) => {
581
+ if (alive && seq === generation) thumb.src = url;
582
+ },
583
+ () => void 0
584
+ );
585
+ }
586
+ button.append(el("span", void 0, String(i + 1)));
587
+ button.addEventListener("click", () => {
588
+ if (i !== index) open(i);
589
+ });
590
+ strip.append(button);
591
+ });
592
+ layer.append(bar, main, strip);
593
+ frame.append(layer);
594
+ drawSide();
595
+ drawFullButton();
596
+ paint();
597
+ stage.focus({ preventScroll: true });
598
+ ready = new Promise((r) => {
599
+ settleImage = r;
600
+ });
601
+ const img = image;
602
+ const p = path();
603
+ img.addEventListener(
604
+ "load",
605
+ () => {
606
+ if (seq !== generation || !alive) return;
607
+ loaded = true;
608
+ if (focused) view = focusRegion(focused, base(), viewport());
609
+ paint();
610
+ settleImage(true);
611
+ },
612
+ { once: true }
613
+ );
614
+ img.addEventListener(
615
+ "error",
616
+ () => {
617
+ if (seq !== generation || !alive) return;
618
+ settleImage(false);
619
+ stage?.append(el("p", "bs-lb-hint", options.labels.error));
620
+ },
621
+ { once: true }
622
+ );
623
+ if (p)
624
+ void options.imageUrl(p).then(
625
+ (url) => {
626
+ if (seq === generation && alive) img.src = url;
627
+ },
628
+ () => {
629
+ if (seq === generation) img.dispatchEvent(new Event("error"));
630
+ }
631
+ );
632
+ else settleImage(false);
633
+ options.notify();
634
+ };
635
+ const locate = (anchor) => {
636
+ const target = options.region(anchor);
637
+ if (!target) return { at: "none" };
638
+ if (!layer || target.index !== index) return { at: "elsewhere" };
639
+ if (!loaded || !stage) return { at: "pending" };
640
+ const r = imageRect();
641
+ const s = stage.getBoundingClientRect();
642
+ const o = root.getBoundingClientRect();
643
+ const rect = target.rect;
644
+ const left = Math.max(s.left, r.left + rect.x * r.width);
645
+ const top = Math.max(s.top, r.top + rect.y * r.height);
646
+ const right = Math.min(s.right, r.left + (rect.x + rect.w) * r.width);
647
+ const bottom = Math.min(s.bottom, r.top + (rect.y + rect.h) * r.height);
648
+ if (left > right || top > bottom) return { at: "elsewhere" };
649
+ return {
650
+ at: "rects",
651
+ rects: [{ x: left - o.left, y: top - o.top, width: right - left, height: bottom - top }],
652
+ ...options.changed(anchor, target.path) ? { status: "changed" } : {}
653
+ };
654
+ };
655
+ const reveal = async (anchor) => {
656
+ const target = options.region(anchor);
657
+ if (!target) return false;
658
+ if (options.canComment()) ctx.anchors.annotating.set(true);
659
+ if (!layer || target.index !== index) open(target.index);
660
+ focused = target.rect;
661
+ if (loaded) {
662
+ view = focusRegion(target.rect, base(), viewport());
663
+ paint();
664
+ }
665
+ const seq = generation;
666
+ const ok = await ready;
667
+ return alive && ok && seq === generation;
668
+ };
669
+ const ownsKeys = () => {
670
+ const layers = doc.querySelectorAll("[data-band-layer]");
671
+ const own = root.closest("[data-band-layer]");
672
+ if (own !== (layers[layers.length - 1] ?? null)) return false;
673
+ return blockingDialog(void 0, {
674
+ querySelectorAll: (selector) => [...doc.querySelectorAll(selector)].filter((node) => !node.contains(root))
675
+ }) === null;
676
+ };
677
+ const onKeyDown = withImeGuard((e) => {
678
+ if (!layer || e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey || !ownsKeys()) return;
679
+ const typing = e.target instanceof HTMLElement && (e.target.isContentEditable || !!e.target.closest("input,textarea,select"));
680
+ if (typing) return;
681
+ if (e.key === "Escape") {
682
+ if (gesture?.kind === "mark") {
683
+ cancel();
684
+ paint();
685
+ e.preventDefault();
686
+ e.stopImmediatePropagation();
687
+ return;
688
+ }
689
+ e.preventDefault();
690
+ if (options.canComment() && ctx.anchors.annotating.get()) ctx.anchors.annotating.set(false);
691
+ else if (full) setFull();
692
+ else close();
693
+ return;
694
+ }
695
+ const items = options.items();
696
+ if (!items.length) return;
697
+ if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
698
+ const n = nextImage(index, e.key === "ArrowLeft" ? -1 : 1, items.length);
699
+ if (n !== index) open(n);
700
+ } else if (e.key === "+" || e.key === "=") zoom(1.25);
701
+ else if (e.key === "-") zoom(1 / 1.25);
702
+ else if (e.key === "0") {
703
+ view = FIT;
704
+ paint();
705
+ } else if (e.code === "KeyF" || e.key.toLowerCase() === "f") {
706
+ if (!e.repeat) setFull();
707
+ } else if (e.code === "Space" || e.key === " ") {
708
+ space = true;
709
+ paint();
710
+ } else return;
711
+ e.preventDefault();
712
+ e.stopPropagation();
713
+ });
714
+ const onKeyUp = (e) => {
715
+ if (e.code === "Space" || e.key === " ") {
716
+ space = false;
717
+ paint();
718
+ }
719
+ };
720
+ const resize = () => {
721
+ drawFullButton();
722
+ drawSide();
723
+ paint();
724
+ };
725
+ const blur = () => {
726
+ space = false;
727
+ cancel();
728
+ paint();
729
+ };
730
+ doc.addEventListener("keydown", onKeyDown);
731
+ doc.addEventListener("keyup", onKeyUp);
732
+ win?.addEventListener("resize", resize);
733
+ win?.addEventListener("blur", blur);
734
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(resize);
735
+ observer?.observe(frame);
736
+ const offMode = ctx.anchors.annotating.subscribe(() => {
737
+ cancel();
738
+ drawFullButton();
739
+ paint();
740
+ });
741
+ return {
742
+ open,
743
+ locate,
744
+ reveal,
745
+ setThreads(next) {
746
+ threads = next;
747
+ drawSide();
748
+ drawRegions();
749
+ },
750
+ close,
751
+ refresh() {
752
+ drawSide();
753
+ drawRegions();
754
+ },
755
+ dispose() {
756
+ alive = false;
757
+ close();
758
+ observer?.disconnect();
759
+ offMode();
760
+ doc.removeEventListener("keydown", onKeyDown);
761
+ doc.removeEventListener("keyup", onKeyUp);
762
+ win?.removeEventListener("resize", resize);
763
+ win?.removeEventListener("blur", blur);
764
+ }
765
+ };
766
+ }
767
+ export {
768
+ DRAG_PX,
769
+ FIT,
770
+ LIGHTBOX_DRAG_PX,
771
+ LIGHTBOX_MAX_SCALE,
772
+ LIGHTBOX_STYLE,
773
+ MAX_SCALE,
774
+ MIN_SCALE,
775
+ SNAP_SCALE,
776
+ clampView,
777
+ createImageLightbox,
778
+ fitSize,
779
+ focusRegion,
780
+ lightboxClamp,
781
+ lightboxIcon,
782
+ lightboxSize,
783
+ lightboxZoom,
784
+ nextImage,
785
+ unitRectFrom,
786
+ unitRectOf,
787
+ zoomAt
788
+ };