dsh-figmark 1.2.3 → 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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.4 — 2026-11-06
4
+
5
+ 本版统一批注卡片的预览与发送方式,修复缩放网页导出错位,并改进本机网页的检索和状态保留。
6
+
7
+ ### Improved
8
+
9
+ - 待发送标注进入输入框原生附件区;输入框为空时也可直接点右下角发送,或补充文字后一起发送。
10
+ - 标注卡片的重复附件缩略图合并显示,其他手动附件继续保留;卡片本身只负责预览与移除。
11
+ - 「本机网页」切换侧栏后保留当前页面、代理连接和未发送标注,返回时无需重新检索。
12
+ - 本机网页检索合并首页与 `index.html` 的重复结果;多个结果匹配时要求明确选择。
13
+ - 精简本机网页列表的标题、说明、搜索框和结果布局。
14
+
15
+ ### Fixed
16
+
17
+ - 修复缩放网页导出时按可视尺寸重新排版,导致网页标题和截图内容错位的问题。
18
+ - 修复 HTML 标注卡片把任意 URL 当作普通文件打开的问题。
19
+ - 修复 PDF 多页标注附件在原生输入框中的识别和发送状态。
20
+
3
21
  ## 1.2.3 — 2026-11-02
4
22
 
5
23
  本版增加输入框附图标注,并修复导出 SVG 与画布 Pin 编号的显示问题。
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: "只标注已经在 http:// 上运行、可交互的页面。静态 HTML 只读预览,不进入标注。",
79
- serviceSearch: "输入 localhost:5173 或运行中的地址",
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: "还有 {n} 处",
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
- pendingSendNow: "发送全部标注",
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: "Annotate interactive pages already served over http://. Static HTML is preview-only.",
229
- serviceSearch: "localhost:5173 or another running address",
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 the full annotation",
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: "and {n} more",
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 every annotation in this composer",
333
- pendingSendNow: "Send every annotation",
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. */
@@ -4815,14 +4833,14 @@ window.__ModuleLoader__.load({
4815
4833
  * Upload one file as a draft attachment without putting it in the rail yet.
4816
4834
  * @param conversation - the conversation service.
4817
4835
  * @param sessionId - session the draft belongs to.
4818
- * @param file - the file to hold, when there is one.
4836
+ * @param files - the rendered review images to hold.
4819
4837
  * @returns the ids the send will add, empty when the file could not be made into one.
4820
4838
  */
4821
- function holdFile(conversation, sessionId, file) {
4839
+ function holdFiles(conversation, sessionId, files) {
4822
4840
  const create = conversation.createDrafts?.bind(conversation);
4823
- if (file === void 0 || create === void 0) return [];
4841
+ if (files.length === 0 || create === void 0) return [];
4824
4842
  try {
4825
- return create(sessionId, [file]).map((draft) => draft.id);
4843
+ return create(sessionId, files);
4826
4844
  } catch {
4827
4845
  return [];
4828
4846
  }
@@ -4833,9 +4851,10 @@ window.__ModuleLoader__.load({
4833
4851
  * @param sessionId - session the draft belongs to.
4834
4852
  * @param message - the message, verbatim.
4835
4853
  * @param file - the review picture, when the surface has one to attach.
4854
+ * @param additionalFiles - marked page images of a multi-page review.
4836
4855
  * @returns what was armed, or undefined when this session cannot be armed.
4837
4856
  */
4838
- function armReview(conversation, sessionId, message, file) {
4857
+ function armReview(conversation, sessionId, message, file, additionalFiles = []) {
4839
4858
  let shell;
4840
4859
  try {
4841
4860
  shell = conversation.input.shell(sessionId);
@@ -4843,10 +4862,12 @@ window.__ModuleLoader__.load({
4843
4862
  return;
4844
4863
  }
4845
4864
  if (typeof shell.submit !== "function") return void 0;
4865
+ const drafts = holdFiles(conversation, sessionId, file === void 0 ? additionalFiles : [file, ...additionalFiles]);
4846
4866
  return {
4847
4867
  shell,
4848
4868
  text: message,
4849
- attachmentIds: holdFile(conversation, sessionId, file)
4869
+ attachmentIds: drafts.map((draft) => draft.id),
4870
+ previewUrls: drafts.flatMap((draft) => draft.previewUrl === void 0 ? [] : [draft.previewUrl])
4850
4871
  };
4851
4872
  }
4852
4873
  /**
@@ -4909,12 +4930,13 @@ window.__ModuleLoader__.load({
4909
4930
  for (const claim of claims) {
4910
4931
  const { text, attachmentIds } = claim.armed;
4911
4932
  if (draft.includes(text)) continue;
4912
- if (attachmentIds.length > 0) {
4913
- if (!attach(shell, attachmentIds)) {
4933
+ const missing = attachmentIds.filter((id) => !live.attachmentIds?.includes(id));
4934
+ if (missing.length > 0) {
4935
+ if (!attach(shell, missing)) {
4914
4936
  unwind(shell, typed, added);
4915
4937
  return;
4916
4938
  }
4917
- added.push(...attachmentIds);
4939
+ added.push(...missing);
4918
4940
  }
4919
4941
  draft = draft.trim() === "" ? text : `${draft}\n\n${text}`;
4920
4942
  }
@@ -4974,7 +4996,7 @@ window.__ModuleLoader__.load({
4974
4996
  try {
4975
4997
  shell.setDraft(merge.typed);
4976
4998
  } catch {}
4977
- 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);
4978
5000
  }
4979
5001
  /**
4980
5002
  * Take one card off whatever patch its shell is currently carrying.
@@ -5047,9 +5069,9 @@ window.__ModuleLoader__.load({
5047
5069
  const send = () => {
5048
5070
  try {
5049
5071
  original.apply(shell, args);
5050
- settleClaims(shell, claims, merge);
5051
5072
  } finally {
5052
5073
  waiting = false;
5074
+ settleClaims(shell, claims, merge);
5053
5075
  }
5054
5076
  };
5055
5077
  if (wroteThrough(shell, merge)) {
@@ -5102,13 +5124,27 @@ window.__ModuleLoader__.load({
5102
5124
  */
5103
5125
  function parkInComposer(input) {
5104
5126
  const { document: document_, locale, paths, sessionId } = input;
5105
- const message = buildAnnotationMessage(document_, paths, locale, input.file !== void 0, input.pageImages ?? 0, input.pageCapture);
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
+ }
5106
5144
  rememberDelivered(message, input.thumb);
5107
- const block = matchAnnotation(message, 0);
5108
- const armed = input.conversation === void 0 ? void 0 : armReview(input.conversation, sessionId, message, input.file);
5109
5145
  parkAnnotation({
5110
5146
  sessionId,
5111
- data: block?.data ?? {
5147
+ data: matchAnnotation(message, 0)?.data ?? {
5112
5148
  file: document_.figure.path,
5113
5149
  scope: "",
5114
5150
  target: "",
@@ -5119,7 +5155,10 @@ window.__ModuleLoader__.load({
5119
5155
  },
5120
5156
  raw: message,
5121
5157
  conversation: input.conversation,
5122
- armed,
5158
+ armed: armed === void 0 ? void 0 : {
5159
+ ...armed,
5160
+ text: message
5161
+ },
5123
5162
  send: input.send
5124
5163
  });
5125
5164
  }
@@ -8792,19 +8831,20 @@ body[data-ds-dark-theme] .da-root{--da-quiet:#9aa0a8}
8792
8831
  border:0.5px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));border-radius:12px;
8793
8832
  background:var(--dsw-alias-bg-base,rgba(255,255,255,.72));
8794
8833
  box-shadow:0 1px 2px rgba(20,40,80,.04);white-space:normal}
8795
- button.da-card{appearance:none;font-family:inherit;cursor:pointer;transition:border-color .15s ease,box-shadow .15s ease,background .15s ease}
8796
- button.da-card:hover{border-color:var(--dsw-alias-border-l3,rgba(128,128,128,.46));background:var(--dsw-alias-bg-layer-2,rgba(128,128,128,.08));
8797
- box-shadow:0 3px 10px rgba(20,40,80,.07)}
8798
- button.da-card:focus-visible{outline:2px solid var(--da-accent,#176df2);outline-offset:1px}
8799
- .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;
8800
8839
  border-bottom:0.5px solid var(--dsw-alias-border-l2,rgba(128,128,128,.2));
8801
8840
  background:var(--dsw-alias-bg-layer-1,rgba(128,128,128,.025))}
8841
+ .da-card-head-static{cursor:default}
8802
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;
8803
8843
  border:0.5px solid var(--dsw-alias-border-l2,rgba(128,128,128,.18));
8804
8844
  background:var(--dsw-alias-bg-layer-3,rgba(128,128,128,.1));color:var(--da-accent,#176df2)}
8805
8845
  /* The picture covers the glyph; a picture that fails hides itself, and the glyph
8806
8846
  underneath it all along is what is left. A miss is never an empty box. */
8807
- .da-card-shot img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
8847
+ .da-card-shot img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
8808
8848
  .da-card-shot img[hidden]{display:none}
8809
8849
  .da-card-glyph{width:18px;height:18px}
8810
8850
  .da-card-text{display:flex;flex:1 1 auto;flex-direction:column;gap:2px;min-width:0}
@@ -8827,10 +8867,11 @@ button.da-card:focus-visible{outline:2px solid var(--da-accent,#176df2);outline-
8827
8867
  border-radius:50%;background:var(--dsw-alias-bg-layer-3,rgba(128,128,128,.1));
8828
8868
  color:var(--dsw-alias-label-tertiary,inherit)}
8829
8869
  .da-card-go svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
8830
- button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242,.14));color:var(--da-accent,#176df2)}
8870
+ .da-card-head:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242,.14));color:var(--da-accent,#176df2)}
8831
8871
  /* Each mark is a compact record, not a line of debug output. The row head answers
8832
8872
  "which mark and where"; the element name and selector follow only when the message
8833
8873
  carried them; the reader's sentence is the strongest text. */
8874
+ .da-card[data-expanded="true"] .da-card-go{transform:rotate(90deg)}
8834
8875
  .da-card-body{display:flex;flex-direction:column;min-width:0;padding:0 9px 2px}
8835
8876
  .da-card-row{display:flex;flex-direction:column;gap:2px;min-width:0;padding:6px 0}
8836
8877
  .da-card-row+.da-card-row{border-top:0.5px solid var(--dsw-alias-border-l2,rgba(128,128,128,.14))}
@@ -8932,21 +8973,23 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
8932
8973
  background:none;font:inherit;font-size:13px;line-height:1;cursor:pointer;opacity:.65}
8933
8974
  .da-staged-result-x:hover{opacity:1;background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.72))}
8934
8975
  .da-staged-result-x:focus-visible{outline:2px solid var(--da-accent,#176df2);outline-offset:1px}
8935
- /* One figure, one picture. The message that carries an annotation also attaches the
8936
- review picture, and the shell draws that attachment full width above the bubble —
8937
- so the same figure appeared twice, once huge and once as the card's 38px copy. The
8938
- card can open that figure itself, so the large copy is the one to drop: it is the
8939
- duplicate, and it was the reason the card sat under a picture the reader had
8940
- already seen. Selected from the attachment row forward rather than from the card
8941
- back, because CSS has no parent selector to reach; :has does have a following
8942
- sibling test, which is exactly this relationship.
8943
-
8944
- Two guards keep it narrow. :not([data-static]) means a card that cannot open
8945
- anything (the queue dock's, drawn before sending) hides nothing — that copy is the
8946
- only picture in the dock. And if a shell ever stops marking its attachment row
8947
- with data-message-attachments, this rule stops matching rather than breaking
8948
- anything: the large picture simply returns and the card keeps working. */
8949
- [data-message-attachments]:has(~ * .da-card:not([data-static])){display:none}
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}
8950
8993
 
8951
8994
  /* --- local service browser ------------------------------------------------ */
8952
8995
  .da-service-shell{display:flex;flex-direction:column;height:100%;min-height:0}
@@ -8998,7 +9041,7 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
8998
9041
  .da-services{--da-service-ink:var(--dsw-alias-label-primary,#1b1d20);
8999
9042
  --da-service-muted:var(--dsw-alias-label-tertiary,#787d85);
9000
9043
  --da-service-line:color-mix(in srgb,var(--dsw-alias-border-l2,#c9cbd0) 66%,transparent);
9001
- box-sizing:border-box;height:100%;overflow:auto;padding:clamp(26px,7vh,64px) 22px 40px;
9044
+ box-sizing:border-box;height:100%;overflow:auto;padding:24px 20px 32px;width:100%;
9002
9045
  color:var(--da-service-ink);
9003
9046
  background:
9004
9047
  radial-gradient(circle at 18% 0%,color-mix(in srgb,#4c8a63 8%,transparent),transparent 36%),
@@ -9006,10 +9049,7 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
9006
9049
  .da-services-head,.da-service-search,.da-services-toolbar,.da-service-list,.da-services-error{
9007
9050
  width:min(480px,100%);margin-left:auto;margin-right:auto}
9008
9051
  .da-services-head{padding:0 4px 18px}
9009
- .da-services-kicker{display:block;margin-bottom:8px;color:var(--da-service-muted);
9010
- font:9px/1.2 var(--dsw-font-family-mono,ui-monospace,SFMono-Regular,Menlo,monospace);
9011
- letter-spacing:.16em}
9012
- .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}
9013
9053
  .da-services-head p{max-width:410px;margin:9px 0 0;color:var(--da-service-muted);font-size:12px;line-height:1.65}
9014
9054
  .da-service-search{display:flex;align-items:center;gap:9px;box-sizing:border-box;height:48px;padding:0 7px 0 14px;
9015
9055
  border:1px solid var(--da-service-line);border-radius:15px;
@@ -15559,6 +15599,32 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
15559
15599
  });
15560
15600
  }
15561
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
15562
15628
  //#region src/client/annotation-card.tsx
15563
15629
  /**
15564
15630
  * The annotation a reader sent, as a card inside their own bubble.
@@ -15566,8 +15632,8 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
15566
15632
  * The message still says everything it said — the model reads the same text,
15567
15633
  * copying it copies the same text — but in the transcript the same block is easier
15568
15634
  * to scan as a small sheet: which figure, how large, how many marks, and what each
15569
- * of them asked for. Clicking it opens the figure in the Sidebar, where the full
15570
- * annotator (and every mark, at full size) already is.
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.
15571
15637
  *
15572
15638
  * Two rules keep this a view and nothing more. It reads the block it was dispatched
15573
15639
  * for and never edits it; and it draws from the parsed data alone, so a block whose
@@ -15693,9 +15759,10 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
15693
15759
  * @returns the row.
15694
15760
  */
15695
15761
  function CardRow(props) {
15696
- const { mark, ordinal, page } = props;
15762
+ const { mark, ordinal, page, expanded } = props;
15697
15763
  const said = mark.comment === "" ? void 0 : mark.comment;
15698
15764
  const parts = readCardMarkLabel(mark.label);
15765
+ const number = /\d+$/u.exec(parts.name)?.[0] ?? String(ordinal);
15699
15766
  const kind = parts.name.replace(/\s*\d+$/u, "");
15700
15767
  const label = parts.element === "" ? kind : parts.element;
15701
15768
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -15708,7 +15775,7 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
15708
15775
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
15709
15776
  className: "da-card-kind",
15710
15777
  "aria-hidden": "true",
15711
- children: ordinal
15778
+ children: number
15712
15779
  }),
15713
15780
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
15714
15781
  className: "da-card-mark",
@@ -15737,6 +15804,10 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
15737
15804
  className: "da-card-said",
15738
15805
  children: said
15739
15806
  }),
15807
+ expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
15808
+ className: "da-card-locator",
15809
+ children: mark.label
15810
+ }) : null,
15740
15811
  parts.aside === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
15741
15812
  className: "da-card-warning",
15742
15813
  children: parts.aside
@@ -15750,7 +15821,6 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
15750
15821
  * @returns the card, or the block's own text when its data is not one it can draw.
15751
15822
  */
15752
15823
  function AnnotationCard(props) {
15753
- const [shot, setShot] = (0, react.useState)(true);
15754
15824
  (0, react.useEffect)(() => {
15755
15825
  ensureStyles(document);
15756
15826
  }, []);
@@ -15760,111 +15830,156 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
15760
15830
  className: "da-card-fallback",
15761
15831
  children: raw
15762
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)();
15763
15846
  const interactive = props.interactive !== false;
15764
15847
  const open = props.open;
15765
- const thumb = shot ? thumbFor(raw) : void 0;
15766
- const shown = data.marks.slice(0, MAX_ROWS);
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);
15767
15851
  const rest = data.marks.length - shown.length;
15768
- const liveFile = data.html && loopbackAddressOf(data.file) !== void 0;
15769
- const displayFile = liveFile ? data.file.replace(/[?#].*$/u, "") : data.file;
15852
+ const subject = annotationSubject(data);
15853
+ const liveFile = subject.live;
15770
15854
  const captureNotice = liveFile && thumb === void 0 ? data.pageCapture === "omitted" ? resolveTranslate(props, "cardCaptureOmitted") : data.pageCapture === "failed" ? resolveTranslate(props, "cardCaptureFailed") : "" : "";
15771
- const canOpen = interactive && open !== void 0 && (props.canOpen?.(data.file, data.html) ?? true);
15772
- const cut = Math.max(displayFile.lastIndexOf("/"), displayFile.lastIndexOf("\\"));
15773
- const name = displayFile.slice(cut + 1);
15855
+ const sourceOpener = interactive && (props.canOpen?.(data.file, data.html) ?? true) ? open : void 0;
15774
15856
  const dimensions = /(\d+)\s*[×x]\s*(\d+)/u.exec(data.scope);
15775
15857
  const size = dimensions === null ? "" : `${dimensions[1]}×${dimensions[2]}`;
15776
- const head = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
15777
- className: "da-card-head",
15778
- children: [
15779
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
15780
- className: "da-card-shot",
15781
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SheetIcon, {}), thumb === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
15782
- alt: "",
15783
- src: thumb,
15784
- onError: () => {
15785
- setShot(false);
15786
- }
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
15787
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
15788
15931
  }),
15789
15932
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
15790
- className: "da-card-text",
15791
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
15792
- className: "da-card-title-row",
15793
- children: [
15794
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
15795
- className: "da-card-title",
15796
- children: name
15797
- }),
15798
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
15799
- className: "da-card-type",
15800
- children: data.html ? "HTML" : "FIG"
15801
- }),
15802
- captureNotice === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
15803
- className: "da-card-capture",
15804
- children: captureNotice
15805
- })
15806
- ]
15807
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
15808
- className: "da-card-scope",
15809
- children: [size === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
15810
- className: "da-card-meta",
15811
- title: data.scope,
15812
- children: size
15813
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
15814
- className: "da-card-tally",
15815
- "aria-label": resolveTranslate(props, "cardCount", { n: data.marks.length }),
15816
- title: data.scope,
15817
- children: data.marks.length
15818
- })]
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")
15819
15974
  })]
15820
15975
  }),
15821
- canOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
15822
- className: "da-card-go",
15823
- "aria-hidden": "true",
15824
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
15825
- viewBox: "0 0 16 16",
15826
- focusable: "false",
15827
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m6 3.5 4.5 4.5L6 12.5" })
15828
- })
15976
+ trouble ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
15977
+ className: "da-card-error",
15978
+ role: "status",
15979
+ children: resolveTranslate(props, "cardOpenFailed")
15829
15980
  }) : null
15830
15981
  ]
15831
15982
  });
15832
- const body = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
15833
- className: "da-card-body",
15834
- children: [
15835
- shown.map((mark, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardRow, {
15836
- mark,
15837
- ordinal: index + 1,
15838
- page: mark.page === void 0 ? void 0 : resolveTranslate(props, "cardPage", { n: mark.page })
15839
- }, `${String(mark.page ?? "")}-${String(index)}`)),
15840
- rest > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
15841
- className: "da-card-row da-card-rest",
15842
- children: resolveTranslate(props, "cardMore", { n: rest })
15843
- }) : null,
15844
- data.target === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
15845
- className: "da-card-row da-card-target",
15846
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
15847
- className: "da-card-goal",
15848
- children: resolveTranslate(props, "cardGoal")
15849
- }), data.target]
15850
- })
15851
- ]
15852
- });
15853
- const onClick = canOpen && open !== void 0 ? () => {
15854
- open(data.file, data.html);
15855
- } : void 0;
15856
- if (onClick === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
15857
- className: "da-card",
15858
- "data-static": "",
15859
- children: [head, body]
15860
- });
15861
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
15862
- type: "button",
15863
- className: "da-card",
15864
- title: `${resolveTranslate(props, liveFile ? "cardOpenLive" : "cardOpen")} — ${displayFile}`,
15865
- onClick,
15866
- children: [head, body]
15867
- });
15868
15983
  }
15869
15984
  //#endregion
15870
15985
  //#region src/client/document-seat.ts
@@ -15948,7 +16063,8 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
15948
16063
  paths: saved,
15949
16064
  thumb,
15950
16065
  file: void 0,
15951
- pageImages: images.length,
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),
15952
16068
  locale: options.locale,
15953
16069
  send: async () => {
15954
16070
  if (sessions === void 0) throw new Error("the sessions service is unavailable");
@@ -16167,31 +16283,9 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
16167
16283
  //#endregion
16168
16284
  //#region src/client/pending-card.tsx
16169
16285
  /**
16170
- * The parked annotation, drawn inside the composer as an attachment card.
16171
- *
16172
- * The composer is a lexical editor owned by the conversation package: nothing can be
16173
- * written into it that stays a card, and the whole annotation block sitting in the
16174
- * input box is exactly what a reader cannot scan. So the block is armed beside the box
16175
- * — its message held, its picture uploaded — and the chip stands beside the reader's
16176
- * own attachments as the face of what that send will carry: the figure's thumbnail, how
16177
- * many marks it carries, the file's name, and the first mark's note.
16178
- * It is portalled into the composer card's own flow (see `composer-seat`), so it
16179
- * sits where an attached picture sits; the dock entry that owns it holds only the
16180
- * anchor, and falls back to drawing in the dock itself when there is no card.
16181
- *
16182
- * What the card shows and what it hides is the point. A dimension or a pair of
16183
- * coordinates is not something to read at a glance, so it stays in the block the model
16184
- * reads and in the hover title, and never on the face: the face says the figure and the
16185
- * count first, the note second, and truncates both rather than wrapping into a wall of
16186
- * text.
16187
- *
16188
- * One press is the point, and the box keeps the reader's own words while it is not
16189
- * pressed: nothing is written into the draft, so the sentence being typed stays the only
16190
- * text in it. The box's own send carries the armed message — typing above the card and
16191
- * pressing Send sends the sentence and the review as one message, with the annotation
16192
- * behind the sentence — and the card's own action runs that same send for a reader who
16193
- * wants to fire it from the card. The card goes when that send takes it, or when the
16194
- * 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.
16195
16289
  * @module dsh-figmark/client/pending-card
16196
16290
  */
16197
16291
  /**
@@ -16229,28 +16323,6 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
16229
16323
  });
16230
16324
  }
16231
16325
  /**
16232
- * The small outline that stands for a mark's shape on the card's one line.
16233
- * @returns the glyph.
16234
- */
16235
- function MarkIcon() {
16236
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
16237
- className: "da-staged-mark-icon",
16238
- viewBox: "0 0 12 12",
16239
- "aria-hidden": "true",
16240
- focusable: "false",
16241
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
16242
- x: "1",
16243
- y: "2",
16244
- width: "10",
16245
- height: "8",
16246
- rx: "1.5",
16247
- fill: "none",
16248
- stroke: "currentColor",
16249
- strokeWidth: "1.1"
16250
- })
16251
- });
16252
- }
16253
- /**
16254
16326
  * A mark's name without its coordinates: the kind and its number, which is what a
16255
16327
  * reader recognises, with a space between the two the way the product writes it.
16256
16328
  * @param label - the mark's own name, coordinates and all.
@@ -16269,25 +16341,15 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
16269
16341
  const [busy, setBusy] = (0, react.useState)(false);
16270
16342
  const [trouble, setTrouble] = (0, react.useState)("");
16271
16343
  const [shot, setShot] = (0, react.useState)(true);
16344
+ const [expanded, setExpanded] = (0, react.useState)(false);
16345
+ const detailId = (0, react.useId)();
16272
16346
  const sending = (0, react.useRef)(false);
16273
16347
  const armed = entry.armed;
16274
16348
  const data = entry.data;
16275
16349
  const thumb = shot ? thumbFor(entry.raw) : void 0;
16276
16350
  const send = async () => {
16277
16351
  if (sending.current) return;
16278
- if (armed !== void 0) {
16279
- sending.current = true;
16280
- setBusy(true);
16281
- try {
16282
- armed.shell.submit?.();
16283
- } finally {
16284
- queueMicrotask(() => {
16285
- sending.current = false;
16286
- setBusy(false);
16287
- });
16288
- }
16289
- return;
16290
- }
16352
+ setTrouble("");
16291
16353
  sending.current = true;
16292
16354
  setBusy(true);
16293
16355
  try {
@@ -16311,88 +16373,127 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
16311
16373
  entry.id,
16312
16374
  sessionId
16313
16375
  ]);
16314
- const shownFile = data.file.replace(/[?#].*$/u, "");
16315
- const cut = Math.max(shownFile.lastIndexOf("/"), shownFile.lastIndexOf("\\"));
16316
- const name = shownFile.slice(cut + 1);
16376
+ const subject = annotationSubject(data);
16377
+ const name = subject.name;
16317
16378
  const first = data.marks[0];
16318
- const captureNotice = data.html && thumb === void 0 ? data.pageCapture === "omitted" ? resolveTranslate(props, "cardCaptureOmitted") : data.pageCapture === "failed" ? resolveTranslate(props, "cardCaptureFailed") : "" : "";
16379
+ const captureNotice = subject.live && thumb === void 0 ? data.pageCapture === "omitted" ? resolveTranslate(props, "cardCaptureOmitted") : data.pageCapture === "failed" ? resolveTranslate(props, "cardCaptureFailed") : "" : "";
16319
16380
  const detail = first === void 0 ? name : first.comment === "" ? first.label : `${first.label} · ${first.comment}`;
16320
16381
  const hover = data.scope === "" ? data.file : `${data.file} · ${data.scope}`;
16321
16382
  const removeLabel = resolveTranslate(props, "pendingRemove");
16383
+ const hint = resolveTranslate(props, "pendingSendNow");
16322
16384
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
16323
16385
  className: "da-staged",
16324
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
16325
- className: "da-staged-card",
16326
- title: hover,
16327
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
16328
- type: "button",
16329
- className: "da-staged-open",
16330
- disabled: busy,
16331
- title: resolveTranslate(props, armed === void 0 ? "pendingSendNow" : "pendingSendHint"),
16332
- onClick: () => {
16333
- send();
16334
- },
16335
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
16336
- className: "da-staged-shot",
16337
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FigureIcon, {}), thumb === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
16338
- alt: "",
16339
- src: thumb,
16340
- onError: () => {
16341
- setShot(false);
16342
- }
16343
- })]
16344
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
16345
- 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
+ },
16346
16399
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
16347
- className: "da-staged-head",
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",
16348
16410
  children: [
16349
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
16350
- className: "da-staged-title",
16351
- title: name,
16352
- children: name
16353
- }),
16354
- captureNotice === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
16355
- className: "da-staged-capture",
16356
- children: captureNotice
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
+ ]
16357
16429
  }),
16358
16430
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
16359
- className: "da-staged-count",
16360
- "aria-label": resolveTranslate(props, "cardCount", { n: data.marks.length }),
16361
- children: resolveTranslate(props, "pendingCount", { n: data.marks.length })
16362
- })
16363
- ]
16364
- }), first === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
16365
- className: "da-staged-line",
16366
- title: detail,
16367
- children: [
16368
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarkIcon, {}),
16369
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
16370
- className: "da-staged-shape",
16371
- children: shortLabel(first.label)
16431
+ className: "da-staged-type",
16432
+ children: subject.badge
16372
16433
  }),
16373
- first.comment === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
16374
- className: "da-staged-said",
16375
- children: first.comment
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
+ ]
16376
16451
  })
16377
16452
  ]
16378
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: "×"
16379
16466
  })]
16380
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
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", {
16381
16482
  type: "button",
16382
- className: "da-staged-x",
16383
- "aria-label": removeLabel,
16384
- title: removeLabel,
16483
+ className: "da-staged-send",
16385
16484
  disabled: busy,
16485
+ title: hint,
16386
16486
  onClick: () => {
16387
- if (armed !== void 0) disarmReview(armed, entry.conversation);
16388
- unparkAnnotation(sessionId, entry.id);
16487
+ send();
16389
16488
  },
16390
- children: "×"
16391
- })]
16392
- }), trouble === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
16393
- className: "da-staged-trouble",
16394
- children: trouble
16395
- })]
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
+ ]
16396
16497
  });
16397
16498
  }
16398
16499
  /**
@@ -16427,6 +16528,11 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
16427
16528
  return mountSeat(node, setSeat);
16428
16529
  }, [sessionId, showing]);
16429
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
+ ` });
16430
16536
  const cards = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: entries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ParkedChip, {
16431
16537
  entry,
16432
16538
  sessionId,
@@ -16453,11 +16559,15 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
16453
16559
  children: "×"
16454
16560
  })]
16455
16561
  });
16456
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
16457
- className: "da-staged-anchor",
16458
- ref: anchor,
16459
- hidden: true
16460
- }), 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)] });
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
+ ] });
16461
16571
  }
16462
16572
  //#endregion
16463
16573
  //#region src/client/live-drafts.ts
@@ -17041,13 +17151,13 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
17041
17151
  return;
17042
17152
  }
17043
17153
  const needle = query.trim().toLowerCase();
17044
- const titleMatch = visible.find((service) => service.title?.toLowerCase().includes(needle) === true);
17045
- if (titleMatch !== void 0) {
17046
- openWorkspace(titleMatch.url, titleMatch.title);
17047
- return;
17048
- }
17049
- const first = visible[0];
17050
- openWorkspace(looksLikeAddress(query) || first === void 0 ? query : first.url, looksLikeAddress(query) || first === void 0 ? void 0 : first.title);
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"));
17051
17161
  };
17052
17162
  const visible = (0, react.useMemo)(() => {
17053
17163
  const needle = query.trim().toLowerCase();
@@ -17146,14 +17256,7 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
17146
17256
  children: [
17147
17257
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
17148
17258
  className: "da-services-head",
17149
- children: [
17150
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
17151
- className: "da-services-kicker",
17152
- children: t("serviceKicker")
17153
- }),
17154
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", { children: t("serviceTitle") }),
17155
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("serviceHint") })
17156
- ]
17259
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", { children: t("serviceTitle") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("serviceHint") })]
17157
17260
  }),
17158
17261
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
17159
17262
  className: "da-service-search",
@@ -17211,7 +17314,7 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
17211
17314
  children: t("serviceScanning")
17212
17315
  }) : visible.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17213
17316
  className: "da-services-empty",
17214
- 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") })]
17215
17318
  }) : visible.map((service) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
17216
17319
  type: "button",
17217
17320
  className: "da-service-row",
@@ -18445,6 +18548,7 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
18445
18548
  */
18446
18549
  function canOpenFigure(ctx, sessionId, path, html) {
18447
18550
  if (sessionId === "" || path === "") return false;
18551
+ if (/^[a-z][a-z\d+.-]*:\/\//iu.test(path) && !isLivePage(path, html)) return false;
18448
18552
  const sidebar = ctx.get("sidebarRight");
18449
18553
  if (sidebar === void 0) return false;
18450
18554
  return !isLivePage(path, html) || sidebar.openTab !== void 0;
@@ -18521,6 +18625,7 @@ button.da-card:hover .da-card-go{background:var(--da-accent-soft,rgba(23,109,242
18521
18625
  ctx.effect(() => sidebarTabs.register({
18522
18626
  id: SERVICE_BODY_ID,
18523
18627
  kind: SERVICE_KIND,
18628
+ keepMounted: true,
18524
18629
  priority: "extension",
18525
18630
  title: () => locale.bind(NAMESPACE)("serviceTitle"),
18526
18631
  guide: [{
package/lib/index.js CHANGED
@@ -885,9 +885,14 @@ ${readFileSync(createRequire(import.meta.url).resolve("modern-screenshot/dist/in
885
885
  ? [rect.left + scroll[0], rect.top + scroll[1], width, height]
886
886
  : [0, 0, width, height]
887
887
  var target = surface || root
888
+ // The deck's ancestor may scale a 1920px slide into a 548px viewport. Layout
889
+ // at the original size, then map the bitmap into imageRect. Feeding rect.width
890
+ // back as CSS width reflows/clips the slide while its children stay full-size.
891
+ var layoutWidth = surface ? surface.offsetWidth || width : width
892
+ var layoutHeight = surface ? surface.offsetHeight || height : height
888
893
  // The parent composes at no more than 1.5x, so a larger source only costs a DOM
889
894
  // clone and PNG encode without adding pixels the delivered picture keeps.
890
- var scale = Math.min(1.5, 1800 / Math.max(width, height))
895
+ var scale = Math.min(1.5, 1800 / Math.max(layoutWidth, layoutHeight))
891
896
  var screenshot = window.modernScreenshot
892
897
  if (!screenshot || typeof screenshot.domToDataUrl !== 'function') {
893
898
  send({
@@ -900,8 +905,9 @@ ${readFileSync(createRequire(import.meta.url).resolve("modern-screenshot/dist/in
900
905
  return
901
906
  }
902
907
  screenshot.domToDataUrl(target, {
903
- width: width,
904
- height: height,
908
+ width: layoutWidth,
909
+ height: layoutHeight,
910
+ style: surface ? { transform: 'none', zoom: '1' } : null,
905
911
  scale: scale,
906
912
  backgroundColor: '#ffffff',
907
913
  maximumCanvasSize: 4096,
@@ -1555,7 +1561,7 @@ function probe(candidate) {
1555
1561
  const finishResponse = () => {
1556
1562
  /* v8 ignore next -- Node populates statusCode before emitting `response`. */
1557
1563
  const status = res.statusCode ?? 0;
1558
- if (status < 200 || status >= 500) {
1564
+ if (status < 200 || status >= 400) {
1559
1565
  finish(void 0);
1560
1566
  return;
1561
1567
  }
@@ -1630,7 +1636,16 @@ async function discoverServices(request, sessionCwd) {
1630
1636
  workspace: 1,
1631
1637
  common: 2
1632
1638
  };
1633
- return services.sort((left, right) => rank[left.source] - rank[right.source] || new URL(left.url).port.localeCompare(new URL(right.url).port, void 0, { numeric: true }));
1639
+ const sorted = services.sort((left, right) => rank[left.source] - rank[right.source] || new URL(left.url).port.localeCompare(new URL(right.url).port, void 0, { numeric: true }));
1640
+ const pages = /* @__PURE__ */ new Set();
1641
+ return sorted.filter((service) => {
1642
+ const url = new URL(service.url);
1643
+ const path = service.title === void 0 ? url.pathname : url.pathname.replace(/\/index\.html?$/iu, "/");
1644
+ const key = `${url.origin}${path}${url.search}${url.hash}\n${service.title ?? ""}`;
1645
+ if (pages.has(key)) return false;
1646
+ pages.add(key);
1647
+ return true;
1648
+ });
1634
1649
  }
1635
1650
  //#endregion
1636
1651
  //#region src/host/store-schema.ts
package/market/index.json CHANGED
@@ -101,8 +101,8 @@
101
101
  },
102
102
  "versions": [
103
103
  {
104
- "version": "1.2.2",
105
- "publishTimestamp": 1793526077,
104
+ "version": "1.2.4",
105
+ "publishTimestamp": 1793932197,
106
106
  "source": {
107
107
  "type": "npm",
108
108
  "registry": "https://registry.npmjs.org"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-figmark",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "private": false,
5
5
  "description": "DSH 插件:在右侧栏预览里对科研配图、SVG、PDF 与本机网页直接圈画批注,标注写进侧车文件并可一键交给智能体改图源",
6
6
  "license": "MIT",