dsh-figmark 1.2.2 → 1.2.4
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/CHANGELOG.md +37 -0
- package/README.md +8 -0
- package/lib/client.js +1508 -303
- package/lib/index.js +20 -5
- package/market/index.json +2 -2
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -73,15 +73,17 @@ window.__ModuleLoader__.load({
|
|
|
73
73
|
reveal_hidden: "元素目前不可见,请先展开或翻到对应页面",
|
|
74
74
|
reveal_wrong_surface: "无法切换到标注所在的页面",
|
|
75
75
|
reveal_timeout: "页面没有响应定位请求",
|
|
76
|
-
serviceKicker: "LOCAL RUNTIME",
|
|
77
76
|
serviceTitle: "本机网页",
|
|
78
|
-
serviceHint: "
|
|
79
|
-
serviceSearch: "
|
|
77
|
+
serviceHint: "打开正在运行的本机网页,直接圈画与批注。",
|
|
78
|
+
serviceSearch: "搜索页面,或输入 localhost:5173",
|
|
80
79
|
serviceOpen: "打开",
|
|
81
80
|
serviceOpening: "正在连接",
|
|
82
81
|
serviceLocal: "正在运行",
|
|
83
82
|
serviceScanning: "检索中",
|
|
84
83
|
serviceRescan: "重新检索",
|
|
84
|
+
serviceNoMatch: "没有匹配的页面",
|
|
85
|
+
serviceChooseMatch: "找到多个页面,请选择下方的一项。",
|
|
86
|
+
serviceSearchHint: "换个关键词,或输入完整的本机地址。",
|
|
85
87
|
serviceEmpty: "没有发现本机服务",
|
|
86
88
|
serviceEmptyHint: "先启动开发服务器,或在上方直接输入地址。",
|
|
87
89
|
serviceBadAddress: "请输入本机 HTTP 地址",
|
|
@@ -169,18 +171,25 @@ window.__ModuleLoader__.load({
|
|
|
169
171
|
detailHint: "当前选中",
|
|
170
172
|
detailUnset: "未选中",
|
|
171
173
|
detailIdle: "在图上点一下、框一块或画一笔,就能为那处写说明。",
|
|
172
|
-
cardOpen: "
|
|
174
|
+
cardOpen: "打开原文件",
|
|
173
175
|
cardOpenLive: "回到本机网页",
|
|
174
176
|
cardCaptureOmitted: "位置标注",
|
|
175
177
|
cardCaptureFailed: "截图失败",
|
|
176
|
-
cardMore: "
|
|
178
|
+
cardMore: "查看其余 {n} 处",
|
|
179
|
+
cardExpand: "查看完整批注",
|
|
180
|
+
cardCollapse: "收起详情",
|
|
181
|
+
cardOpenFailed: "未能打开原内容,请重试。",
|
|
177
182
|
cardGoal: "目标",
|
|
178
183
|
cardPage: "第 {n} 页",
|
|
184
|
+
cardPages: "共 {n} 页",
|
|
179
185
|
cardCount: "{n} 处标注",
|
|
180
186
|
pendingCount: "{n}",
|
|
181
187
|
pendingRemove: "移除附图标注",
|
|
182
|
-
pendingSendHint: "
|
|
183
|
-
|
|
188
|
+
pendingSendHint: "发送当前消息及全部待发送标注",
|
|
189
|
+
pendingSendMessage: "发送消息",
|
|
190
|
+
pendingSending: "发送中…",
|
|
191
|
+
pendingUnavailable: "当前输入框暂时无法发送。",
|
|
192
|
+
pendingSendNow: "发送这份标注",
|
|
184
193
|
pendingFailed: "没能发送:"
|
|
185
194
|
};
|
|
186
195
|
/** English copy. */
|
|
@@ -223,15 +232,17 @@ window.__ModuleLoader__.load({
|
|
|
223
232
|
reveal_hidden: "The element is not visible yet",
|
|
224
233
|
reveal_wrong_surface: "Could not switch to the mark’s page",
|
|
225
234
|
reveal_timeout: "The page did not answer the locate request",
|
|
226
|
-
serviceKicker: "LOCAL RUNTIME",
|
|
227
235
|
serviceTitle: "Local pages",
|
|
228
|
-
serviceHint: "
|
|
229
|
-
serviceSearch: "
|
|
236
|
+
serviceHint: "Open a running local page to draw and comment.",
|
|
237
|
+
serviceSearch: "Find a page or enter localhost:5173",
|
|
230
238
|
serviceOpen: "Open",
|
|
231
239
|
serviceOpening: "Connecting",
|
|
232
240
|
serviceLocal: "Running",
|
|
233
241
|
serviceScanning: "Scanning",
|
|
234
242
|
serviceRescan: "Scan again",
|
|
243
|
+
serviceNoMatch: "No matching pages",
|
|
244
|
+
serviceChooseMatch: "Several pages match. Choose one below.",
|
|
245
|
+
serviceSearchHint: "Try another keyword or enter the full local address.",
|
|
235
246
|
serviceEmpty: "No local services found",
|
|
236
247
|
serviceEmptyHint: "Start a development server, or type its address above.",
|
|
237
248
|
serviceBadAddress: "Enter a local HTTP address",
|
|
@@ -319,18 +330,25 @@ window.__ModuleLoader__.load({
|
|
|
319
330
|
detailHint: "selected",
|
|
320
331
|
detailUnset: "nothing selected",
|
|
321
332
|
detailIdle: "Click the figure, or box a region or draw a stroke, to write about that spot.",
|
|
322
|
-
cardOpen: "Open
|
|
333
|
+
cardOpen: "Open source file",
|
|
323
334
|
cardOpenLive: "Return to the local page",
|
|
324
335
|
cardCaptureOmitted: "location only",
|
|
325
336
|
cardCaptureFailed: "screenshot failed",
|
|
326
|
-
cardMore: "
|
|
337
|
+
cardMore: "Show {n} more",
|
|
338
|
+
cardExpand: "Review all annotations",
|
|
339
|
+
cardCollapse: "Collapse details",
|
|
340
|
+
cardOpenFailed: "Could not open the source. Try again.",
|
|
327
341
|
cardGoal: "Goal",
|
|
328
342
|
cardPage: "page {n}",
|
|
343
|
+
cardPages: "{n} pages",
|
|
329
344
|
cardCount: "{n} annotations",
|
|
330
345
|
pendingCount: "{n}",
|
|
331
346
|
pendingRemove: "Remove the annotation",
|
|
332
|
-
pendingSendHint: "Send
|
|
333
|
-
|
|
347
|
+
pendingSendHint: "Send the message and all staged reviews",
|
|
348
|
+
pendingSendMessage: "Send message",
|
|
349
|
+
pendingSending: "Sending…",
|
|
350
|
+
pendingUnavailable: "This composer cannot send right now.",
|
|
351
|
+
pendingSendNow: "Send this review",
|
|
334
352
|
pendingFailed: "Could not send: "
|
|
335
353
|
};
|
|
336
354
|
/** Locale namespace this dictionary registers under. */
|
|
@@ -3808,6 +3826,7 @@ window.__ModuleLoader__.load({
|
|
|
3808
3826
|
fontSize: pinFontSizeOf(unit),
|
|
3809
3827
|
fontWeight: 700,
|
|
3810
3828
|
fill: PIN_NUMBER_COLOR,
|
|
3829
|
+
stroke: "none",
|
|
3811
3830
|
textAnchor: "middle",
|
|
3812
3831
|
dominantBaseline: "central",
|
|
3813
3832
|
children: number
|
|
@@ -4026,6 +4045,7 @@ window.__ModuleLoader__.load({
|
|
|
4026
4045
|
fontSize,
|
|
4027
4046
|
fontWeight: 600,
|
|
4028
4047
|
fill: CODE_TEXT_COLOR,
|
|
4048
|
+
stroke: "none",
|
|
4029
4049
|
dominantBaseline: "central",
|
|
4030
4050
|
children: code
|
|
4031
4051
|
})] });
|
|
@@ -4813,14 +4833,14 @@ window.__ModuleLoader__.load({
|
|
|
4813
4833
|
* Upload one file as a draft attachment without putting it in the rail yet.
|
|
4814
4834
|
* @param conversation - the conversation service.
|
|
4815
4835
|
* @param sessionId - session the draft belongs to.
|
|
4816
|
-
* @param
|
|
4836
|
+
* @param files - the rendered review images to hold.
|
|
4817
4837
|
* @returns the ids the send will add, empty when the file could not be made into one.
|
|
4818
4838
|
*/
|
|
4819
|
-
function
|
|
4839
|
+
function holdFiles(conversation, sessionId, files) {
|
|
4820
4840
|
const create = conversation.createDrafts?.bind(conversation);
|
|
4821
|
-
if (
|
|
4841
|
+
if (files.length === 0 || create === void 0) return [];
|
|
4822
4842
|
try {
|
|
4823
|
-
return create(sessionId,
|
|
4843
|
+
return create(sessionId, files);
|
|
4824
4844
|
} catch {
|
|
4825
4845
|
return [];
|
|
4826
4846
|
}
|
|
@@ -4831,9 +4851,10 @@ window.__ModuleLoader__.load({
|
|
|
4831
4851
|
* @param sessionId - session the draft belongs to.
|
|
4832
4852
|
* @param message - the message, verbatim.
|
|
4833
4853
|
* @param file - the review picture, when the surface has one to attach.
|
|
4854
|
+
* @param additionalFiles - marked page images of a multi-page review.
|
|
4834
4855
|
* @returns what was armed, or undefined when this session cannot be armed.
|
|
4835
4856
|
*/
|
|
4836
|
-
function armReview(conversation, sessionId, message, file) {
|
|
4857
|
+
function armReview(conversation, sessionId, message, file, additionalFiles = []) {
|
|
4837
4858
|
let shell;
|
|
4838
4859
|
try {
|
|
4839
4860
|
shell = conversation.input.shell(sessionId);
|
|
@@ -4841,10 +4862,12 @@ window.__ModuleLoader__.load({
|
|
|
4841
4862
|
return;
|
|
4842
4863
|
}
|
|
4843
4864
|
if (typeof shell.submit !== "function") return void 0;
|
|
4865
|
+
const drafts = holdFiles(conversation, sessionId, file === void 0 ? additionalFiles : [file, ...additionalFiles]);
|
|
4844
4866
|
return {
|
|
4845
4867
|
shell,
|
|
4846
4868
|
text: message,
|
|
4847
|
-
attachmentIds:
|
|
4869
|
+
attachmentIds: drafts.map((draft) => draft.id),
|
|
4870
|
+
previewUrls: drafts.flatMap((draft) => draft.previewUrl === void 0 ? [] : [draft.previewUrl])
|
|
4848
4871
|
};
|
|
4849
4872
|
}
|
|
4850
4873
|
/**
|
|
@@ -4907,12 +4930,13 @@ window.__ModuleLoader__.load({
|
|
|
4907
4930
|
for (const claim of claims) {
|
|
4908
4931
|
const { text, attachmentIds } = claim.armed;
|
|
4909
4932
|
if (draft.includes(text)) continue;
|
|
4910
|
-
|
|
4911
|
-
|
|
4933
|
+
const missing = attachmentIds.filter((id) => !live.attachmentIds?.includes(id));
|
|
4934
|
+
if (missing.length > 0) {
|
|
4935
|
+
if (!attach(shell, missing)) {
|
|
4912
4936
|
unwind(shell, typed, added);
|
|
4913
4937
|
return;
|
|
4914
4938
|
}
|
|
4915
|
-
added.push(...
|
|
4939
|
+
added.push(...missing);
|
|
4916
4940
|
}
|
|
4917
4941
|
draft = draft.trim() === "" ? text : `${draft}\n\n${text}`;
|
|
4918
4942
|
}
|
|
@@ -4972,7 +4996,7 @@ window.__ModuleLoader__.load({
|
|
|
4972
4996
|
try {
|
|
4973
4997
|
shell.setDraft(merge.typed);
|
|
4974
4998
|
} catch {}
|
|
4975
|
-
for (const claim of claims) for (const id of claim.armed.attachmentIds) shell.removeAttachment?.(id);
|
|
4999
|
+
for (const claim of claims) if (claim.armed.staged !== true) for (const id of claim.armed.attachmentIds) shell.removeAttachment?.(id);
|
|
4976
5000
|
}
|
|
4977
5001
|
/**
|
|
4978
5002
|
* Take one card off whatever patch its shell is currently carrying.
|
|
@@ -5045,9 +5069,9 @@ window.__ModuleLoader__.load({
|
|
|
5045
5069
|
const send = () => {
|
|
5046
5070
|
try {
|
|
5047
5071
|
original.apply(shell, args);
|
|
5048
|
-
settleClaims(shell, claims, merge);
|
|
5049
5072
|
} finally {
|
|
5050
5073
|
waiting = false;
|
|
5074
|
+
settleClaims(shell, claims, merge);
|
|
5051
5075
|
}
|
|
5052
5076
|
};
|
|
5053
5077
|
if (wroteThrough(shell, merge)) {
|
|
@@ -5100,13 +5124,27 @@ window.__ModuleLoader__.load({
|
|
|
5100
5124
|
*/
|
|
5101
5125
|
function parkInComposer(input) {
|
|
5102
5126
|
const { document: document_, locale, paths, sessionId } = input;
|
|
5103
|
-
|
|
5127
|
+
let armed = input.conversation === void 0 ? void 0 : armReview(input.conversation, sessionId, "", input.file, input.files);
|
|
5128
|
+
const planned = (input.file === void 0 ? 0 : 1) + (input.files?.length ?? 0);
|
|
5129
|
+
const attached = armed?.attachmentIds.length ?? planned;
|
|
5130
|
+
const pageCapture = input.pageCapture === "attached" && attached === 0 ? "failed" : input.pageCapture;
|
|
5131
|
+
const message = buildAnnotationMessage(document_, paths, locale, attached > 0, Math.min(input.pageImages ?? 0, attached), pageCapture);
|
|
5132
|
+
if (armed !== void 0 && input.conversation !== void 0) {
|
|
5133
|
+
const ids = armed.attachmentIds.length > 0 ? armed.attachmentIds : holdFiles(input.conversation, sessionId, [new File([message], "annotation.txt", { type: "text/plain" })]).map((draft) => draft.id);
|
|
5134
|
+
if (ids.length > 0 && attach(armed.shell, ids)) armed = {
|
|
5135
|
+
...armed,
|
|
5136
|
+
attachmentIds: ids,
|
|
5137
|
+
staged: true
|
|
5138
|
+
};
|
|
5139
|
+
else {
|
|
5140
|
+
for (const id of ids) input.conversation.releaseDraftAttachment?.(id);
|
|
5141
|
+
armed = void 0;
|
|
5142
|
+
}
|
|
5143
|
+
}
|
|
5104
5144
|
rememberDelivered(message, input.thumb);
|
|
5105
|
-
const block = matchAnnotation(message, 0);
|
|
5106
|
-
const armed = input.conversation === void 0 ? void 0 : armReview(input.conversation, sessionId, message, input.file);
|
|
5107
5145
|
parkAnnotation({
|
|
5108
5146
|
sessionId,
|
|
5109
|
-
data:
|
|
5147
|
+
data: matchAnnotation(message, 0)?.data ?? {
|
|
5110
5148
|
file: document_.figure.path,
|
|
5111
5149
|
scope: "",
|
|
5112
5150
|
target: "",
|
|
@@ -5117,7 +5155,10 @@ window.__ModuleLoader__.load({
|
|
|
5117
5155
|
},
|
|
5118
5156
|
raw: message,
|
|
5119
5157
|
conversation: input.conversation,
|
|
5120
|
-
armed
|
|
5158
|
+
armed: armed === void 0 ? void 0 : {
|
|
5159
|
+
...armed,
|
|
5160
|
+
text: message
|
|
5161
|
+
},
|
|
5121
5162
|
send: input.send
|
|
5122
5163
|
});
|
|
5123
5164
|
}
|
|
@@ -5498,9 +5539,15 @@ window.__ModuleLoader__.load({
|
|
|
5498
5539
|
*/
|
|
5499
5540
|
function figureSvg(layer, lit = []) {
|
|
5500
5541
|
if (layer.kind === "raster") return `<image x="0" y="0" width="${layer.width}" height="${layer.height}" href="${layer.dataUrl}"/>`;
|
|
5501
|
-
const
|
|
5502
|
-
const
|
|
5503
|
-
|
|
5542
|
+
const litMarkup = lightMarkup(layer.markup, lit);
|
|
5543
|
+
const root = new DOMParser().parseFromString(litMarkup, "image/svg+xml").documentElement;
|
|
5544
|
+
root.setAttribute("x", "0");
|
|
5545
|
+
root.setAttribute("y", "0");
|
|
5546
|
+
root.setAttribute("width", String(layer.width));
|
|
5547
|
+
root.setAttribute("height", String(layer.height));
|
|
5548
|
+
if (layer.viewBox !== void 0) root.setAttribute("viewBox", layer.viewBox);
|
|
5549
|
+
root.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
5550
|
+
return new XMLSerializer().serializeToString(root);
|
|
5504
5551
|
}
|
|
5505
5552
|
/**
|
|
5506
5553
|
* Encode bytes as a `data:` URL.
|
|
@@ -8784,19 +8831,20 @@ body[data-ds-dark-theme] .da-root{--da-quiet:#9aa0a8}
|
|
|
8784
8831
|
border:0.5px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));border-radius:12px;
|
|
8785
8832
|
background:var(--dsw-alias-bg-base,rgba(255,255,255,.72));
|
|
8786
8833
|
box-shadow:0 1px 2px rgba(20,40,80,.04);white-space:normal}
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
.da-card-head{display:flex;align-items:center;gap:8px;min-width:0;padding:7px 9px;
|
|
8834
|
+
/* Review, source navigation and delivery are separate native buttons. */
|
|
8835
|
+
.da-card button,.da-staged-send{appearance:none;font:inherit;color:inherit;cursor:pointer}
|
|
8836
|
+
.da-card button:focus-visible,.da-staged-send:focus-visible{outline:2px solid var(--da-accent,#176df2);outline-offset:-2px}
|
|
8837
|
+
.da-card button:hover,.da-staged-send:hover{background:var(--dsw-alias-bg-layer-2,rgba(128,128,128,.08));color:var(--da-accent,#176df2)}
|
|
8838
|
+
.da-card-head{width:100%;box-sizing:border-box;border:0;text-align:left;display:flex;align-items:center;gap:8px;min-width:0;padding:7px 9px;
|
|
8792
8839
|
border-bottom:0.5px solid var(--dsw-alias-border-l2,rgba(128,128,128,.2));
|
|
8793
8840
|
background:var(--dsw-alias-bg-layer-1,rgba(128,128,128,.025))}
|
|
8841
|
+
.da-card-head-static{cursor:default}
|
|
8794
8842
|
.da-card-shot{position:relative;display:flex;flex:0 0 auto;align-items:center;justify-content:center;width:34px;height:34px;overflow:hidden;border-radius:7px;
|
|
8795
8843
|
border:0.5px solid var(--dsw-alias-border-l2,rgba(128,128,128,.18));
|
|
8796
8844
|
background:var(--dsw-alias-bg-layer-3,rgba(128,128,128,.1));color:var(--da-accent,#176df2)}
|
|
8797
8845
|
/* The picture covers the glyph; a picture that fails hides itself, and the glyph
|
|
8798
8846
|
underneath it all along is what is left. A miss is never an empty box. */
|
|
8799
|
-
.da-card-shot img{position:absolute;inset:0;width:100%;height:100%;object-fit:
|
|
8847
|
+
.da-card-shot img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
|
|
8800
8848
|
.da-card-shot img[hidden]{display:none}
|
|
8801
8849
|
.da-card-glyph{width:18px;height:18px}
|
|
8802
8850
|
.da-card-text{display:flex;flex:1 1 auto;flex-direction:column;gap:2px;min-width:0}
|
|
@@ -8819,10 +8867,11 @@ button.da-card:focus-visible{outline:2px solid var(--da-accent,#176df2);outline-
|
|
|
8819
8867
|
border-radius:50%;background:var(--dsw-alias-bg-layer-3,rgba(128,128,128,.1));
|
|
8820
8868
|
color:var(--dsw-alias-label-tertiary,inherit)}
|
|
8821
8869
|
.da-card-go svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
|
|
8822
|
-
|
|
8870
|
+
.da-card-head:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242,.14));color:var(--da-accent,#176df2)}
|
|
8823
8871
|
/* Each mark is a compact record, not a line of debug output. The row head answers
|
|
8824
8872
|
"which mark and where"; the element name and selector follow only when the message
|
|
8825
8873
|
carried them; the reader's sentence is the strongest text. */
|
|
8874
|
+
.da-card[data-expanded="true"] .da-card-go{transform:rotate(90deg)}
|
|
8826
8875
|
.da-card-body{display:flex;flex-direction:column;min-width:0;padding:0 9px 2px}
|
|
8827
8876
|
.da-card-row{display:flex;flex-direction:column;gap:2px;min-width:0;padding:6px 0}
|
|
8828
8877
|
.da-card-row+.da-card-row{border-top:0.5px solid var(--dsw-alias-border-l2,rgba(128,128,128,.14))}
|
|
@@ -8924,21 +8973,23 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
8924
8973
|
background:none;font:inherit;font-size:13px;line-height:1;cursor:pointer;opacity:.65}
|
|
8925
8974
|
.da-staged-result-x:hover{opacity:1;background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.72))}
|
|
8926
8975
|
.da-staged-result-x:focus-visible{outline:2px solid var(--da-accent,#176df2);outline-offset:1px}
|
|
8927
|
-
/*
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8976
|
+
/* Attachments belong to the message. A review may carry several PDF pages,
|
|
8977
|
+
and a message may also carry unrelated files; its source is not that snapshot. */
|
|
8978
|
+
.da-card-actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:4px;padding:3px 5px;
|
|
8979
|
+
border-top:0.5px solid var(--dsw-alias-border-l2,rgba(128,128,128,.2))}
|
|
8980
|
+
.da-card-toggle,.da-card-source{border:0;border-radius:6px;background:none;padding:6px 4px;min-height:30px;font-size:11px!important}
|
|
8981
|
+
.da-card-source{color:var(--da-accent,#176df2)!important}
|
|
8982
|
+
.da-card-error{padding:6px 9px;color:var(--dsw-alias-label-error,#c0392b);font-size:11px}
|
|
8983
|
+
.da-card-locator{margin-left:22px;overflow-wrap:anywhere;white-space:pre-wrap;color:var(--dsw-alias-label-tertiary,inherit);
|
|
8984
|
+
font:10px/1.5 var(--dsw-font-family-mono,ui-monospace,SFMono-Regular,Menlo,monospace)}
|
|
8985
|
+
.da-card[data-expanded="true"] .da-card-said{display:block;white-space:pre-wrap;-webkit-line-clamp:unset}
|
|
8986
|
+
.da-card[data-expanded="true"] .da-card-name{white-space:normal;overflow-wrap:anywhere}
|
|
8987
|
+
.da-card-target{overflow-wrap:anywhere}
|
|
8988
|
+
.da-staged-type{font-size:9px;font-weight:650;letter-spacing:.04em;color:var(--dsw-alias-label-tertiary,inherit)}
|
|
8989
|
+
.da-staged-details{display:block;width:min(440px,100%);margin-top:4px}
|
|
8990
|
+
.da-staged-details[hidden]{display:none}
|
|
8991
|
+
.da-staged-send{border:0;border-radius:6px;background:none;min-height:28px;padding:4px 8px;font-size:11px;color:var(--da-accent,#176df2)}
|
|
8992
|
+
.da-staged-send[disabled]{opacity:.6;cursor:default}
|
|
8942
8993
|
|
|
8943
8994
|
/* --- local service browser ------------------------------------------------ */
|
|
8944
8995
|
.da-service-shell{display:flex;flex-direction:column;height:100%;min-height:0}
|
|
@@ -8990,7 +9041,7 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
8990
9041
|
.da-services{--da-service-ink:var(--dsw-alias-label-primary,#1b1d20);
|
|
8991
9042
|
--da-service-muted:var(--dsw-alias-label-tertiary,#787d85);
|
|
8992
9043
|
--da-service-line:color-mix(in srgb,var(--dsw-alias-border-l2,#c9cbd0) 66%,transparent);
|
|
8993
|
-
box-sizing:border-box;height:100%;overflow:auto;padding:
|
|
9044
|
+
box-sizing:border-box;height:100%;overflow:auto;padding:24px 20px 32px;width:100%;
|
|
8994
9045
|
color:var(--da-service-ink);
|
|
8995
9046
|
background:
|
|
8996
9047
|
radial-gradient(circle at 18% 0%,color-mix(in srgb,#4c8a63 8%,transparent),transparent 36%),
|
|
@@ -8998,10 +9049,7 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
8998
9049
|
.da-services-head,.da-service-search,.da-services-toolbar,.da-service-list,.da-services-error{
|
|
8999
9050
|
width:min(480px,100%);margin-left:auto;margin-right:auto}
|
|
9000
9051
|
.da-services-head{padding:0 4px 18px}
|
|
9001
|
-
.da-services-
|
|
9002
|
-
font:9px/1.2 var(--dsw-font-family-mono,ui-monospace,SFMono-Regular,Menlo,monospace);
|
|
9003
|
-
letter-spacing:.16em}
|
|
9004
|
-
.da-services-head h1{margin:0;font-size:clamp(25px,4.5vw,34px);font-weight:680;line-height:1.08;letter-spacing:-.035em}
|
|
9052
|
+
.da-services-head h1{margin:0;font-size:20px;font-weight:680;line-height:1.08;letter-spacing:-.035em}
|
|
9005
9053
|
.da-services-head p{max-width:410px;margin:9px 0 0;color:var(--da-service-muted);font-size:12px;line-height:1.65}
|
|
9006
9054
|
.da-service-search{display:flex;align-items:center;gap:9px;box-sizing:border-box;height:48px;padding:0 7px 0 14px;
|
|
9007
9055
|
border:1px solid var(--da-service-line);border-radius:15px;
|
|
@@ -9456,6 +9504,46 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
9456
9504
|
]
|
|
9457
9505
|
});
|
|
9458
9506
|
}
|
|
9507
|
+
/** Put one more copy beside the existing sheet. */
|
|
9508
|
+
function CopyIcon() {
|
|
9509
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
9510
|
+
className: "da-ico",
|
|
9511
|
+
viewBox: "0 0 24 24",
|
|
9512
|
+
"aria-hidden": "true",
|
|
9513
|
+
focusable: "false",
|
|
9514
|
+
children: [
|
|
9515
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
9516
|
+
x: "8",
|
|
9517
|
+
y: "8",
|
|
9518
|
+
width: "11.5",
|
|
9519
|
+
height: "11.5",
|
|
9520
|
+
rx: "2.2"
|
|
9521
|
+
}),
|
|
9522
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M16 8V6.5a2.2 2.2 0 0 0-2.2-2.2H6.7a2.2 2.2 0 0 0-2.2 2.2v7.1A2.2 2.2 0 0 0 6.7 15.8H8" }),
|
|
9523
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M13.75 11.3v5M11.25 13.8h5" })
|
|
9524
|
+
]
|
|
9525
|
+
});
|
|
9526
|
+
}
|
|
9527
|
+
/** Save a file out of the editor. */
|
|
9528
|
+
function DownloadIcon() {
|
|
9529
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
9530
|
+
className: "da-ico",
|
|
9531
|
+
viewBox: "0 0 24 24",
|
|
9532
|
+
"aria-hidden": "true",
|
|
9533
|
+
focusable: "false",
|
|
9534
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 3.5v11M7.8 10.5 12 14.7l4.2-4.2" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4.5 16.5v2a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2v-2" })]
|
|
9535
|
+
});
|
|
9536
|
+
}
|
|
9537
|
+
/** Dismiss the editor without suggesting that the draft should be saved. */
|
|
9538
|
+
function CloseIcon() {
|
|
9539
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
9540
|
+
className: "da-ico",
|
|
9541
|
+
viewBox: "0 0 24 24",
|
|
9542
|
+
"aria-hidden": "true",
|
|
9543
|
+
focusable: "false",
|
|
9544
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M6.5 6.5l11 11M17.5 6.5l-11 11" })
|
|
9545
|
+
});
|
|
9546
|
+
}
|
|
9459
9547
|
/** A step back, as the usual curved arrow. */
|
|
9460
9548
|
function UndoIcon() {
|
|
9461
9549
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
@@ -9539,7 +9627,7 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
9539
9627
|
*/
|
|
9540
9628
|
const travelHeight = (fraction) => `calc(var(--da-track-inset) + var(--da-knob-size) / 2 + (100% - 2 * var(--da-track-inset) - var(--da-knob-size)) * ${fraction})`;
|
|
9541
9629
|
/** Tools in toolbar order. */
|
|
9542
|
-
const TOOLS = [
|
|
9630
|
+
const TOOLS$1 = [
|
|
9543
9631
|
{
|
|
9544
9632
|
tool: "select",
|
|
9545
9633
|
label: "toolSelect"
|
|
@@ -15079,7 +15167,7 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
15079
15167
|
stripRef: paletteRef,
|
|
15080
15168
|
style: paletteStyle,
|
|
15081
15169
|
handle: paletteHandle,
|
|
15082
|
-
tools: isVector || isHtml ? TOOLS : TOOLS.filter((entry) => !ELEMENT_TOOLS.has(entry.tool)),
|
|
15170
|
+
tools: isVector || isHtml ? TOOLS$1 : TOOLS$1.filter((entry) => !ELEMENT_TOOLS.has(entry.tool)),
|
|
15083
15171
|
tool,
|
|
15084
15172
|
elementGuide: tool === "element" ? {
|
|
15085
15173
|
label: pickLevel === void 0 ? `${t("toolElementMode")}, ${t("toolElementHint")}, ${t("toolElementKeys")}` : `${t("toolElementMode")}, ${t("toolElementDepth", {
|
|
@@ -15511,6 +15599,32 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
15511
15599
|
});
|
|
15512
15600
|
}
|
|
15513
15601
|
//#endregion
|
|
15602
|
+
//#region src/client/annotation-subject.ts
|
|
15603
|
+
/** Shared display identity for a review, before and after sending. */
|
|
15604
|
+
/**
|
|
15605
|
+
* Identify the reviewed source, independently of its mark tools or screenshot.
|
|
15606
|
+
* @param data - the immutable message facts.
|
|
15607
|
+
* @returns a readable name, media badge, and whether navigation belongs to the service tab.
|
|
15608
|
+
*/
|
|
15609
|
+
function annotationSubject(data) {
|
|
15610
|
+
const address = data.html ? loopbackAddressOf(data.file) : void 0;
|
|
15611
|
+
if (address !== void 0) {
|
|
15612
|
+
const url = new URL(address);
|
|
15613
|
+
return {
|
|
15614
|
+
name: `${url.host}${url.pathname === "/" ? "" : url.pathname}`,
|
|
15615
|
+
badge: "WEB",
|
|
15616
|
+
live: true
|
|
15617
|
+
};
|
|
15618
|
+
}
|
|
15619
|
+
const suffix = suffixOf(data.file);
|
|
15620
|
+
const badge = data.html ? "HTML" : /^(?:pdf|svg|png|jpe?g|webp|gif|bmp|ico)$/u.test(suffix) ? suffix.toUpperCase() : "FIG";
|
|
15621
|
+
return {
|
|
15622
|
+
name: baseNameOf(data.file),
|
|
15623
|
+
badge,
|
|
15624
|
+
live: false
|
|
15625
|
+
};
|
|
15626
|
+
}
|
|
15627
|
+
//#endregion
|
|
15514
15628
|
//#region src/client/annotation-card.tsx
|
|
15515
15629
|
/**
|
|
15516
15630
|
* The annotation a reader sent, as a card inside their own bubble.
|
|
@@ -15518,8 +15632,8 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
15518
15632
|
* The message still says everything it said — the model reads the same text,
|
|
15519
15633
|
* copying it copies the same text — but in the transcript the same block is easier
|
|
15520
15634
|
* to scan as a small sheet: which figure, how large, how many marks, and what each
|
|
15521
|
-
* of them asked for. Clicking
|
|
15522
|
-
*
|
|
15635
|
+
* of them asked for. Clicking its header expands the delivered review; a separate action opens the
|
|
15636
|
+
* current source in the Sidebar, which may have changed since this message was sent.
|
|
15523
15637
|
*
|
|
15524
15638
|
* Two rules keep this a view and nothing more. It reads the block it was dispatched
|
|
15525
15639
|
* for and never edits it; and it draws from the parsed data alone, so a block whose
|
|
@@ -15645,9 +15759,10 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
15645
15759
|
* @returns the row.
|
|
15646
15760
|
*/
|
|
15647
15761
|
function CardRow(props) {
|
|
15648
|
-
const { mark, ordinal, page } = props;
|
|
15762
|
+
const { mark, ordinal, page, expanded } = props;
|
|
15649
15763
|
const said = mark.comment === "" ? void 0 : mark.comment;
|
|
15650
15764
|
const parts = readCardMarkLabel(mark.label);
|
|
15765
|
+
const number = /\d+$/u.exec(parts.name)?.[0] ?? String(ordinal);
|
|
15651
15766
|
const kind = parts.name.replace(/\s*\d+$/u, "");
|
|
15652
15767
|
const label = parts.element === "" ? kind : parts.element;
|
|
15653
15768
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -15660,7 +15775,7 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
15660
15775
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
15661
15776
|
className: "da-card-kind",
|
|
15662
15777
|
"aria-hidden": "true",
|
|
15663
|
-
children:
|
|
15778
|
+
children: number
|
|
15664
15779
|
}),
|
|
15665
15780
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
15666
15781
|
className: "da-card-mark",
|
|
@@ -15689,6 +15804,10 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
15689
15804
|
className: "da-card-said",
|
|
15690
15805
|
children: said
|
|
15691
15806
|
}),
|
|
15807
|
+
expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
15808
|
+
className: "da-card-locator",
|
|
15809
|
+
children: mark.label
|
|
15810
|
+
}) : null,
|
|
15692
15811
|
parts.aside === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
15693
15812
|
className: "da-card-warning",
|
|
15694
15813
|
children: parts.aside
|
|
@@ -15702,7 +15821,6 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
15702
15821
|
* @returns the card, or the block's own text when its data is not one it can draw.
|
|
15703
15822
|
*/
|
|
15704
15823
|
function AnnotationCard(props) {
|
|
15705
|
-
const [shot, setShot] = (0, react.useState)(true);
|
|
15706
15824
|
(0, react.useEffect)(() => {
|
|
15707
15825
|
ensureStyles(document);
|
|
15708
15826
|
}, []);
|
|
@@ -15712,111 +15830,156 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
15712
15830
|
className: "da-card-fallback",
|
|
15713
15831
|
children: raw
|
|
15714
15832
|
});
|
|
15833
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReviewCard, {
|
|
15834
|
+
...props,
|
|
15835
|
+
data,
|
|
15836
|
+
raw
|
|
15837
|
+
}, raw || JSON.stringify(data));
|
|
15838
|
+
}
|
|
15839
|
+
/** The review snapshot; navigation to the current source is a separate action. */
|
|
15840
|
+
function ReviewCard(props) {
|
|
15841
|
+
const { data, raw, embedded = false } = props;
|
|
15842
|
+
const [failedThumb, setFailedThumb] = (0, react.useState)();
|
|
15843
|
+
const [expanded, setExpanded] = (0, react.useState)(embedded);
|
|
15844
|
+
const [trouble, setTrouble] = (0, react.useState)(false);
|
|
15845
|
+
const bodyId = (0, react.useId)();
|
|
15715
15846
|
const interactive = props.interactive !== false;
|
|
15716
15847
|
const open = props.open;
|
|
15717
|
-
const
|
|
15718
|
-
const
|
|
15848
|
+
const candidate = thumbFor(raw);
|
|
15849
|
+
const thumb = candidate === failedThumb ? void 0 : candidate;
|
|
15850
|
+
const shown = embedded || expanded ? data.marks : data.marks.slice(0, MAX_ROWS);
|
|
15719
15851
|
const rest = data.marks.length - shown.length;
|
|
15720
|
-
const
|
|
15721
|
-
const
|
|
15852
|
+
const subject = annotationSubject(data);
|
|
15853
|
+
const liveFile = subject.live;
|
|
15722
15854
|
const captureNotice = liveFile && thumb === void 0 ? data.pageCapture === "omitted" ? resolveTranslate(props, "cardCaptureOmitted") : data.pageCapture === "failed" ? resolveTranslate(props, "cardCaptureFailed") : "" : "";
|
|
15723
|
-
const
|
|
15724
|
-
const cut = Math.max(displayFile.lastIndexOf("/"), displayFile.lastIndexOf("\\"));
|
|
15725
|
-
const name = displayFile.slice(cut + 1);
|
|
15855
|
+
const sourceOpener = interactive && (props.canOpen?.(data.file, data.html) ?? true) ? open : void 0;
|
|
15726
15856
|
const dimensions = /(\d+)\s*[×x]\s*(\d+)/u.exec(data.scope);
|
|
15727
15857
|
const size = dimensions === null ? "" : `${dimensions[1]}×${dimensions[2]}`;
|
|
15728
|
-
const
|
|
15729
|
-
|
|
15730
|
-
|
|
15731
|
-
|
|
15732
|
-
|
|
15733
|
-
|
|
15734
|
-
|
|
15735
|
-
|
|
15736
|
-
|
|
15737
|
-
|
|
15738
|
-
|
|
15858
|
+
const pages = /^(?:共 (\d+) 页|(\d+) pages)(?:,|,|$)/u.exec(data.scope);
|
|
15859
|
+
const scope = pages === null ? size : resolveTranslate(props, "cardPages", { n: Number(pages[1] ?? pages[2]) });
|
|
15860
|
+
const headContent = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
15861
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
15862
|
+
className: "da-card-shot",
|
|
15863
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SheetIcon, {}), thumb === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
15864
|
+
alt: "",
|
|
15865
|
+
src: thumb,
|
|
15866
|
+
onError: () => {
|
|
15867
|
+
setFailedThumb(thumb);
|
|
15868
|
+
}
|
|
15869
|
+
})]
|
|
15870
|
+
}),
|
|
15871
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
15872
|
+
className: "da-card-text",
|
|
15873
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
15874
|
+
className: "da-card-title-row",
|
|
15875
|
+
children: [
|
|
15876
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
15877
|
+
className: "da-card-title",
|
|
15878
|
+
title: data.file,
|
|
15879
|
+
children: subject.name
|
|
15880
|
+
}),
|
|
15881
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
15882
|
+
className: "da-card-type",
|
|
15883
|
+
children: subject.badge
|
|
15884
|
+
}),
|
|
15885
|
+
captureNotice === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
15886
|
+
className: "da-card-capture",
|
|
15887
|
+
children: captureNotice
|
|
15888
|
+
})
|
|
15889
|
+
]
|
|
15890
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
15891
|
+
className: "da-card-scope",
|
|
15892
|
+
children: [scope === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
15893
|
+
className: "da-card-meta",
|
|
15894
|
+
title: data.scope,
|
|
15895
|
+
children: scope
|
|
15896
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
15897
|
+
className: "da-card-tally",
|
|
15898
|
+
"aria-label": resolveTranslate(props, "cardCount", { n: data.marks.length }),
|
|
15899
|
+
title: data.scope,
|
|
15900
|
+
children: data.marks.length
|
|
15739
15901
|
})]
|
|
15902
|
+
})]
|
|
15903
|
+
}),
|
|
15904
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
15905
|
+
className: "da-card-go",
|
|
15906
|
+
"aria-hidden": "true",
|
|
15907
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
15908
|
+
viewBox: "0 0 16 16",
|
|
15909
|
+
focusable: "false",
|
|
15910
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m6 3.5 4.5 4.5L6 12.5" })
|
|
15911
|
+
})
|
|
15912
|
+
})
|
|
15913
|
+
] });
|
|
15914
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
15915
|
+
className: "da-card",
|
|
15916
|
+
"data-expanded": expanded,
|
|
15917
|
+
children: [
|
|
15918
|
+
embedded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
15919
|
+
className: "da-card-head da-card-head-static",
|
|
15920
|
+
children: headContent
|
|
15921
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
15922
|
+
type: "button",
|
|
15923
|
+
className: "da-card-head",
|
|
15924
|
+
"aria-expanded": expanded,
|
|
15925
|
+
"aria-controls": bodyId,
|
|
15926
|
+
title: resolveTranslate(props, expanded ? "cardCollapse" : "cardExpand"),
|
|
15927
|
+
onClick: () => {
|
|
15928
|
+
setExpanded((value) => !value);
|
|
15929
|
+
},
|
|
15930
|
+
children: headContent
|
|
15740
15931
|
}),
|
|
15741
15932
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
15742
|
-
className: "da-card-
|
|
15743
|
-
|
|
15744
|
-
|
|
15745
|
-
|
|
15746
|
-
|
|
15747
|
-
|
|
15748
|
-
|
|
15749
|
-
})
|
|
15750
|
-
|
|
15751
|
-
|
|
15752
|
-
|
|
15753
|
-
})
|
|
15754
|
-
|
|
15755
|
-
|
|
15756
|
-
|
|
15757
|
-
|
|
15758
|
-
|
|
15759
|
-
|
|
15760
|
-
|
|
15761
|
-
|
|
15762
|
-
|
|
15763
|
-
|
|
15764
|
-
|
|
15765
|
-
|
|
15766
|
-
|
|
15767
|
-
|
|
15768
|
-
|
|
15769
|
-
|
|
15770
|
-
|
|
15933
|
+
className: "da-card-body",
|
|
15934
|
+
id: bodyId,
|
|
15935
|
+
children: [
|
|
15936
|
+
shown.map((mark, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardRow, {
|
|
15937
|
+
mark,
|
|
15938
|
+
ordinal: index + 1,
|
|
15939
|
+
expanded,
|
|
15940
|
+
page: mark.page === void 0 ? void 0 : resolveTranslate(props, "cardPage", { n: mark.page })
|
|
15941
|
+
}, `${String(mark.page ?? "")}-${String(index)}`)),
|
|
15942
|
+
rest > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
15943
|
+
className: "da-card-rest",
|
|
15944
|
+
children: resolveTranslate(props, "cardMore", { n: rest })
|
|
15945
|
+
}) : null,
|
|
15946
|
+
data.target === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
15947
|
+
className: "da-card-row da-card-target",
|
|
15948
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
15949
|
+
className: "da-card-goal",
|
|
15950
|
+
children: resolveTranslate(props, "cardGoal")
|
|
15951
|
+
}), data.target]
|
|
15952
|
+
})
|
|
15953
|
+
]
|
|
15954
|
+
}),
|
|
15955
|
+
embedded ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
15956
|
+
className: "da-card-actions",
|
|
15957
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
15958
|
+
type: "button",
|
|
15959
|
+
className: "da-card-toggle",
|
|
15960
|
+
"aria-expanded": expanded,
|
|
15961
|
+
"aria-controls": bodyId,
|
|
15962
|
+
onClick: () => {
|
|
15963
|
+
setExpanded((value) => !value);
|
|
15964
|
+
},
|
|
15965
|
+
children: resolveTranslate(props, expanded ? "cardCollapse" : "cardExpand")
|
|
15966
|
+
}), sourceOpener === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
15967
|
+
type: "button",
|
|
15968
|
+
className: "da-card-source",
|
|
15969
|
+
title: data.file,
|
|
15970
|
+
onClick: () => {
|
|
15971
|
+
setTrouble(sourceOpener(data.file, data.html) === false);
|
|
15972
|
+
},
|
|
15973
|
+
children: resolveTranslate(props, liveFile ? "cardOpenLive" : "cardOpen")
|
|
15771
15974
|
})]
|
|
15772
15975
|
}),
|
|
15773
|
-
|
|
15774
|
-
className: "da-card-
|
|
15775
|
-
|
|
15776
|
-
children:
|
|
15777
|
-
viewBox: "0 0 16 16",
|
|
15778
|
-
focusable: "false",
|
|
15779
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m6 3.5 4.5 4.5L6 12.5" })
|
|
15780
|
-
})
|
|
15976
|
+
trouble ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
15977
|
+
className: "da-card-error",
|
|
15978
|
+
role: "status",
|
|
15979
|
+
children: resolveTranslate(props, "cardOpenFailed")
|
|
15781
15980
|
}) : null
|
|
15782
15981
|
]
|
|
15783
15982
|
});
|
|
15784
|
-
const body = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
15785
|
-
className: "da-card-body",
|
|
15786
|
-
children: [
|
|
15787
|
-
shown.map((mark, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardRow, {
|
|
15788
|
-
mark,
|
|
15789
|
-
ordinal: index + 1,
|
|
15790
|
-
page: mark.page === void 0 ? void 0 : resolveTranslate(props, "cardPage", { n: mark.page })
|
|
15791
|
-
}, `${String(mark.page ?? "")}-${String(index)}`)),
|
|
15792
|
-
rest > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
15793
|
-
className: "da-card-row da-card-rest",
|
|
15794
|
-
children: resolveTranslate(props, "cardMore", { n: rest })
|
|
15795
|
-
}) : null,
|
|
15796
|
-
data.target === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
15797
|
-
className: "da-card-row da-card-target",
|
|
15798
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
15799
|
-
className: "da-card-goal",
|
|
15800
|
-
children: resolveTranslate(props, "cardGoal")
|
|
15801
|
-
}), data.target]
|
|
15802
|
-
})
|
|
15803
|
-
]
|
|
15804
|
-
});
|
|
15805
|
-
const onClick = canOpen && open !== void 0 ? () => {
|
|
15806
|
-
open(data.file, data.html);
|
|
15807
|
-
} : void 0;
|
|
15808
|
-
if (onClick === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
15809
|
-
className: "da-card",
|
|
15810
|
-
"data-static": "",
|
|
15811
|
-
children: [head, body]
|
|
15812
|
-
});
|
|
15813
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
15814
|
-
type: "button",
|
|
15815
|
-
className: "da-card",
|
|
15816
|
-
title: `${resolveTranslate(props, liveFile ? "cardOpenLive" : "cardOpen")} — ${displayFile}`,
|
|
15817
|
-
onClick,
|
|
15818
|
-
children: [head, body]
|
|
15819
|
-
});
|
|
15820
15983
|
}
|
|
15821
15984
|
//#endregion
|
|
15822
15985
|
//#region src/client/document-seat.ts
|
|
@@ -15900,7 +16063,8 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
15900
16063
|
paths: saved,
|
|
15901
16064
|
thumb,
|
|
15902
16065
|
file: void 0,
|
|
15903
|
-
|
|
16066
|
+
files: images.slice(0, 8).map(({ page, image }) => new File([image], `${baseNameOf(document.figure.path)}${pageFileSuffix(page)}.annotated.png`, { type: "image/png" })),
|
|
16067
|
+
pageImages: Math.min(images.length, 8),
|
|
15904
16068
|
locale: options.locale,
|
|
15905
16069
|
send: async () => {
|
|
15906
16070
|
if (sessions === void 0) throw new Error("the sessions service is unavailable");
|
|
@@ -16119,31 +16283,9 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
16119
16283
|
//#endregion
|
|
16120
16284
|
//#region src/client/pending-card.tsx
|
|
16121
16285
|
/**
|
|
16122
|
-
*
|
|
16123
|
-
*
|
|
16124
|
-
*
|
|
16125
|
-
* written into it that stays a card, and the whole annotation block sitting in the
|
|
16126
|
-
* input box is exactly what a reader cannot scan. So the block is armed beside the box
|
|
16127
|
-
* — its message held, its picture uploaded — and the chip stands beside the reader's
|
|
16128
|
-
* own attachments as the face of what that send will carry: the figure's thumbnail, how
|
|
16129
|
-
* many marks it carries, the file's name, and the first mark's note.
|
|
16130
|
-
* It is portalled into the composer card's own flow (see `composer-seat`), so it
|
|
16131
|
-
* sits where an attached picture sits; the dock entry that owns it holds only the
|
|
16132
|
-
* anchor, and falls back to drawing in the dock itself when there is no card.
|
|
16133
|
-
*
|
|
16134
|
-
* What the card shows and what it hides is the point. A dimension or a pair of
|
|
16135
|
-
* coordinates is not something to read at a glance, so it stays in the block the model
|
|
16136
|
-
* reads and in the hover title, and never on the face: the face says the figure and the
|
|
16137
|
-
* count first, the note second, and truncates both rather than wrapping into a wall of
|
|
16138
|
-
* text.
|
|
16139
|
-
*
|
|
16140
|
-
* One press is the point, and the box keeps the reader's own words while it is not
|
|
16141
|
-
* pressed: nothing is written into the draft, so the sentence being typed stays the only
|
|
16142
|
-
* text in it. The box's own send carries the armed message — typing above the card and
|
|
16143
|
-
* pressing Send sends the sentence and the review as one message, with the annotation
|
|
16144
|
-
* behind the sentence — and the card's own action runs that same send for a reader who
|
|
16145
|
-
* wants to fire it from the card. The card goes when that send takes it, or when the
|
|
16146
|
-
* reader drops it, because that is when it stops standing for anything.
|
|
16286
|
+
* Reviews staged beside the composer. The chip previews its immutable payload;
|
|
16287
|
+
* the native composer Send carries armed reviews, with or without typed text.
|
|
16288
|
+
* Older hosts without draft admission retain a direct-delivery fallback.
|
|
16147
16289
|
* @module dsh-figmark/client/pending-card
|
|
16148
16290
|
*/
|
|
16149
16291
|
/**
|
|
@@ -16181,28 +16323,6 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
16181
16323
|
});
|
|
16182
16324
|
}
|
|
16183
16325
|
/**
|
|
16184
|
-
* The small outline that stands for a mark's shape on the card's one line.
|
|
16185
|
-
* @returns the glyph.
|
|
16186
|
-
*/
|
|
16187
|
-
function MarkIcon() {
|
|
16188
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
16189
|
-
className: "da-staged-mark-icon",
|
|
16190
|
-
viewBox: "0 0 12 12",
|
|
16191
|
-
"aria-hidden": "true",
|
|
16192
|
-
focusable: "false",
|
|
16193
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
16194
|
-
x: "1",
|
|
16195
|
-
y: "2",
|
|
16196
|
-
width: "10",
|
|
16197
|
-
height: "8",
|
|
16198
|
-
rx: "1.5",
|
|
16199
|
-
fill: "none",
|
|
16200
|
-
stroke: "currentColor",
|
|
16201
|
-
strokeWidth: "1.1"
|
|
16202
|
-
})
|
|
16203
|
-
});
|
|
16204
|
-
}
|
|
16205
|
-
/**
|
|
16206
16326
|
* A mark's name without its coordinates: the kind and its number, which is what a
|
|
16207
16327
|
* reader recognises, with a space between the two the way the product writes it.
|
|
16208
16328
|
* @param label - the mark's own name, coordinates and all.
|
|
@@ -16221,25 +16341,15 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
16221
16341
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
16222
16342
|
const [trouble, setTrouble] = (0, react.useState)("");
|
|
16223
16343
|
const [shot, setShot] = (0, react.useState)(true);
|
|
16344
|
+
const [expanded, setExpanded] = (0, react.useState)(false);
|
|
16345
|
+
const detailId = (0, react.useId)();
|
|
16224
16346
|
const sending = (0, react.useRef)(false);
|
|
16225
16347
|
const armed = entry.armed;
|
|
16226
16348
|
const data = entry.data;
|
|
16227
16349
|
const thumb = shot ? thumbFor(entry.raw) : void 0;
|
|
16228
16350
|
const send = async () => {
|
|
16229
16351
|
if (sending.current) return;
|
|
16230
|
-
|
|
16231
|
-
sending.current = true;
|
|
16232
|
-
setBusy(true);
|
|
16233
|
-
try {
|
|
16234
|
-
armed.shell.submit?.();
|
|
16235
|
-
} finally {
|
|
16236
|
-
queueMicrotask(() => {
|
|
16237
|
-
sending.current = false;
|
|
16238
|
-
setBusy(false);
|
|
16239
|
-
});
|
|
16240
|
-
}
|
|
16241
|
-
return;
|
|
16242
|
-
}
|
|
16352
|
+
setTrouble("");
|
|
16243
16353
|
sending.current = true;
|
|
16244
16354
|
setBusy(true);
|
|
16245
16355
|
try {
|
|
@@ -16263,88 +16373,127 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
16263
16373
|
entry.id,
|
|
16264
16374
|
sessionId
|
|
16265
16375
|
]);
|
|
16266
|
-
const
|
|
16267
|
-
const
|
|
16268
|
-
const name = shownFile.slice(cut + 1);
|
|
16376
|
+
const subject = annotationSubject(data);
|
|
16377
|
+
const name = subject.name;
|
|
16269
16378
|
const first = data.marks[0];
|
|
16270
|
-
const captureNotice =
|
|
16379
|
+
const captureNotice = subject.live && thumb === void 0 ? data.pageCapture === "omitted" ? resolveTranslate(props, "cardCaptureOmitted") : data.pageCapture === "failed" ? resolveTranslate(props, "cardCaptureFailed") : "" : "";
|
|
16271
16380
|
const detail = first === void 0 ? name : first.comment === "" ? first.label : `${first.label} · ${first.comment}`;
|
|
16272
16381
|
const hover = data.scope === "" ? data.file : `${data.file} · ${data.scope}`;
|
|
16273
16382
|
const removeLabel = resolveTranslate(props, "pendingRemove");
|
|
16383
|
+
const hint = resolveTranslate(props, "pendingSendNow");
|
|
16274
16384
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
16275
16385
|
className: "da-staged",
|
|
16276
|
-
children: [
|
|
16277
|
-
|
|
16278
|
-
|
|
16279
|
-
|
|
16280
|
-
|
|
16281
|
-
|
|
16282
|
-
|
|
16283
|
-
|
|
16284
|
-
|
|
16285
|
-
|
|
16286
|
-
|
|
16287
|
-
|
|
16288
|
-
|
|
16289
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FigureIcon, {}), thumb === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
16290
|
-
alt: "",
|
|
16291
|
-
src: thumb,
|
|
16292
|
-
onError: () => {
|
|
16293
|
-
setShot(false);
|
|
16294
|
-
}
|
|
16295
|
-
})]
|
|
16296
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
16297
|
-
className: "da-staged-text",
|
|
16386
|
+
children: [
|
|
16387
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
16388
|
+
className: "da-staged-card",
|
|
16389
|
+
title: hover,
|
|
16390
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
16391
|
+
type: "button",
|
|
16392
|
+
className: "da-staged-open",
|
|
16393
|
+
"aria-expanded": expanded,
|
|
16394
|
+
"aria-controls": detailId,
|
|
16395
|
+
title: resolveTranslate(props, expanded ? "cardCollapse" : "cardExpand"),
|
|
16396
|
+
onClick: () => {
|
|
16397
|
+
setExpanded((value) => !value);
|
|
16398
|
+
},
|
|
16298
16399
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
16299
|
-
className: "da-staged-
|
|
16400
|
+
className: "da-staged-shot",
|
|
16401
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FigureIcon, {}), thumb === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
16402
|
+
alt: "",
|
|
16403
|
+
src: thumb,
|
|
16404
|
+
onError: () => {
|
|
16405
|
+
setShot(false);
|
|
16406
|
+
}
|
|
16407
|
+
})]
|
|
16408
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
16409
|
+
className: "da-staged-text",
|
|
16300
16410
|
children: [
|
|
16301
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
16302
|
-
className: "da-staged-
|
|
16303
|
-
|
|
16304
|
-
|
|
16305
|
-
|
|
16306
|
-
|
|
16307
|
-
|
|
16308
|
-
|
|
16411
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
16412
|
+
className: "da-staged-head",
|
|
16413
|
+
children: [
|
|
16414
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
16415
|
+
className: "da-staged-title",
|
|
16416
|
+
title: name,
|
|
16417
|
+
children: name
|
|
16418
|
+
}),
|
|
16419
|
+
captureNotice === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
16420
|
+
className: "da-staged-capture",
|
|
16421
|
+
children: captureNotice
|
|
16422
|
+
}),
|
|
16423
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
16424
|
+
className: "da-staged-count",
|
|
16425
|
+
"aria-label": resolveTranslate(props, "cardCount", { n: data.marks.length }),
|
|
16426
|
+
children: resolveTranslate(props, "pendingCount", { n: data.marks.length })
|
|
16427
|
+
})
|
|
16428
|
+
]
|
|
16309
16429
|
}),
|
|
16310
16430
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
16311
|
-
className: "da-staged-
|
|
16312
|
-
|
|
16313
|
-
children: resolveTranslate(props, "pendingCount", { n: data.marks.length })
|
|
16314
|
-
})
|
|
16315
|
-
]
|
|
16316
|
-
}), first === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
16317
|
-
className: "da-staged-line",
|
|
16318
|
-
title: detail,
|
|
16319
|
-
children: [
|
|
16320
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarkIcon, {}),
|
|
16321
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
16322
|
-
className: "da-staged-shape",
|
|
16323
|
-
children: shortLabel(first.label)
|
|
16431
|
+
className: "da-staged-type",
|
|
16432
|
+
children: subject.badge
|
|
16324
16433
|
}),
|
|
16325
|
-
first
|
|
16326
|
-
className: "da-staged-
|
|
16327
|
-
|
|
16434
|
+
first === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
16435
|
+
className: "da-staged-line",
|
|
16436
|
+
title: detail,
|
|
16437
|
+
children: [
|
|
16438
|
+
first.page === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
16439
|
+
className: "da-card-page",
|
|
16440
|
+
children: resolveTranslate(props, "cardPage", { n: first.page })
|
|
16441
|
+
}),
|
|
16442
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
16443
|
+
className: "da-staged-shape",
|
|
16444
|
+
children: shortLabel(first.label)
|
|
16445
|
+
}),
|
|
16446
|
+
first.comment === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
16447
|
+
className: "da-staged-said",
|
|
16448
|
+
children: first.comment
|
|
16449
|
+
})
|
|
16450
|
+
]
|
|
16328
16451
|
})
|
|
16329
16452
|
]
|
|
16330
16453
|
})]
|
|
16454
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
16455
|
+
type: "button",
|
|
16456
|
+
className: "da-staged-x",
|
|
16457
|
+
"aria-label": removeLabel,
|
|
16458
|
+
title: removeLabel,
|
|
16459
|
+
disabled: busy,
|
|
16460
|
+
onClick: () => {
|
|
16461
|
+
if (sending.current) return;
|
|
16462
|
+
if (armed !== void 0) disarmReview(armed, entry.conversation);
|
|
16463
|
+
unparkAnnotation(sessionId, entry.id);
|
|
16464
|
+
},
|
|
16465
|
+
children: "×"
|
|
16331
16466
|
})]
|
|
16332
|
-
}),
|
|
16467
|
+
}),
|
|
16468
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
16469
|
+
id: detailId,
|
|
16470
|
+
className: "da-staged-details",
|
|
16471
|
+
hidden: !expanded,
|
|
16472
|
+
children: expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotationCard, {
|
|
16473
|
+
node: { data },
|
|
16474
|
+
raw: entry.raw,
|
|
16475
|
+
interactive: false,
|
|
16476
|
+
embedded: true,
|
|
16477
|
+
t: props.t,
|
|
16478
|
+
localeId: props.localeId
|
|
16479
|
+
}) : null
|
|
16480
|
+
}),
|
|
16481
|
+
armed === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
16333
16482
|
type: "button",
|
|
16334
|
-
className: "da-staged-
|
|
16335
|
-
"aria-label": removeLabel,
|
|
16336
|
-
title: removeLabel,
|
|
16483
|
+
className: "da-staged-send",
|
|
16337
16484
|
disabled: busy,
|
|
16485
|
+
title: hint,
|
|
16338
16486
|
onClick: () => {
|
|
16339
|
-
|
|
16340
|
-
unparkAnnotation(sessionId, entry.id);
|
|
16487
|
+
send();
|
|
16341
16488
|
},
|
|
16342
|
-
children: "
|
|
16343
|
-
})
|
|
16344
|
-
|
|
16345
|
-
|
|
16346
|
-
|
|
16347
|
-
|
|
16489
|
+
children: resolveTranslate(props, busy ? "pendingSending" : "pendingSendNow")
|
|
16490
|
+
}) : null,
|
|
16491
|
+
trouble === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
16492
|
+
className: "da-staged-trouble",
|
|
16493
|
+
role: "alert",
|
|
16494
|
+
children: trouble
|
|
16495
|
+
})
|
|
16496
|
+
]
|
|
16348
16497
|
});
|
|
16349
16498
|
}
|
|
16350
16499
|
/**
|
|
@@ -16379,6 +16528,11 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
16379
16528
|
return mountSeat(node, setSeat);
|
|
16380
16529
|
}, [sessionId, showing]);
|
|
16381
16530
|
if (!showing) return null;
|
|
16531
|
+
const owned = entries.flatMap((entry) => entry.armed?.previewUrls ?? []).map((url) => `img[src=${JSON.stringify(url)}]`).join(",");
|
|
16532
|
+
const thumbnailStyle = owned === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: `
|
|
16533
|
+
[data-composer-card] [role="group"] > div:has(${owned}){display:none}
|
|
16534
|
+
[data-composer-card] > div:has(> div > [role="group"]):not(:has([role="group"] > div:not(:has(${owned})))){display:none}
|
|
16535
|
+
` });
|
|
16382
16536
|
const cards = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: entries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ParkedChip, {
|
|
16383
16537
|
entry,
|
|
16384
16538
|
sessionId,
|
|
@@ -16405,11 +16559,15 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
16405
16559
|
children: "×"
|
|
16406
16560
|
})]
|
|
16407
16561
|
});
|
|
16408
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
16409
|
-
|
|
16410
|
-
|
|
16411
|
-
|
|
16412
|
-
|
|
16562
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
16563
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
16564
|
+
className: "da-staged-anchor",
|
|
16565
|
+
ref: anchor,
|
|
16566
|
+
hidden: true
|
|
16567
|
+
}),
|
|
16568
|
+
thumbnailStyle,
|
|
16569
|
+
seat === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [cards, report] }) : (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [cards, report] }), seat)
|
|
16570
|
+
] });
|
|
16413
16571
|
}
|
|
16414
16572
|
//#endregion
|
|
16415
16573
|
//#region src/client/live-drafts.ts
|
|
@@ -16993,13 +17151,13 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
16993
17151
|
return;
|
|
16994
17152
|
}
|
|
16995
17153
|
const needle = query.trim().toLowerCase();
|
|
16996
|
-
|
|
16997
|
-
|
|
16998
|
-
|
|
16999
|
-
|
|
17000
|
-
|
|
17001
|
-
|
|
17002
|
-
|
|
17154
|
+
if (needle === "") return;
|
|
17155
|
+
const titleMatch = visible.some((service) => service.title?.toLowerCase().includes(needle) === true);
|
|
17156
|
+
if (looksLikeAddress(query) && !titleMatch) openWorkspace(query);
|
|
17157
|
+
else if (visible.length === 1) {
|
|
17158
|
+
const only = visible[0];
|
|
17159
|
+
openWorkspace(only.url, only.title);
|
|
17160
|
+
} else setError(t(visible.length === 0 ? "serviceNoMatch" : "serviceChooseMatch"));
|
|
17003
17161
|
};
|
|
17004
17162
|
const visible = (0, react.useMemo)(() => {
|
|
17005
17163
|
const needle = query.trim().toLowerCase();
|
|
@@ -17098,14 +17256,7 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
17098
17256
|
children: [
|
|
17099
17257
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
17100
17258
|
className: "da-services-head",
|
|
17101
|
-
children: [
|
|
17102
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17103
|
-
className: "da-services-kicker",
|
|
17104
|
-
children: t("serviceKicker")
|
|
17105
|
-
}),
|
|
17106
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", { children: t("serviceTitle") }),
|
|
17107
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("serviceHint") })
|
|
17108
|
-
]
|
|
17259
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", { children: t("serviceTitle") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("serviceHint") })]
|
|
17109
17260
|
}),
|
|
17110
17261
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
17111
17262
|
className: "da-service-search",
|
|
@@ -17163,7 +17314,7 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
17163
17314
|
children: t("serviceScanning")
|
|
17164
17315
|
}) : visible.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17165
17316
|
className: "da-services-empty",
|
|
17166
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("serviceEmpty") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("serviceEmptyHint") })]
|
|
17317
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t(query.trim() === "" ? "serviceEmpty" : "serviceNoMatch") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(query.trim() === "" ? "serviceEmptyHint" : "serviceSearchHint") })]
|
|
17167
17318
|
}) : visible.map((service) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
17168
17319
|
type: "button",
|
|
17169
17320
|
className: "da-service-row",
|
|
@@ -17278,6 +17429,1057 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
17278
17429
|
});
|
|
17279
17430
|
}
|
|
17280
17431
|
//#endregion
|
|
17432
|
+
//#region src/client/attachment-annotator/model.ts
|
|
17433
|
+
/** One explicitly opened image at a time; retained across sidebar remounts. */
|
|
17434
|
+
function createAttachmentStore() {
|
|
17435
|
+
let job = null;
|
|
17436
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
17437
|
+
const publish = () => {
|
|
17438
|
+
for (const listener of listeners) listener();
|
|
17439
|
+
};
|
|
17440
|
+
return {
|
|
17441
|
+
getSnapshot: () => job,
|
|
17442
|
+
subscribe(listener) {
|
|
17443
|
+
listeners.add(listener);
|
|
17444
|
+
return () => {
|
|
17445
|
+
listeners.delete(listener);
|
|
17446
|
+
};
|
|
17447
|
+
},
|
|
17448
|
+
/** Refuse to overwrite an unfinished image, including one in another Session. */
|
|
17449
|
+
open(sessionId, source) {
|
|
17450
|
+
if (job !== null) return job.sessionId === sessionId && job.source.id === source.id;
|
|
17451
|
+
job = {
|
|
17452
|
+
sessionId,
|
|
17453
|
+
source,
|
|
17454
|
+
marks: [],
|
|
17455
|
+
added: null
|
|
17456
|
+
};
|
|
17457
|
+
publish();
|
|
17458
|
+
return true;
|
|
17459
|
+
},
|
|
17460
|
+
change(source, marks) {
|
|
17461
|
+
if (job === null || job.source !== source || job.marks === marks) return;
|
|
17462
|
+
job = {
|
|
17463
|
+
...job,
|
|
17464
|
+
marks
|
|
17465
|
+
};
|
|
17466
|
+
publish();
|
|
17467
|
+
},
|
|
17468
|
+
rememberCopy(source, marks, id) {
|
|
17469
|
+
if (job === null || job.source !== source) return;
|
|
17470
|
+
job = {
|
|
17471
|
+
...job,
|
|
17472
|
+
added: {
|
|
17473
|
+
marks,
|
|
17474
|
+
id
|
|
17475
|
+
}
|
|
17476
|
+
};
|
|
17477
|
+
publish();
|
|
17478
|
+
},
|
|
17479
|
+
clear() {
|
|
17480
|
+
job = null;
|
|
17481
|
+
publish();
|
|
17482
|
+
},
|
|
17483
|
+
dispose() {
|
|
17484
|
+
job = null;
|
|
17485
|
+
listeners.clear();
|
|
17486
|
+
}
|
|
17487
|
+
};
|
|
17488
|
+
}
|
|
17489
|
+
/** Submission phases never accept a result; an attachment sent or removed is never resurrected. */
|
|
17490
|
+
function canAttach(job, sessionId, input) {
|
|
17491
|
+
return sessionId === job.sessionId && input.attachmentIds.includes(job.source.id) && (input.phase === "plain" || input.phase === "claimed");
|
|
17492
|
+
}
|
|
17493
|
+
/**
|
|
17494
|
+
* The text that keeps each numbered comment searchable after the flattened image
|
|
17495
|
+
* is staged. The image and the list carry the same ordinals, so the model can
|
|
17496
|
+
* connect a sentence to the mark it describes without relying on OCR.
|
|
17497
|
+
*/
|
|
17498
|
+
function attachmentMessage(fileName, marks) {
|
|
17499
|
+
const counted = /* @__PURE__ */ new Map();
|
|
17500
|
+
const lines = marks.map((mark) => {
|
|
17501
|
+
const ordinal = (counted.get(mark.kind) ?? 0) + 1;
|
|
17502
|
+
counted.set(mark.kind, ordinal);
|
|
17503
|
+
return describeMark(mark, ordinal, "zh");
|
|
17504
|
+
});
|
|
17505
|
+
return [
|
|
17506
|
+
`【附图批注】已附上「${fileName.trim() || "图片"}」的标注副本,共 ${marks.length} 处。请按编号逐条处理:`,
|
|
17507
|
+
...lines,
|
|
17508
|
+
"",
|
|
17509
|
+
"逐条修改原图的生成源。"
|
|
17510
|
+
].join("\n");
|
|
17511
|
+
}
|
|
17512
|
+
/** Stage one copy and its numbered comments without replacing the original or sending. */
|
|
17513
|
+
function attachCopy(job, sessionId, input, conversation, actions, file) {
|
|
17514
|
+
if (!canAttach(job, sessionId, input)) throw new Error("原附件已移除、已发送,或输入框正在发送。请下载批注副本。");
|
|
17515
|
+
if (!conversation.resolveDraftAttachments([job.source.id]).some((draft) => draft.id === job.source.id && draft.kind === "image" && "file" in draft && draft.file === job.source.file)) throw new Error("原附件已失效,请下载批注副本。");
|
|
17516
|
+
const ids = conversation.createDrafts(sessionId, [file], false).map((draft) => draft.id);
|
|
17517
|
+
let attached = false;
|
|
17518
|
+
try {
|
|
17519
|
+
if (ids.length !== 1 || !actions.addAttachments(ids)) throw new Error("输入框未接受副本;原附件保持不变。");
|
|
17520
|
+
attached = true;
|
|
17521
|
+
const message = attachmentMessage(job.source.file.name, job.marks);
|
|
17522
|
+
actions.setDraft(input.draft.trim() === "" ? message : `${input.draft}\n\n${message}`);
|
|
17523
|
+
return ids[0];
|
|
17524
|
+
} catch (error) {
|
|
17525
|
+
if (attached) try {
|
|
17526
|
+
actions.setDraft(input.draft);
|
|
17527
|
+
} catch {}
|
|
17528
|
+
for (const id of ids) conversation.releaseDraftAttachment(id);
|
|
17529
|
+
throw error;
|
|
17530
|
+
}
|
|
17531
|
+
}
|
|
17532
|
+
/** Older hosts can keep all existing Figmark features without crashing this optional entry. */
|
|
17533
|
+
function attachmentConversation(value) {
|
|
17534
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
17535
|
+
const candidate = value;
|
|
17536
|
+
return typeof candidate.resolveDraftAttachments === "function" && typeof candidate.createDrafts === "function" && typeof candidate.releaseDraftAttachment === "function" ? candidate : void 0;
|
|
17537
|
+
}
|
|
17538
|
+
//#endregion
|
|
17539
|
+
//#region src/client/attachment-annotator/export.ts
|
|
17540
|
+
/** Plain XML text; comments must never become SVG markup. */
|
|
17541
|
+
function escapeText(value) {
|
|
17542
|
+
return value.replace(/&/gu, "&").replace(/</gu, "<").replace(/>/gu, ">");
|
|
17543
|
+
}
|
|
17544
|
+
/** Build one self-contained SVG with a readable legend below the original image. */
|
|
17545
|
+
function attachmentSvg(dataUrl, size, marks) {
|
|
17546
|
+
const layer = {
|
|
17547
|
+
kind: "raster",
|
|
17548
|
+
dataUrl,
|
|
17549
|
+
...size
|
|
17550
|
+
};
|
|
17551
|
+
const paper = reviewSize(layer, marks);
|
|
17552
|
+
const numbers = markOrdinals(marks);
|
|
17553
|
+
const font = Math.max(14, Math.min(32, paper.width / 45));
|
|
17554
|
+
const width = Math.max(paper.width, 320);
|
|
17555
|
+
const columns = Math.max(10, Math.floor((width - 32) / font));
|
|
17556
|
+
const lines = marks.flatMap((mark) => {
|
|
17557
|
+
if (mark.detail === void 0 || mark.detail.trim() === "") return [];
|
|
17558
|
+
return `${mark.kind === "point" ? "Pin" : mark.kind} ${numbers.get(mark.id)} · ${mark.detail}`.split(/\r?\n/u).flatMap((line) => {
|
|
17559
|
+
const chars = Array.from(line);
|
|
17560
|
+
const wrapped = [];
|
|
17561
|
+
for (let at = 0; at < chars.length; at += columns) wrapped.push(chars.slice(at, at + columns).join(""));
|
|
17562
|
+
return wrapped;
|
|
17563
|
+
});
|
|
17564
|
+
});
|
|
17565
|
+
const height = paper.height + (lines.length === 0 ? 0 : 32 + lines.length * font * 1.5);
|
|
17566
|
+
return {
|
|
17567
|
+
svg: `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><rect width="100%" height="100%" fill="white"/>${composeReviewSvg(layer, marks)}${lines.map((line, at) => `<text x="16" y="${paper.height + 24 + at * font * 1.5}" font-size="${font}" fill="#222" font-family="sans-serif">${escapeText(line)}</text>`).join("")}</svg>`,
|
|
17568
|
+
width,
|
|
17569
|
+
height,
|
|
17570
|
+
scale: Math.min(1, 8192 / width, 8192 / height, Math.sqrt(16e6 / (width * height)))
|
|
17571
|
+
};
|
|
17572
|
+
}
|
|
17573
|
+
/** Read only when exporting; the host's preview URL remains owned by the host. */
|
|
17574
|
+
async function exportAttachment(file, size, marks) {
|
|
17575
|
+
const image = attachmentSvg(await new Promise((resolve, reject) => {
|
|
17576
|
+
const reader = new FileReader();
|
|
17577
|
+
reader.onload = () => {
|
|
17578
|
+
resolve(reader.result);
|
|
17579
|
+
};
|
|
17580
|
+
reader.onerror = () => {
|
|
17581
|
+
reject(/* @__PURE__ */ new Error("无法读取原图,请重试。"));
|
|
17582
|
+
};
|
|
17583
|
+
reader.readAsDataURL(file);
|
|
17584
|
+
}), size, marks);
|
|
17585
|
+
const blob = await rasterizePng(image.svg, image.width, image.height, image.scale);
|
|
17586
|
+
return new File([blob], `${file.name.replace(/\.[^.]*$/u, "") || "image"}.annotated.png`, { type: "image/png" });
|
|
17587
|
+
}
|
|
17588
|
+
/** Download is explicit and does not involve any Session. */
|
|
17589
|
+
function downloadAttachment(file) {
|
|
17590
|
+
const url = URL.createObjectURL(file);
|
|
17591
|
+
const link = document.createElement("a");
|
|
17592
|
+
link.href = url;
|
|
17593
|
+
link.download = file.name;
|
|
17594
|
+
link.click();
|
|
17595
|
+
setTimeout(() => {
|
|
17596
|
+
URL.revokeObjectURL(url);
|
|
17597
|
+
}, 1e3);
|
|
17598
|
+
}
|
|
17599
|
+
//#endregion
|
|
17600
|
+
//#region src/client/attachment-annotator/styles.ts
|
|
17601
|
+
/** Styles for the composer-thumbnail annotation overlay. */
|
|
17602
|
+
const ATTACHMENT_CSS = `
|
|
17603
|
+
.da-attachment-anchor{display:block;width:0;height:0;overflow:hidden;pointer-events:none}
|
|
17604
|
+
.da-attachment{
|
|
17605
|
+
--da-accent:#e03131;
|
|
17606
|
+
--da-blue:#176df2;
|
|
17607
|
+
--da-ink:#242529;
|
|
17608
|
+
--da-muted:#74777f;
|
|
17609
|
+
--da-faint:#9699a1;
|
|
17610
|
+
--da-line:rgba(36,37,41,.11);
|
|
17611
|
+
--da-paper:#fff;
|
|
17612
|
+
--da-rail:#2b2c30;
|
|
17613
|
+
position:fixed;
|
|
17614
|
+
inset:0;
|
|
17615
|
+
z-index:2147483000;
|
|
17616
|
+
color:var(--da-ink);
|
|
17617
|
+
font-family:var(--dsw-font-family,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif);
|
|
17618
|
+
font-size:13px;
|
|
17619
|
+
outline:none;
|
|
17620
|
+
}
|
|
17621
|
+
.da-attachment,.da-attachment *{box-sizing:border-box}
|
|
17622
|
+
.da-attachment button,.da-attachment textarea,.da-attachment input{font:inherit}
|
|
17623
|
+
.da-attachment button{cursor:pointer}
|
|
17624
|
+
.da-attachment button:focus-visible,.da-attachment textarea:focus-visible,.da-attachment input:focus-visible{outline:2px solid #6ea2ff;outline-offset:2px}
|
|
17625
|
+
.da-attachment button[aria-disabled=true]{opacity:.38;cursor:default}
|
|
17626
|
+
.da-attachment-backdrop{
|
|
17627
|
+
position:absolute;
|
|
17628
|
+
inset:0;
|
|
17629
|
+
display:grid;
|
|
17630
|
+
place-items:center;
|
|
17631
|
+
padding:24px;
|
|
17632
|
+
background:rgba(18,19,22,.58);
|
|
17633
|
+
backdrop-filter:blur(7px);
|
|
17634
|
+
animation:da-attachment-fade .18s ease-out both;
|
|
17635
|
+
}
|
|
17636
|
+
.da-attachment-dialog{
|
|
17637
|
+
position:relative;
|
|
17638
|
+
isolation:isolate;
|
|
17639
|
+
display:grid;
|
|
17640
|
+
grid-template-rows:minmax(0,1fr) auto;
|
|
17641
|
+
width:min(1240px,calc(100vw - 32px));
|
|
17642
|
+
height:min(860px,calc(100vh - 32px));
|
|
17643
|
+
min-width:520px;
|
|
17644
|
+
min-height:420px;
|
|
17645
|
+
overflow:visible;
|
|
17646
|
+
background:transparent;
|
|
17647
|
+
animation:da-attachment-rise .22s cubic-bezier(.22,1,.36,1) both;
|
|
17648
|
+
}
|
|
17649
|
+
.da-attachment svg{fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
|
|
17650
|
+
.da-attachment-stage{
|
|
17651
|
+
position:relative;
|
|
17652
|
+
display:flex;
|
|
17653
|
+
min-width:0;
|
|
17654
|
+
min-height:0;
|
|
17655
|
+
overflow:auto;
|
|
17656
|
+
padding:34px 100px 30px 34px;
|
|
17657
|
+
background:transparent;
|
|
17658
|
+
scrollbar-width:thin;
|
|
17659
|
+
overscroll-behavior:contain;
|
|
17660
|
+
}
|
|
17661
|
+
.da-attachment-canvas{
|
|
17662
|
+
position:relative;
|
|
17663
|
+
flex:0 0 auto;
|
|
17664
|
+
margin:auto;
|
|
17665
|
+
}
|
|
17666
|
+
.da-attachment-paper{
|
|
17667
|
+
position:relative;
|
|
17668
|
+
transform-origin:top left;
|
|
17669
|
+
overflow:visible;
|
|
17670
|
+
background:transparent;
|
|
17671
|
+
}
|
|
17672
|
+
.da-attachment-paper[data-busy=true]{cursor:progress}
|
|
17673
|
+
.da-attachment-paper>img{position:absolute;inset:0;display:block;width:100%;height:100%;border-radius:inherit;pointer-events:none;user-select:none}
|
|
17674
|
+
.da-attachment-paper>.da-overlay{position:absolute;inset:0;display:block;width:100%;height:100%;overflow:visible;touch-action:none}
|
|
17675
|
+
.da-attachment-paper>.da-overlay[data-tool=select] [data-da-mark]{cursor:move}
|
|
17676
|
+
.da-attachment-rail{
|
|
17677
|
+
position:absolute;
|
|
17678
|
+
z-index:20;
|
|
17679
|
+
top:50%;
|
|
17680
|
+
right:16px;
|
|
17681
|
+
display:flex;
|
|
17682
|
+
flex-direction:column;
|
|
17683
|
+
align-items:center;
|
|
17684
|
+
gap:3px;
|
|
17685
|
+
width:48px;
|
|
17686
|
+
max-height:calc(100% - 34px);
|
|
17687
|
+
overflow-y:auto;
|
|
17688
|
+
padding:7px 6px;
|
|
17689
|
+
transform:translateY(-50%);
|
|
17690
|
+
border:1px solid rgba(255,255,255,.09);
|
|
17691
|
+
border-radius:17px;
|
|
17692
|
+
background:var(--da-rail);
|
|
17693
|
+
color:#fff;
|
|
17694
|
+
box-shadow:0 12px 30px rgba(18,19,23,.3),inset 0 1px 0 rgba(255,255,255,.08);
|
|
17695
|
+
scrollbar-width:none;
|
|
17696
|
+
}
|
|
17697
|
+
.da-attachment-rail::-webkit-scrollbar{display:none}
|
|
17698
|
+
.da-attachment-close svg{width:16px;height:16px}
|
|
17699
|
+
.da-attachment-tool{
|
|
17700
|
+
display:flex;
|
|
17701
|
+
align-items:center;
|
|
17702
|
+
justify-content:center;
|
|
17703
|
+
width:35px;
|
|
17704
|
+
height:35px;
|
|
17705
|
+
flex:0 0 auto;
|
|
17706
|
+
padding:0;
|
|
17707
|
+
border:0;
|
|
17708
|
+
border-radius:10px;
|
|
17709
|
+
background:transparent;
|
|
17710
|
+
color:rgba(255,255,255,.72);
|
|
17711
|
+
font-size:18px;
|
|
17712
|
+
line-height:1;
|
|
17713
|
+
}
|
|
17714
|
+
.da-attachment-tool:hover{background:rgba(255,255,255,.1);color:#fff}
|
|
17715
|
+
.da-attachment-tool[aria-pressed=true]{background:rgba(255,255,255,.19);color:#fff}
|
|
17716
|
+
.da-attachment-tool[aria-disabled=true]:hover{background:transparent;color:rgba(255,255,255,.72)}
|
|
17717
|
+
.da-attachment-tool svg{width:17px;height:17px}
|
|
17718
|
+
.da-attachment-separator{width:22px;height:1px;flex:0 0 auto;margin:3px 0;background:rgba(255,255,255,.15)}
|
|
17719
|
+
.da-attachment-color{
|
|
17720
|
+
position:relative;
|
|
17721
|
+
display:flex;
|
|
17722
|
+
align-items:center;
|
|
17723
|
+
justify-content:center;
|
|
17724
|
+
width:35px;
|
|
17725
|
+
height:35px;
|
|
17726
|
+
flex:0 0 auto;
|
|
17727
|
+
border-radius:10px;
|
|
17728
|
+
cursor:pointer;
|
|
17729
|
+
}
|
|
17730
|
+
.da-attachment-color:hover{background:rgba(255,255,255,.1)}
|
|
17731
|
+
.da-attachment-color::after{content:"";position:absolute;inset:9px;border:2px solid #fff;border-radius:50%;background:var(--da-accent);box-shadow:0 0 0 1px rgba(0,0,0,.15)}
|
|
17732
|
+
.da-attachment-color input{position:absolute;inset:0;width:100%;height:100%;padding:0;border:0;opacity:0;cursor:pointer}
|
|
17733
|
+
.da-attachment-zoom{
|
|
17734
|
+
display:block;
|
|
17735
|
+
min-width:34px;
|
|
17736
|
+
padding:2px 0 3px;
|
|
17737
|
+
color:rgba(255,255,255,.55);
|
|
17738
|
+
font-size:8.5px;
|
|
17739
|
+
font-variant-numeric:tabular-nums;
|
|
17740
|
+
line-height:1;
|
|
17741
|
+
text-align:center;
|
|
17742
|
+
}
|
|
17743
|
+
.da-attachment-footer{
|
|
17744
|
+
display:flex;
|
|
17745
|
+
flex-direction:column;
|
|
17746
|
+
align-items:center;
|
|
17747
|
+
gap:6px;
|
|
17748
|
+
align-self:center;
|
|
17749
|
+
width:auto;
|
|
17750
|
+
margin:0 auto 14px;
|
|
17751
|
+
padding:0;
|
|
17752
|
+
}
|
|
17753
|
+
.da-attachment-status{
|
|
17754
|
+
display:flex;
|
|
17755
|
+
flex-direction:column;
|
|
17756
|
+
align-items:center;
|
|
17757
|
+
gap:3px;
|
|
17758
|
+
max-width:min(560px,calc(100vw - 56px));
|
|
17759
|
+
padding:6px 10px;
|
|
17760
|
+
border-radius:9px;
|
|
17761
|
+
background:rgba(23,24,27,.82);
|
|
17762
|
+
color:rgba(255,255,255,.72);
|
|
17763
|
+
font-size:10px;
|
|
17764
|
+
line-height:1.4;
|
|
17765
|
+
text-align:center;
|
|
17766
|
+
backdrop-filter:blur(12px);
|
|
17767
|
+
}
|
|
17768
|
+
.da-attachment-warning{color:#9a6418}
|
|
17769
|
+
.da-attachment-actions{display:flex;align-items:center;gap:7px;flex:0 0 auto}
|
|
17770
|
+
.da-attachment-action{
|
|
17771
|
+
display:inline-flex;
|
|
17772
|
+
align-items:center;
|
|
17773
|
+
justify-content:center;
|
|
17774
|
+
gap:7px;
|
|
17775
|
+
height:31px;
|
|
17776
|
+
padding:0 11px;
|
|
17777
|
+
border:.5px solid rgba(255,255,255,.28);
|
|
17778
|
+
border-radius:999px;
|
|
17779
|
+
background:rgba(251,251,252,.92);
|
|
17780
|
+
color:#4f5157;
|
|
17781
|
+
font-size:10.5px;
|
|
17782
|
+
font-weight:580;
|
|
17783
|
+
line-height:1;
|
|
17784
|
+
white-space:nowrap;
|
|
17785
|
+
box-shadow:0 5px 18px rgba(5,6,9,.18);
|
|
17786
|
+
}
|
|
17787
|
+
.da-attachment-action:hover{background:#eeeef0;color:var(--da-ink)}
|
|
17788
|
+
.da-attachment-action svg{width:14px;height:14px}
|
|
17789
|
+
.da-attachment-action-primary{border-color:#26272b;background:#26272b;color:#fff;box-shadow:0 1px 2px rgba(0,0,0,.14)}
|
|
17790
|
+
.da-attachment-action-primary:hover{background:#3a3b40;color:#fff}
|
|
17791
|
+
.da-attachment-comment-pill{
|
|
17792
|
+
position:absolute;
|
|
17793
|
+
z-index:15;
|
|
17794
|
+
width:286px;
|
|
17795
|
+
max-width:calc(100% - 16px);
|
|
17796
|
+
padding:3px;
|
|
17797
|
+
border:.5px solid var(--da-line);
|
|
17798
|
+
border-radius:20px;
|
|
17799
|
+
background:color-mix(in srgb,var(--da-paper) 94%,transparent);
|
|
17800
|
+
color:var(--da-ink);
|
|
17801
|
+
box-shadow:0 5px 18px rgba(20,40,80,.14),0 0 0 .5px rgba(20,40,80,.06);
|
|
17802
|
+
backdrop-filter:blur(10px);
|
|
17803
|
+
}
|
|
17804
|
+
.da-attachment-comment-pill textarea{
|
|
17805
|
+
width:100%;
|
|
17806
|
+
min-height:0;
|
|
17807
|
+
max-height:min(180px,42vh);
|
|
17808
|
+
resize:none;
|
|
17809
|
+
padding:7px 10px;
|
|
17810
|
+
border:0;
|
|
17811
|
+
outline:0;
|
|
17812
|
+
background:transparent;
|
|
17813
|
+
color:var(--da-ink);
|
|
17814
|
+
font:inherit;
|
|
17815
|
+
font-size:13px;
|
|
17816
|
+
line-height:1.55;
|
|
17817
|
+
field-sizing:content;
|
|
17818
|
+
overflow-y:auto;
|
|
17819
|
+
scrollbar-width:none;
|
|
17820
|
+
}
|
|
17821
|
+
.da-attachment-comment-pill textarea::-webkit-scrollbar{width:0;height:0}
|
|
17822
|
+
.da-attachment-comment-pill textarea::placeholder{color:#a1a4ab}
|
|
17823
|
+
.da-attachment .da-attachment-comment-pill textarea:focus-visible{outline:0}
|
|
17824
|
+
.da-attachment-confirm{
|
|
17825
|
+
position:absolute;
|
|
17826
|
+
z-index:40;
|
|
17827
|
+
left:50%;
|
|
17828
|
+
bottom:82px;
|
|
17829
|
+
display:flex;
|
|
17830
|
+
align-items:center;
|
|
17831
|
+
justify-content:space-between;
|
|
17832
|
+
gap:20px;
|
|
17833
|
+
width:min(500px,calc(100% - 40px));
|
|
17834
|
+
padding:12px 12px 12px 15px;
|
|
17835
|
+
transform:translateX(-50%);
|
|
17836
|
+
border:1px solid rgba(180,35,24,.2);
|
|
17837
|
+
border-radius:13px;
|
|
17838
|
+
background:rgba(255,252,251,.98);
|
|
17839
|
+
box-shadow:0 14px 38px rgba(50,25,20,.18);
|
|
17840
|
+
}
|
|
17841
|
+
.da-attachment-confirm>div:first-child{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
17842
|
+
.da-attachment-confirm strong{color:#352f2e;font-size:11.5px;font-weight:650}
|
|
17843
|
+
.da-attachment-confirm span{color:#827875;font-size:9.5px;line-height:1.35}
|
|
17844
|
+
.da-attachment-confirm-actions{display:flex;gap:6px;flex:0 0 auto}
|
|
17845
|
+
.da-attachment-confirm button{height:29px;padding:0 10px;border:.5px solid rgba(36,37,41,.13);border-radius:8px;background:#fff;color:#666970;font-size:10.5px}
|
|
17846
|
+
.da-attachment-confirm button:hover{background:#f1f1f2}
|
|
17847
|
+
.da-attachment-confirm .da-attachment-danger{border-color:rgba(180,35,24,.22);color:#a1281f}
|
|
17848
|
+
.da-attachment-confirm .da-attachment-danger:hover{background:#fae9e7}
|
|
17849
|
+
@keyframes da-attachment-fade{from{opacity:0}to{opacity:1}}
|
|
17850
|
+
@keyframes da-attachment-rise{from{opacity:0;transform:translateY(8px) scale(.992)}to{opacity:1;transform:none}}
|
|
17851
|
+
@media(max-width:760px){
|
|
17852
|
+
.da-attachment-backdrop{padding:0}
|
|
17853
|
+
.da-attachment-dialog{width:100vw;height:100vh;min-width:0;min-height:0}
|
|
17854
|
+
.da-attachment-stage{padding:16px 70px 18px 16px}
|
|
17855
|
+
.da-attachment-rail{right:9px;width:46px}
|
|
17856
|
+
.da-attachment-footer{gap:6px;margin-bottom:10px}
|
|
17857
|
+
.da-attachment-status{max-width:calc(100vw - 32px)}
|
|
17858
|
+
.da-attachment-actions{justify-content:flex-end}
|
|
17859
|
+
.da-attachment-action{flex:0 0 auto}
|
|
17860
|
+
.da-attachment-comment-pill{width:248px}
|
|
17861
|
+
}
|
|
17862
|
+
@media(pointer:coarse){
|
|
17863
|
+
.da-attachment-tool,.da-attachment-color{width:40px;height:40px}
|
|
17864
|
+
.da-attachment-rail{width:53px}
|
|
17865
|
+
}
|
|
17866
|
+
@media(prefers-reduced-motion:reduce){
|
|
17867
|
+
.da-attachment-backdrop,.da-attachment-dialog{animation:none}
|
|
17868
|
+
}
|
|
17869
|
+
`;
|
|
17870
|
+
//#endregion
|
|
17871
|
+
//#region src/client/attachment-annotator/editor.tsx
|
|
17872
|
+
/** A comp-access annotation overlay opened from a composer image thumbnail. */
|
|
17873
|
+
const TOOL_LABELS = {
|
|
17874
|
+
select: "选择",
|
|
17875
|
+
pen: "画笔",
|
|
17876
|
+
arrow: "箭头",
|
|
17877
|
+
rect: "矩形",
|
|
17878
|
+
ellipse: "椭圆",
|
|
17879
|
+
point: "Pin 评论"
|
|
17880
|
+
};
|
|
17881
|
+
const TOOLS = [
|
|
17882
|
+
"select",
|
|
17883
|
+
"pen",
|
|
17884
|
+
"arrow",
|
|
17885
|
+
"rect",
|
|
17886
|
+
"ellipse",
|
|
17887
|
+
"point"
|
|
17888
|
+
].map((tool) => [tool, TOOL_LABELS[tool]]);
|
|
17889
|
+
const ZOOMS = [
|
|
17890
|
+
1,
|
|
17891
|
+
1.5,
|
|
17892
|
+
2,
|
|
17893
|
+
4
|
|
17894
|
+
];
|
|
17895
|
+
const ZOOM_IN = {
|
|
17896
|
+
1: 1.5,
|
|
17897
|
+
1.5: 2,
|
|
17898
|
+
2: 4,
|
|
17899
|
+
4: 4
|
|
17900
|
+
};
|
|
17901
|
+
const ZOOM_OUT = {
|
|
17902
|
+
1: 1,
|
|
17903
|
+
1.5: 1,
|
|
17904
|
+
2: 1.5,
|
|
17905
|
+
4: 2
|
|
17906
|
+
};
|
|
17907
|
+
function PointerIcon() {
|
|
17908
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
17909
|
+
viewBox: "0 0 24 24",
|
|
17910
|
+
"aria-hidden": "true",
|
|
17911
|
+
focusable: "false",
|
|
17912
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: POINTER_PATH })
|
|
17913
|
+
});
|
|
17914
|
+
}
|
|
17915
|
+
function ToolGlyph({ tool }) {
|
|
17916
|
+
return tool === "select" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PointerIcon, {}) : ToolIcon(tool);
|
|
17917
|
+
}
|
|
17918
|
+
function imagesOf(conversation, attachmentIds) {
|
|
17919
|
+
if (conversation === void 0) return [];
|
|
17920
|
+
return conversation.resolveDraftAttachments(attachmentIds).filter((draft) => draft.kind === "image" && "file" in draft);
|
|
17921
|
+
}
|
|
17922
|
+
/** Where a mark's adjacent comment should be anchored, in figure pixels. */
|
|
17923
|
+
function attachmentFocusPoint(mark) {
|
|
17924
|
+
if (mark.kind === "arrow") return mark.points[mark.points.length - 1] ?? [0, 0];
|
|
17925
|
+
if (mark.kind === "rect" || mark.kind === "ellipse") {
|
|
17926
|
+
const first = mark.points[0] ?? [0, 0];
|
|
17927
|
+
const second = mark.points[1] ?? first;
|
|
17928
|
+
return [(first[0] + second[0]) / 2, (first[1] + second[1]) / 2];
|
|
17929
|
+
}
|
|
17930
|
+
if (mark.kind === "pen") {
|
|
17931
|
+
const points = mark.points.length === 0 ? [[0, 0]] : mark.points;
|
|
17932
|
+
const xs = points.map((point) => point[0]);
|
|
17933
|
+
const ys = points.map((point) => point[1]);
|
|
17934
|
+
return [(Math.min(...xs) + Math.max(...xs)) / 2, (Math.min(...ys) + Math.max(...ys)) / 2];
|
|
17935
|
+
}
|
|
17936
|
+
return mark.points[0] ?? [0, 0];
|
|
17937
|
+
}
|
|
17938
|
+
function canEditAttachment(job, sessionId, input) {
|
|
17939
|
+
return canAttach(job, sessionId, input);
|
|
17940
|
+
}
|
|
17941
|
+
/** Mounted once per Session. It has no visible row until a draft image is clicked. */
|
|
17942
|
+
function AttachmentOverlay(props) {
|
|
17943
|
+
const { store, sessionId, conversation, useInput } = props;
|
|
17944
|
+
const draft = useInput((input) => input.draft);
|
|
17945
|
+
const attachmentIds = useInput((input) => input.attachmentIds);
|
|
17946
|
+
const input = {
|
|
17947
|
+
draft,
|
|
17948
|
+
attachmentIds,
|
|
17949
|
+
phase: useInput((input) => input.phase)
|
|
17950
|
+
};
|
|
17951
|
+
const job = (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot);
|
|
17952
|
+
const images = imagesOf(conversation, attachmentIds);
|
|
17953
|
+
const imagesRef = (0, react.useRef)(images);
|
|
17954
|
+
const pendingThumbnail = (0, react.useRef)(null);
|
|
17955
|
+
const anchor = (0, react.useRef)(null);
|
|
17956
|
+
imagesRef.current = images;
|
|
17957
|
+
(0, react.useEffect)(() => {
|
|
17958
|
+
const card = anchor.current?.closest("[data-composer-card]");
|
|
17959
|
+
if (card === null || card === void 0) return;
|
|
17960
|
+
const openImage = (event) => {
|
|
17961
|
+
if (!(event instanceof MouseEvent)) return;
|
|
17962
|
+
if (event.button !== 0) return;
|
|
17963
|
+
const target = event.target;
|
|
17964
|
+
if (!(target instanceof Element)) return;
|
|
17965
|
+
const imageNode = target instanceof HTMLImageElement ? target : target.closest("button")?.querySelector("img") ?? null;
|
|
17966
|
+
if (imageNode === null) return;
|
|
17967
|
+
const previewUrl = imageNode.getAttribute("src");
|
|
17968
|
+
if (previewUrl === null) return;
|
|
17969
|
+
const image = imagesRef.current.find((draft) => draft.previewUrl === previewUrl);
|
|
17970
|
+
if (image === void 0 && imagesRef.current.length > 0) return;
|
|
17971
|
+
event.preventDefault();
|
|
17972
|
+
event.stopPropagation();
|
|
17973
|
+
if (image !== void 0) {
|
|
17974
|
+
store.open(sessionId, image);
|
|
17975
|
+
return;
|
|
17976
|
+
}
|
|
17977
|
+
pendingThumbnail.current = previewUrl;
|
|
17978
|
+
};
|
|
17979
|
+
card.addEventListener("click", openImage, true);
|
|
17980
|
+
return () => {
|
|
17981
|
+
card.removeEventListener("click", openImage, true);
|
|
17982
|
+
};
|
|
17983
|
+
}, [sessionId, store]);
|
|
17984
|
+
(0, react.useEffect)(() => {
|
|
17985
|
+
const previewUrl = pendingThumbnail.current;
|
|
17986
|
+
if (previewUrl === null) return;
|
|
17987
|
+
const image = images.find((draft) => draft.previewUrl === previewUrl);
|
|
17988
|
+
if (image === void 0) return;
|
|
17989
|
+
pendingThumbnail.current = null;
|
|
17990
|
+
store.open(sessionId, image);
|
|
17991
|
+
}, [
|
|
17992
|
+
images,
|
|
17993
|
+
sessionId,
|
|
17994
|
+
store
|
|
17995
|
+
]);
|
|
17996
|
+
(0, react.useEffect)(() => {
|
|
17997
|
+
if (job === null) return;
|
|
17998
|
+
if (job.sessionId !== sessionId || !canEditAttachment(job, sessionId, input)) store.clear();
|
|
17999
|
+
}, [
|
|
18000
|
+
input.attachmentIds,
|
|
18001
|
+
input.phase,
|
|
18002
|
+
job,
|
|
18003
|
+
sessionId,
|
|
18004
|
+
store
|
|
18005
|
+
]);
|
|
18006
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
18007
|
+
ref: anchor,
|
|
18008
|
+
className: "da-attachment-anchor",
|
|
18009
|
+
"aria-hidden": "true"
|
|
18010
|
+
}), job !== null && job.sessionId === sessionId ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AttachmentEditor, {
|
|
18011
|
+
...props,
|
|
18012
|
+
job
|
|
18013
|
+
}, `${job.sessionId}/${job.source.id}`), document.body) : null] });
|
|
18014
|
+
}
|
|
18015
|
+
/** The whole editor is one modal surface; the host's own composer remains intact. */
|
|
18016
|
+
function AttachmentEditor(props) {
|
|
18017
|
+
const { store, sessionId, conversation, useInput, inputActions, job } = props;
|
|
18018
|
+
const input = {
|
|
18019
|
+
draft: useInput((input) => input.draft),
|
|
18020
|
+
attachmentIds: useInput((input) => input.attachmentIds),
|
|
18021
|
+
phase: useInput((input) => input.phase)
|
|
18022
|
+
};
|
|
18023
|
+
const history = useAnnotationHistory(job.marks);
|
|
18024
|
+
const latest = (0, react.useRef)({
|
|
18025
|
+
input,
|
|
18026
|
+
sessionId
|
|
18027
|
+
});
|
|
18028
|
+
latest.current = {
|
|
18029
|
+
input,
|
|
18030
|
+
sessionId
|
|
18031
|
+
};
|
|
18032
|
+
const mounted = (0, react.useRef)(true);
|
|
18033
|
+
const root = (0, react.useRef)(null);
|
|
18034
|
+
const paper = (0, react.useRef)(null);
|
|
18035
|
+
const viewport = (0, react.useRef)(null);
|
|
18036
|
+
const canvasInput = (0, react.useRef)(null);
|
|
18037
|
+
const busyRef = (0, react.useRef)(false);
|
|
18038
|
+
const [previewUrl, setPreviewUrl] = (0, react.useState)();
|
|
18039
|
+
const [tool, setTool] = (0, react.useState)("select");
|
|
18040
|
+
const [color, setColor] = (0, react.useState)("#e03131");
|
|
18041
|
+
const [selectedId, setSelectedId] = (0, react.useState)(null);
|
|
18042
|
+
const [size, setSize] = (0, react.useState)(null);
|
|
18043
|
+
const [zoom, setZoom] = (0, react.useState)(1);
|
|
18044
|
+
const [viewportSize, setViewportSize] = (0, react.useState)({
|
|
18045
|
+
width: 760,
|
|
18046
|
+
height: 520
|
|
18047
|
+
});
|
|
18048
|
+
const [notice, setNotice] = (0, react.useState)("");
|
|
18049
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
18050
|
+
const [confirmClose, setConfirmClose] = (0, react.useState)(false);
|
|
18051
|
+
(0, react.useEffect)(() => {
|
|
18052
|
+
mounted.current = true;
|
|
18053
|
+
root.current?.focus();
|
|
18054
|
+
return () => {
|
|
18055
|
+
mounted.current = false;
|
|
18056
|
+
};
|
|
18057
|
+
}, []);
|
|
18058
|
+
(0, react.useEffect)(() => {
|
|
18059
|
+
const url = URL.createObjectURL(job.source.file);
|
|
18060
|
+
setPreviewUrl(url);
|
|
18061
|
+
return () => {
|
|
18062
|
+
URL.revokeObjectURL(url);
|
|
18063
|
+
};
|
|
18064
|
+
}, [job.source.file]);
|
|
18065
|
+
(0, react.useEffect)(() => {
|
|
18066
|
+
store.change(job.source, history.marks);
|
|
18067
|
+
}, [
|
|
18068
|
+
history.marks,
|
|
18069
|
+
job.source,
|
|
18070
|
+
store
|
|
18071
|
+
]);
|
|
18072
|
+
(0, react.useEffect)(() => {
|
|
18073
|
+
const node = viewport.current;
|
|
18074
|
+
/* v8 ignore next -- the effect runs after the stage ref has attached. */
|
|
18075
|
+
if (node === null) return;
|
|
18076
|
+
const measure = () => {
|
|
18077
|
+
setViewportSize({
|
|
18078
|
+
width: Math.max(280, node.clientWidth),
|
|
18079
|
+
height: Math.max(240, node.clientHeight)
|
|
18080
|
+
});
|
|
18081
|
+
};
|
|
18082
|
+
measure();
|
|
18083
|
+
const observer = new ResizeObserver(measure);
|
|
18084
|
+
observer.observe(node);
|
|
18085
|
+
return () => {
|
|
18086
|
+
observer.disconnect();
|
|
18087
|
+
};
|
|
18088
|
+
}, []);
|
|
18089
|
+
const scale = (size === null ? 1 : Math.min(1, Math.max(.1, (viewportSize.width - 112) / size.width), Math.max(.1, (viewportSize.height - 56) / size.height))) * zoom;
|
|
18090
|
+
const selected = history.marks.find((mark) => mark.id === selectedId) ?? null;
|
|
18091
|
+
const ready = canEditAttachment(job, sessionId, input) && conversation !== void 0;
|
|
18092
|
+
const percentage = Math.round(zoom * 100);
|
|
18093
|
+
const select = (id) => {
|
|
18094
|
+
setSelectedId(id);
|
|
18095
|
+
};
|
|
18096
|
+
const remove = (id) => {
|
|
18097
|
+
history.commit(removedMarks(history.marks, id));
|
|
18098
|
+
setSelectedId(null);
|
|
18099
|
+
};
|
|
18100
|
+
const requestClose = () => {
|
|
18101
|
+
canvasInput.current?.cancel();
|
|
18102
|
+
if (history.marks.length === 0) store.clear();
|
|
18103
|
+
else setConfirmClose(true);
|
|
18104
|
+
};
|
|
18105
|
+
const finish = async (download) => {
|
|
18106
|
+
const liveConversation = conversation;
|
|
18107
|
+
if (busyRef.current || size === null || history.marks.length === 0) return;
|
|
18108
|
+
if (!download && (!ready || liveConversation === void 0)) return;
|
|
18109
|
+
canvasInput.current?.cancel();
|
|
18110
|
+
busyRef.current = true;
|
|
18111
|
+
setBusy(true);
|
|
18112
|
+
setNotice(download ? "正在生成 PNG…" : "正在生成标注副本…");
|
|
18113
|
+
try {
|
|
18114
|
+
const file = await exportAttachment(job.source.file, size, history.marks);
|
|
18115
|
+
if (!mounted.current || store.getSnapshot()?.source !== job.source) return;
|
|
18116
|
+
const editor = root.current;
|
|
18117
|
+
if (editor !== null && typeof editor.checkVisibility === "function" && !editor.checkVisibility()) throw new Error("标注浮层已关闭,请重新打开后重试");
|
|
18118
|
+
if (download) {
|
|
18119
|
+
downloadAttachment(file);
|
|
18120
|
+
setNotice("已下载 PNG;原图和输入框未改变。");
|
|
18121
|
+
return;
|
|
18122
|
+
}
|
|
18123
|
+
if (store.getSnapshot()?.marks !== history.marks) throw new Error("生成过程中批注已更改,请重新生成");
|
|
18124
|
+
/* v8 ignore next -- the callable form above already refused an absent Host. */
|
|
18125
|
+
if (liveConversation === void 0) return;
|
|
18126
|
+
const live = latest.current;
|
|
18127
|
+
const id = attachCopy(job, live.sessionId, live.input, liveConversation, inputActions, file);
|
|
18128
|
+
store.rememberCopy(job.source, history.marks, id);
|
|
18129
|
+
store.clear();
|
|
18130
|
+
} catch (error) {
|
|
18131
|
+
if (mounted.current && store.getSnapshot() !== null) setNotice(`生成或加入副本失败:${String(error)}。批注已保留,可重试或下载。`);
|
|
18132
|
+
} finally {
|
|
18133
|
+
busyRef.current = false;
|
|
18134
|
+
if (mounted.current && store.getSnapshot() !== null) setBusy(false);
|
|
18135
|
+
}
|
|
18136
|
+
};
|
|
18137
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
18138
|
+
ref: root,
|
|
18139
|
+
className: "da-attachment",
|
|
18140
|
+
tabIndex: -1,
|
|
18141
|
+
role: "dialog",
|
|
18142
|
+
"aria-modal": "true",
|
|
18143
|
+
"aria-label": "附图标注编辑器",
|
|
18144
|
+
onKeyDown: (event) => {
|
|
18145
|
+
event.stopPropagation();
|
|
18146
|
+
if (event.target instanceof HTMLTextAreaElement || event.target instanceof HTMLInputElement) return;
|
|
18147
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "z") {
|
|
18148
|
+
event.preventDefault();
|
|
18149
|
+
if (event.shiftKey) history.redo();
|
|
18150
|
+
else history.undo();
|
|
18151
|
+
} else if (event.key === "Escape") {
|
|
18152
|
+
canvasInput.current?.cancel();
|
|
18153
|
+
setSelectedId(null);
|
|
18154
|
+
requestClose();
|
|
18155
|
+
} else if ((event.key === "Delete" || event.key === "Backspace") && selectedId !== null) {
|
|
18156
|
+
event.preventDefault();
|
|
18157
|
+
remove(selectedId);
|
|
18158
|
+
}
|
|
18159
|
+
},
|
|
18160
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: ATTACHMENT_CSS }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
18161
|
+
className: "da-attachment-backdrop",
|
|
18162
|
+
onPointerDown: (event) => {
|
|
18163
|
+
if (event.target === event.currentTarget) requestClose();
|
|
18164
|
+
},
|
|
18165
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
18166
|
+
className: "da-attachment-dialog",
|
|
18167
|
+
children: [
|
|
18168
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
18169
|
+
className: "da-attachment-stage",
|
|
18170
|
+
ref: viewport,
|
|
18171
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
18172
|
+
className: "da-attachment-canvas",
|
|
18173
|
+
style: size === null ? {} : {
|
|
18174
|
+
width: size.width * scale,
|
|
18175
|
+
height: size.height * scale
|
|
18176
|
+
},
|
|
18177
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
18178
|
+
ref: paper,
|
|
18179
|
+
className: "da-attachment-paper",
|
|
18180
|
+
"data-busy": busy || void 0,
|
|
18181
|
+
style: size === null ? {} : {
|
|
18182
|
+
width: size.width,
|
|
18183
|
+
height: size.height,
|
|
18184
|
+
transform: `scale(${scale})`
|
|
18185
|
+
},
|
|
18186
|
+
onPointerDownCapture: (event) => {
|
|
18187
|
+
if (!busyRef.current) return;
|
|
18188
|
+
event.preventDefault();
|
|
18189
|
+
event.stopPropagation();
|
|
18190
|
+
},
|
|
18191
|
+
children: [
|
|
18192
|
+
previewUrl !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
18193
|
+
src: previewUrl,
|
|
18194
|
+
alt: "待标注原图",
|
|
18195
|
+
draggable: false,
|
|
18196
|
+
onLoad: (event) => {
|
|
18197
|
+
const image = event.currentTarget;
|
|
18198
|
+
if (image.naturalWidth === 0 || image.naturalHeight === 0) {
|
|
18199
|
+
setSize(null);
|
|
18200
|
+
setNotice("图片尺寸无效。");
|
|
18201
|
+
return;
|
|
18202
|
+
}
|
|
18203
|
+
setSize({
|
|
18204
|
+
width: image.naturalWidth,
|
|
18205
|
+
height: image.naturalHeight
|
|
18206
|
+
});
|
|
18207
|
+
},
|
|
18208
|
+
onError: () => {
|
|
18209
|
+
setSize(null);
|
|
18210
|
+
setNotice("原图加载失败;请重新添加图片后重试。");
|
|
18211
|
+
}
|
|
18212
|
+
}),
|
|
18213
|
+
size !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Canvas, {
|
|
18214
|
+
width: size.width,
|
|
18215
|
+
height: size.height,
|
|
18216
|
+
marks: history.marks,
|
|
18217
|
+
tool,
|
|
18218
|
+
color,
|
|
18219
|
+
containerRef: paper,
|
|
18220
|
+
inputRef: canvasInput,
|
|
18221
|
+
anchoring: false,
|
|
18222
|
+
scale,
|
|
18223
|
+
selectedId,
|
|
18224
|
+
onSelect: select,
|
|
18225
|
+
onAdd: (mark) => {
|
|
18226
|
+
if (history.marks.length >= 100) {
|
|
18227
|
+
setNotice("单张图片最多 100 条批注。");
|
|
18228
|
+
return;
|
|
18229
|
+
}
|
|
18230
|
+
history.commit([...history.marks, mark]);
|
|
18231
|
+
},
|
|
18232
|
+
onMove: (id, points, anchor) => {
|
|
18233
|
+
const next = movedMarks(history.marks, id, points, anchor);
|
|
18234
|
+
if (next !== null) history.commit(next);
|
|
18235
|
+
},
|
|
18236
|
+
onUpdate: (id, patch) => {
|
|
18237
|
+
history.commit(patchedMarks(history.marks, id, patch));
|
|
18238
|
+
},
|
|
18239
|
+
onRemove: remove,
|
|
18240
|
+
onEdit: (id) => {
|
|
18241
|
+
setSelectedId(id);
|
|
18242
|
+
setTool("select");
|
|
18243
|
+
},
|
|
18244
|
+
onDrawn: (id) => {
|
|
18245
|
+
setSelectedId(id);
|
|
18246
|
+
if (tool !== "pen") setTool("select");
|
|
18247
|
+
}
|
|
18248
|
+
}),
|
|
18249
|
+
selected !== null && size !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommentPill, {
|
|
18250
|
+
mark: selected,
|
|
18251
|
+
size,
|
|
18252
|
+
scale,
|
|
18253
|
+
onChange: (value) => {
|
|
18254
|
+
history.amend((marks) => detailedMarks(marks, selected.id, value));
|
|
18255
|
+
},
|
|
18256
|
+
onClose: () => {
|
|
18257
|
+
setSelectedId(null);
|
|
18258
|
+
}
|
|
18259
|
+
})
|
|
18260
|
+
]
|
|
18261
|
+
})
|
|
18262
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
18263
|
+
className: "da-attachment-rail",
|
|
18264
|
+
role: "toolbar",
|
|
18265
|
+
"aria-label": "绘图工具",
|
|
18266
|
+
children: [
|
|
18267
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
18268
|
+
type: "button",
|
|
18269
|
+
className: "da-attachment-tool da-attachment-close",
|
|
18270
|
+
"aria-label": "关闭附图标注",
|
|
18271
|
+
title: "关闭",
|
|
18272
|
+
onClick: requestClose,
|
|
18273
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CloseIcon, {})
|
|
18274
|
+
}),
|
|
18275
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "da-attachment-separator" }),
|
|
18276
|
+
TOOLS.map(([value, label]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
18277
|
+
type: "button",
|
|
18278
|
+
className: "da-attachment-tool",
|
|
18279
|
+
"aria-label": label,
|
|
18280
|
+
title: label,
|
|
18281
|
+
"aria-pressed": tool === value,
|
|
18282
|
+
onClick: () => {
|
|
18283
|
+
canvasInput.current?.cancel();
|
|
18284
|
+
setTool(value);
|
|
18285
|
+
},
|
|
18286
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolGlyph, { tool: value })
|
|
18287
|
+
}, value)),
|
|
18288
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "da-attachment-separator" }),
|
|
18289
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
18290
|
+
className: "da-attachment-color",
|
|
18291
|
+
title: "批注颜色",
|
|
18292
|
+
style: { "--da-accent": color },
|
|
18293
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
18294
|
+
type: "color",
|
|
18295
|
+
"aria-label": "批注颜色",
|
|
18296
|
+
value: color,
|
|
18297
|
+
onChange: (event) => {
|
|
18298
|
+
setColor(event.target.value);
|
|
18299
|
+
if (selectedId !== null) history.commit(patchedMarks(history.marks, selectedId, { color: event.target.value }));
|
|
18300
|
+
}
|
|
18301
|
+
})
|
|
18302
|
+
}),
|
|
18303
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
18304
|
+
type: "button",
|
|
18305
|
+
className: "da-attachment-tool",
|
|
18306
|
+
"aria-label": "撤销",
|
|
18307
|
+
title: "撤销",
|
|
18308
|
+
"aria-disabled": history.depth.past === 0,
|
|
18309
|
+
onClick: history.undo,
|
|
18310
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UndoIcon, {})
|
|
18311
|
+
}),
|
|
18312
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
18313
|
+
type: "button",
|
|
18314
|
+
className: "da-attachment-tool",
|
|
18315
|
+
"aria-label": "重做",
|
|
18316
|
+
title: "重做",
|
|
18317
|
+
"aria-disabled": history.depth.future === 0,
|
|
18318
|
+
onClick: history.redo,
|
|
18319
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RedoIcon, {})
|
|
18320
|
+
}),
|
|
18321
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "da-attachment-separator" }),
|
|
18322
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
18323
|
+
type: "button",
|
|
18324
|
+
className: "da-attachment-tool",
|
|
18325
|
+
"aria-label": "缩小",
|
|
18326
|
+
title: "缩小",
|
|
18327
|
+
"aria-disabled": zoom === ZOOMS[0],
|
|
18328
|
+
onClick: () => {
|
|
18329
|
+
setZoom(ZOOM_OUT[zoom]);
|
|
18330
|
+
},
|
|
18331
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
18332
|
+
"aria-hidden": "true",
|
|
18333
|
+
children: "−"
|
|
18334
|
+
})
|
|
18335
|
+
}),
|
|
18336
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
18337
|
+
className: "da-attachment-zoom",
|
|
18338
|
+
"aria-label": "当前缩放",
|
|
18339
|
+
children: [percentage, "%"]
|
|
18340
|
+
}),
|
|
18341
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
18342
|
+
type: "button",
|
|
18343
|
+
className: "da-attachment-tool",
|
|
18344
|
+
"aria-label": "放大",
|
|
18345
|
+
title: "放大",
|
|
18346
|
+
"aria-disabled": zoom === ZOOMS[ZOOMS.length - 1],
|
|
18347
|
+
onClick: () => {
|
|
18348
|
+
setZoom(ZOOM_IN[zoom]);
|
|
18349
|
+
},
|
|
18350
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
18351
|
+
"aria-hidden": "true",
|
|
18352
|
+
children: "+"
|
|
18353
|
+
})
|
|
18354
|
+
})
|
|
18355
|
+
]
|
|
18356
|
+
})]
|
|
18357
|
+
}),
|
|
18358
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
|
|
18359
|
+
className: "da-attachment-footer",
|
|
18360
|
+
children: [(notice !== "" || !ready) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
18361
|
+
className: "da-attachment-status",
|
|
18362
|
+
children: [notice !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
18363
|
+
role: "status",
|
|
18364
|
+
"aria-live": "polite",
|
|
18365
|
+
children: notice
|
|
18366
|
+
}), !ready && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
18367
|
+
className: "da-attachment-warning",
|
|
18368
|
+
children: "原附件已移除、已发送,或输入框正在发送。仍可下载 PNG。"
|
|
18369
|
+
})]
|
|
18370
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
18371
|
+
className: "da-attachment-actions",
|
|
18372
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
18373
|
+
type: "button",
|
|
18374
|
+
className: "da-attachment-action",
|
|
18375
|
+
"aria-disabled": busy || size === null || history.marks.length === 0,
|
|
18376
|
+
onClick: () => {
|
|
18377
|
+
finish(true);
|
|
18378
|
+
},
|
|
18379
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadIcon, {}), "下载 PNG"]
|
|
18380
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
18381
|
+
type: "button",
|
|
18382
|
+
className: "da-attachment-action da-attachment-action-primary",
|
|
18383
|
+
"aria-disabled": busy || size === null || history.marks.length === 0 || !ready,
|
|
18384
|
+
title: "生成标注图片,并把逐条评论写入输入框;原图不变,也不会自动发送",
|
|
18385
|
+
onClick: () => {
|
|
18386
|
+
finish(false);
|
|
18387
|
+
},
|
|
18388
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyIcon, {}), "加入输入框"]
|
|
18389
|
+
})]
|
|
18390
|
+
})]
|
|
18391
|
+
}),
|
|
18392
|
+
confirmClose && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
18393
|
+
className: "da-attachment-confirm",
|
|
18394
|
+
role: "alert",
|
|
18395
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "放弃这些本地批注?" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "原图不会改变;未生成的批注会在关闭后清除。" })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
18396
|
+
className: "da-attachment-confirm-actions",
|
|
18397
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
18398
|
+
type: "button",
|
|
18399
|
+
onClick: () => {
|
|
18400
|
+
setConfirmClose(false);
|
|
18401
|
+
},
|
|
18402
|
+
children: "继续编辑"
|
|
18403
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
18404
|
+
type: "button",
|
|
18405
|
+
className: "da-attachment-danger",
|
|
18406
|
+
onClick: () => {
|
|
18407
|
+
store.clear();
|
|
18408
|
+
},
|
|
18409
|
+
children: "放弃批注"
|
|
18410
|
+
})]
|
|
18411
|
+
})]
|
|
18412
|
+
})
|
|
18413
|
+
]
|
|
18414
|
+
})
|
|
18415
|
+
})]
|
|
18416
|
+
});
|
|
18417
|
+
}
|
|
18418
|
+
function CommentPill(props) {
|
|
18419
|
+
const { mark, size, scale } = props;
|
|
18420
|
+
const input = (0, react.useRef)(null);
|
|
18421
|
+
(0, react.useEffect)(() => {
|
|
18422
|
+
input.current?.focus();
|
|
18423
|
+
}, [mark.id]);
|
|
18424
|
+
const [x, y] = attachmentFocusPoint(mark);
|
|
18425
|
+
const right = x > size.width * .62;
|
|
18426
|
+
const below = y < 92;
|
|
18427
|
+
const translateX = right ? "calc(-100% - 14px)" : "14px";
|
|
18428
|
+
const translateY = below ? "14px" : "calc(-100% - 14px)";
|
|
18429
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
18430
|
+
className: "da-attachment-comment-pill",
|
|
18431
|
+
role: "group",
|
|
18432
|
+
"aria-label": "批注评论",
|
|
18433
|
+
style: {
|
|
18434
|
+
left: `${x}px`,
|
|
18435
|
+
top: `${y}px`,
|
|
18436
|
+
transform: `scale(${1 / scale}) translate(${translateX}, ${translateY})`,
|
|
18437
|
+
transformOrigin: `${right ? "right" : "left"} ${below ? "top" : "bottom"}`
|
|
18438
|
+
},
|
|
18439
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
18440
|
+
ref: input,
|
|
18441
|
+
"data-mark": mark.id,
|
|
18442
|
+
"aria-label": `批注 ${mark.id} 评论`,
|
|
18443
|
+
placeholder: "说明这里要怎么改…",
|
|
18444
|
+
rows: 1,
|
|
18445
|
+
maxLength: MAX_DETAIL_CHARS,
|
|
18446
|
+
value: mark.detail ?? "",
|
|
18447
|
+
onChange: (event) => {
|
|
18448
|
+
props.onChange(event.target.value);
|
|
18449
|
+
},
|
|
18450
|
+
onKeyDown: (event) => {
|
|
18451
|
+
if (event.key === "Escape") {
|
|
18452
|
+
event.preventDefault();
|
|
18453
|
+
props.onClose();
|
|
18454
|
+
}
|
|
18455
|
+
}
|
|
18456
|
+
})
|
|
18457
|
+
});
|
|
18458
|
+
}
|
|
18459
|
+
//#endregion
|
|
18460
|
+
//#region src/client/attachment-annotator/register.ts
|
|
18461
|
+
const ATTACHMENT_BODY_ID = "dsh-figmark/attachment";
|
|
18462
|
+
/**
|
|
18463
|
+
* One invisible floating seat. It listens only to its Session's native draft
|
|
18464
|
+
* thumbnails, so the shell keeps ownership of uploads, removal and sending.
|
|
18465
|
+
*/
|
|
18466
|
+
function registerAttachmentAnnotator(ctx, slots) {
|
|
18467
|
+
const store = createAttachmentStore();
|
|
18468
|
+
ctx.effect(() => () => {
|
|
18469
|
+
store.dispose();
|
|
18470
|
+
}, "dsh-figmark: attachment draft lifecycle");
|
|
18471
|
+
const injected = (_sessionId) => ({
|
|
18472
|
+
store,
|
|
18473
|
+
conversation: attachmentConversation(ctx.get("conversation"))
|
|
18474
|
+
});
|
|
18475
|
+
ctx.effect(() => slots.inject("conversation.input.overlay", () => slots.register({
|
|
18476
|
+
name: "conversation.input.overlay",
|
|
18477
|
+
id: ATTACHMENT_BODY_ID,
|
|
18478
|
+
order: 80,
|
|
18479
|
+
inject: injected
|
|
18480
|
+
}, AttachmentOverlay)), "dsh-figmark: attachment overlay");
|
|
18481
|
+
}
|
|
18482
|
+
//#endregion
|
|
17281
18483
|
//#region src/client/index.tsx
|
|
17282
18484
|
/** Plugin name reported to the client module loader. */
|
|
17283
18485
|
const name = "dsh-figmark-client";
|
|
@@ -17346,6 +18548,7 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
17346
18548
|
*/
|
|
17347
18549
|
function canOpenFigure(ctx, sessionId, path, html) {
|
|
17348
18550
|
if (sessionId === "" || path === "") return false;
|
|
18551
|
+
if (/^[a-z][a-z\d+.-]*:\/\//iu.test(path) && !isLivePage(path, html)) return false;
|
|
17349
18552
|
const sidebar = ctx.get("sidebarRight");
|
|
17350
18553
|
if (sidebar === void 0) return false;
|
|
17351
18554
|
return !isLivePage(path, html) || sidebar.openTab !== void 0;
|
|
@@ -17422,6 +18625,7 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
17422
18625
|
ctx.effect(() => sidebarTabs.register({
|
|
17423
18626
|
id: SERVICE_BODY_ID,
|
|
17424
18627
|
kind: SERVICE_KIND,
|
|
18628
|
+
keepMounted: true,
|
|
17425
18629
|
priority: "extension",
|
|
17426
18630
|
title: () => locale.bind(NAMESPACE)("serviceTitle"),
|
|
17427
18631
|
guide: [{
|
|
@@ -17530,6 +18734,7 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
|
|
|
17530
18734
|
localeId: locale.getLocale().locale ?? "zh"
|
|
17531
18735
|
})
|
|
17532
18736
|
}, PendingCard)), "dsh-figmark: parked annotation");
|
|
18737
|
+
registerAttachmentAnnotator(ctx, slots);
|
|
17533
18738
|
}
|
|
17534
18739
|
//#endregion
|
|
17535
18740
|
exports.BODY_ID = BODY_ID;
|