pi-studio 0.9.17 → 0.9.18

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.
@@ -131,6 +131,7 @@
131
131
  const shortcutsBtn = document.getElementById("shortcutsBtn");
132
132
  const shortcutsOverlayEl = document.getElementById("shortcutsOverlay");
133
133
  const shortcutsDialogEl = document.getElementById("shortcutsDialog");
134
+ const shortcutsBodyEl = document.getElementById("shortcutsBody");
134
135
  const shortcutsCloseBtn = document.getElementById("shortcutsCloseBtn");
135
136
  const leftFocusBtn = document.getElementById("leftFocusBtn");
136
137
  const rightFocusBtn = document.getElementById("rightFocusBtn");
@@ -211,6 +212,18 @@
211
212
  let studioHtmlFocusFullscreenBtn = null;
212
213
  let studioHtmlFocusLastFocusedEl = null;
213
214
  let studioHtmlFocusRestoreState = null;
215
+ let studioImageFocusOverlayEl = null;
216
+ let studioImageFocusDialogEl = null;
217
+ let studioImageFocusSlotEl = null;
218
+ let studioImageFocusImgEl = null;
219
+ let studioImageFocusTitleEl = null;
220
+ let studioImageFocusOpenLinkEl = null;
221
+ let studioImageFocusFullscreenBtn = null;
222
+ let studioImageFocusCloseBtn = null;
223
+ let studioImageFocusZoomLabelEl = null;
224
+ let studioImageFocusLastFocusedEl = null;
225
+ let studioImageFocusZoomMode = "fit";
226
+ let studioImageFocusZoom = 1;
214
227
  let pendingRequestId = null;
215
228
  let pendingKind = null;
216
229
  let stickyStudioKind = null;
@@ -1914,6 +1927,8 @@
1914
1927
  matlab: { label: "MATLAB", exts: ["m"] },
1915
1928
  latex: { label: "LaTeX", exts: ["tex", "latex"] },
1916
1929
  diff: { label: "Diff", exts: ["diff", "patch"] },
1930
+ csv: { label: "CSV", exts: ["csv"] },
1931
+ tsv: { label: "TSV", exts: ["tsv"] },
1917
1932
  // Languages accepted for upload/detect but without syntax highlighting
1918
1933
  java: { label: "Java", exts: ["java"] },
1919
1934
  go: { label: "Go", exts: ["go"] },
@@ -1941,6 +1956,9 @@
1941
1956
  const ANNOTATION_MODE_STORAGE_KEY = "piStudio.annotationsEnabled";
1942
1957
  const PREVIEW_INPUT_DEBOUNCE_MS = 0;
1943
1958
  const PREVIEW_PENDING_BADGE_DELAY_MS = 220;
1959
+ const DELIMITED_PREVIEW_MAX_DATA_ROWS = 200;
1960
+ const DELIMITED_PREVIEW_MAX_COLUMNS = 50;
1961
+ const DELIMITED_PREVIEW_MAX_CELL_CHARS = 500;
1944
1962
  const previewPendingTimers = new WeakMap();
1945
1963
  const htmlArtifactFramesById = new Map();
1946
1964
  let sourcePreviewRenderTimer = null;
@@ -3666,6 +3684,12 @@
3666
3684
  && typeof studioHtmlFocusShellEl.contains === "function"
3667
3685
  && studioHtmlFocusShellEl.contains(event.target)
3668
3686
  );
3687
+ const imageFocusOwnsEvent = Boolean(
3688
+ studioImageFocusDialogEl
3689
+ && event.target
3690
+ && typeof studioImageFocusDialogEl.contains === "function"
3691
+ && studioImageFocusDialogEl.contains(event.target)
3692
+ );
3669
3693
  const quizOwnsEvent = Boolean(
3670
3694
  quizDialogEl
3671
3695
  && event.target
@@ -3691,6 +3715,14 @@
3691
3715
  return;
3692
3716
  }
3693
3717
 
3718
+ if (isStudioImageFocusOpen() && plainEscape) {
3719
+ event.preventDefault();
3720
+ closeStudioImageFocusViewer();
3721
+ return;
3722
+ }
3723
+
3724
+ if (handleStudioImageFocusShortcut(event)) return;
3725
+
3694
3726
  if (isScratchpadOpen() && plainEscape) {
3695
3727
  event.preventDefault();
3696
3728
  closeScratchpad();
@@ -3703,6 +3735,8 @@
3703
3735
  return;
3704
3736
  }
3705
3737
 
3738
+ if (handleShortcutsScrollShortcut(event)) return;
3739
+
3706
3740
  if (isReviewNotesOpen() && plainEscape) {
3707
3741
  event.preventDefault();
3708
3742
  closeReviewNotes();
@@ -3715,7 +3749,7 @@
3715
3749
  return;
3716
3750
  }
3717
3751
 
3718
- if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || shortcutsOwnsEvent || pdfFocusOwnsEvent || htmlFocusOwnsEvent || quizOwnsEvent) {
3752
+ if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || shortcutsOwnsEvent || pdfFocusOwnsEvent || htmlFocusOwnsEvent || imageFocusOwnsEvent || quizOwnsEvent) {
3719
3753
  return;
3720
3754
  }
3721
3755
 
@@ -3890,6 +3924,22 @@
3890
3924
  }
3891
3925
  }
3892
3926
 
3927
+ function formatStudioExportTimestamp(date) {
3928
+ const value = date instanceof Date ? date : new Date();
3929
+ const pad = (part) => String(part).padStart(2, "0");
3930
+ try {
3931
+ return String(value.getFullYear())
3932
+ + pad(value.getMonth() + 1)
3933
+ + pad(value.getDate())
3934
+ + "-"
3935
+ + pad(value.getHours())
3936
+ + pad(value.getMinutes())
3937
+ + pad(value.getSeconds());
3938
+ } catch {
3939
+ return String(Date.now());
3940
+ }
3941
+ }
3942
+
3893
3943
  function normalizeHistoryKind(kind) {
3894
3944
  return kind === "critique" ? "critique" : "annotation";
3895
3945
  }
@@ -4257,9 +4307,202 @@
4257
4307
  return marker + (lang ? lang : "") + newline + source + newline + marker;
4258
4308
  }
4259
4309
 
4310
+ function getDelimitedTextPreviewConfig(language) {
4311
+ const lang = normalizeFenceLanguage(language || "");
4312
+ if (lang === "csv") return { kind: "csv", label: "CSV", delimiter: "," };
4313
+ if (lang === "tsv") return { kind: "tsv", label: "TSV", delimiter: "\t" };
4314
+ return null;
4315
+ }
4316
+
4317
+ function parseDelimitedTextRows(text, delimiter, maxRows) {
4318
+ const source = String(text || "").replace(/^\uFEFF/, "");
4319
+ const limit = Math.max(1, Number(maxRows) || (DELIMITED_PREVIEW_MAX_DATA_ROWS + 1));
4320
+ const rows = [];
4321
+ let row = [];
4322
+ let cell = "";
4323
+ let inQuotes = false;
4324
+ let truncatedRows = false;
4325
+
4326
+ const pushCell = () => {
4327
+ row.push(cell);
4328
+ cell = "";
4329
+ };
4330
+ const pushRow = (index) => {
4331
+ pushCell();
4332
+ rows.push(row);
4333
+ row = [];
4334
+ if (rows.length >= limit) {
4335
+ truncatedRows = index < source.length - 1;
4336
+ return true;
4337
+ }
4338
+ return false;
4339
+ };
4340
+
4341
+ for (let i = 0; i < source.length; i += 1) {
4342
+ if (rows.length >= limit) {
4343
+ truncatedRows = true;
4344
+ break;
4345
+ }
4346
+ const ch = source[i];
4347
+ if (inQuotes) {
4348
+ if (ch === '"') {
4349
+ if (source[i + 1] === '"') {
4350
+ cell += '"';
4351
+ i += 1;
4352
+ } else {
4353
+ inQuotes = false;
4354
+ }
4355
+ } else {
4356
+ cell += ch;
4357
+ }
4358
+ continue;
4359
+ }
4360
+ if (ch === '"' && cell === "") {
4361
+ inQuotes = true;
4362
+ continue;
4363
+ }
4364
+ if (ch === delimiter) {
4365
+ pushCell();
4366
+ continue;
4367
+ }
4368
+ if (ch === "\n") {
4369
+ if (pushRow(i)) break;
4370
+ continue;
4371
+ }
4372
+ if (ch === "\r") {
4373
+ if (source[i + 1] === "\n") i += 1;
4374
+ if (pushRow(i)) break;
4375
+ continue;
4376
+ }
4377
+ cell += ch;
4378
+ }
4379
+
4380
+ if (!truncatedRows && rows.length < limit && (cell.length > 0 || row.length > 0)) {
4381
+ pushCell();
4382
+ rows.push(row);
4383
+ }
4384
+
4385
+ return { rows, truncatedRows };
4386
+ }
4387
+
4388
+ function buildDelimitedTextPreviewModel(text, language) {
4389
+ const config = getDelimitedTextPreviewConfig(language);
4390
+ if (!config) return null;
4391
+ const parsed = parseDelimitedTextRows(text, config.delimiter, DELIMITED_PREVIEW_MAX_DATA_ROWS + 1);
4392
+ const rows = parsed.rows;
4393
+ const rawColumnCount = rows.reduce((max, row) => Math.max(max, Array.isArray(row) ? row.length : 0), 0);
4394
+ const columnCount = Math.min(rawColumnCount, DELIMITED_PREVIEW_MAX_COLUMNS);
4395
+ const header = rows[0] || [];
4396
+ const dataRows = rows.slice(1);
4397
+ return {
4398
+ ...config,
4399
+ rows,
4400
+ header,
4401
+ dataRows,
4402
+ rawColumnCount,
4403
+ columnCount,
4404
+ truncatedColumns: rawColumnCount > columnCount,
4405
+ truncatedRows: parsed.truncatedRows,
4406
+ };
4407
+ }
4408
+
4409
+ function getDelimitedHeaderLabel(header, index) {
4410
+ const value = String((header && header[index]) || "").trim();
4411
+ return value || ("Column " + (index + 1));
4412
+ }
4413
+
4414
+ function formatDelimitedPreviewCellHtml(value) {
4415
+ const raw = String(value ?? "");
4416
+ if (raw.length <= DELIMITED_PREVIEW_MAX_CELL_CHARS) return escapeHtml(raw);
4417
+ return escapeHtml(raw.slice(0, DELIMITED_PREVIEW_MAX_CELL_CHARS)) + "<span class='delimited-preview-truncation'>…</span>";
4418
+ }
4419
+
4420
+ function formatDelimitedMarkdownCell(value) {
4421
+ const raw = String(value ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
4422
+ const shortened = raw.length > DELIMITED_PREVIEW_MAX_CELL_CHARS
4423
+ ? raw.slice(0, DELIMITED_PREVIEW_MAX_CELL_CHARS) + "…"
4424
+ : raw;
4425
+ return shortened.replace(/\n/g, "<br>").replace(/\|/g, "\\|").trim() || " ";
4426
+ }
4427
+
4428
+ function buildDelimitedTextPreviewHtml(text, language) {
4429
+ const model = buildDelimitedTextPreviewModel(text, language);
4430
+ if (!model) return "";
4431
+ if (!model.rows.length || model.columnCount <= 0) {
4432
+ return "<div class='delimited-preview rendered-markdown'><div class='delimited-preview-header'><strong>" + escapeHtml(model.label) + " preview</strong></div><pre class='plain-markdown'>No tabular data to preview.</pre></div>";
4433
+ }
4434
+ const columnIndexes = Array.from({ length: model.columnCount }, (_, index) => index);
4435
+ const headerHtml = columnIndexes.map((index) => "<th scope='col'>" + escapeHtml(getDelimitedHeaderLabel(model.header, index)) + "</th>").join("");
4436
+ const bodyHtml = model.dataRows.length
4437
+ ? model.dataRows.map((row, rowIndex) => {
4438
+ const cells = columnIndexes.map((index) => {
4439
+ const raw = String((row && row[index]) ?? "");
4440
+ const emptyClass = raw.length === 0 ? " delimited-preview-empty-cell" : "";
4441
+ return "<td class='" + emptyClass.trim() + "'>" + formatDelimitedPreviewCellHtml(raw) + "</td>";
4442
+ }).join("");
4443
+ return "<tr><th scope='row' class='delimited-preview-row-number'>" + String(rowIndex + 1) + "</th>" + cells + "</tr>";
4444
+ }).join("")
4445
+ : "<tr><td colspan='" + String(model.columnCount + 1) + "' class='delimited-preview-empty'>No data rows after the header.</td></tr>";
4446
+ const notices = [];
4447
+ if (model.truncatedRows) notices.push("Showing first " + String(Math.max(0, model.dataRows.length)) + " data rows.");
4448
+ if (model.truncatedColumns) notices.push("Showing first " + String(model.columnCount) + " of " + String(model.rawColumnCount) + " columns.");
4449
+ const noticeHtml = notices.length ? "<div class='preview-warning delimited-preview-notice'>" + escapeHtml(notices.join(" ")) + "</div>" : "";
4450
+ const summaryParts = [String(model.dataRows.length) + (model.truncatedRows ? "+" : "") + " data rows", String(model.rawColumnCount) + " columns"];
4451
+ return "<div class='delimited-preview rendered-markdown'>"
4452
+ + "<div class='delimited-preview-header'><div><strong>" + escapeHtml(model.label) + " preview</strong><span>" + escapeHtml(summaryParts.join(" · ")) + "</span></div></div>"
4453
+ + noticeHtml
4454
+ + "<div class='delimited-preview-table-wrap'><table>"
4455
+ + "<thead><tr><th scope='col' class='delimited-preview-row-number'>#</th>" + headerHtml + "</tr></thead>"
4456
+ + "<tbody>" + bodyHtml + "</tbody>"
4457
+ + "</table></div>"
4458
+ + "</div>";
4459
+ }
4460
+
4461
+ function buildDelimitedTextPreviewMarkdown(text, language) {
4462
+ const model = buildDelimitedTextPreviewModel(text, language);
4463
+ if (!model) return "";
4464
+ if (!model.rows.length || model.columnCount <= 0) return "_No tabular data to preview._";
4465
+ const columnIndexes = Array.from({ length: model.columnCount }, (_, index) => index);
4466
+ const lines = ["**" + model.label + " preview**", ""];
4467
+ const notices = [];
4468
+ if (model.truncatedRows) notices.push("showing first " + String(Math.max(0, model.dataRows.length)) + " data rows");
4469
+ if (model.truncatedColumns) notices.push("showing first " + String(model.columnCount) + " of " + String(model.rawColumnCount) + " columns");
4470
+ if (notices.length) lines.push("_" + notices.join("; ") + "._", "");
4471
+ lines.push("| " + columnIndexes.map((index) => formatDelimitedMarkdownCell(getDelimitedHeaderLabel(model.header, index))).join(" | ") + " |");
4472
+ lines.push("| " + columnIndexes.map(() => "---").join(" | ") + " |");
4473
+ if (model.dataRows.length) {
4474
+ model.dataRows.forEach((row) => {
4475
+ lines.push("| " + columnIndexes.map((index) => formatDelimitedMarkdownCell(row && row[index])).join(" | ") + " |");
4476
+ });
4477
+ } else {
4478
+ lines.push("| " + columnIndexes.map(() => " ").join(" | ") + " |");
4479
+ }
4480
+ return lines.join("\n");
4481
+ }
4482
+
4483
+ function renderDelimitedTextPreview(targetEl, text, pane) {
4484
+ const html = buildDelimitedTextPreviewHtml(text, editorLanguage || "");
4485
+ if (!html || !targetEl) return false;
4486
+ if (pane === "source") {
4487
+ sourcePreviewRenderNonce += 1;
4488
+ } else if (pane === "response") {
4489
+ responsePreviewRenderNonce += 1;
4490
+ }
4491
+ clearPreviewJumpHighlight(targetEl);
4492
+ finishPreviewRender(targetEl);
4493
+ targetEl.innerHTML = html;
4494
+ if (pane === "response") {
4495
+ applyPendingResponseScrollReset();
4496
+ scheduleResponsePaneRepaintNudge();
4497
+ }
4498
+ return true;
4499
+ }
4500
+
4260
4501
  function prepareEditorTextForPdfExport(text) {
4261
4502
  const prepared = prepareEditorTextForPreview(text);
4262
4503
  const lang = normalizeFenceLanguage(editorLanguage || "");
4504
+ const delimitedPreview = buildDelimitedTextPreviewMarkdown(prepared, lang);
4505
+ if (delimitedPreview) return delimitedPreview;
4263
4506
  if (lang && lang !== "markdown" && lang !== "latex") {
4264
4507
  return wrapAsFencedCodeBlock(prepared, lang);
4265
4508
  }
@@ -4269,6 +4512,8 @@
4269
4512
  function prepareEditorTextForHtmlExport(text) {
4270
4513
  const prepared = prepareEditorTextForPreview(text);
4271
4514
  const lang = normalizeFenceLanguage(editorLanguage || "");
4515
+ const delimitedPreview = buildDelimitedTextPreviewMarkdown(prepared, lang);
4516
+ if (delimitedPreview) return delimitedPreview;
4272
4517
  if (lang && lang !== "markdown" && lang !== "latex") {
4273
4518
  return wrapAsFencedCodeBlock(prepared, lang);
4274
4519
  }
@@ -5096,13 +5341,12 @@
5096
5341
  return;
5097
5342
  }
5098
5343
  if (kind === "image") {
5099
- const pendingWindow = window.open("", "_blank");
5100
- void openPreviewImageLink(context.href, context.title, context, pendingWindow).catch((error) => {
5344
+ void openPreviewImageLink(context.href, context.title, context).catch((error) => {
5101
5345
  setStatus((error && error.message) ? error.message : String(error || "Could not open linked image."), "warning");
5102
5346
  });
5103
5347
  return;
5104
5348
  }
5105
- if (kind === "text") {
5349
+ if (kind === "text" || kind === "office") {
5106
5350
  const pendingWindow = window.open("", "_blank");
5107
5351
  void openPreviewDocumentInNewEditor(context.href, pendingWindow, context).catch((error) => {
5108
5352
  setStatus((error && error.message) ? error.message : String(error || "Could not open linked file."), "warning");
@@ -5772,6 +6016,394 @@
5772
6016
  }
5773
6017
  }
5774
6018
 
6019
+ function isStudioImageFocusOpen() {
6020
+ return Boolean(studioImageFocusOverlayEl && studioImageFocusOverlayEl.hidden === false);
6021
+ }
6022
+
6023
+ function isStudioImageFocusSrcAllowed(src) {
6024
+ const value = String(src || "").trim();
6025
+ if (!value) return false;
6026
+ if (/^javascript:/i.test(value)) return false;
6027
+ return /^(?:data:image\/|blob:|https?:|file:|\/|\.\/|\.\.\/)/i.test(value);
6028
+ }
6029
+
6030
+ function clampStudioImageFocusZoom(value) {
6031
+ const parsed = Number(value);
6032
+ if (!Number.isFinite(parsed) || parsed <= 0) return 1;
6033
+ return Math.max(0.1, Math.min(8, parsed));
6034
+ }
6035
+
6036
+ function getStudioImageFocusFitScale() {
6037
+ const img = studioImageFocusImgEl;
6038
+ const slot = studioImageFocusSlotEl;
6039
+ if (!img || !slot) return 1;
6040
+ const naturalWidth = Number(img.naturalWidth) || 0;
6041
+ const naturalHeight = Number(img.naturalHeight) || 0;
6042
+ if (naturalWidth <= 0 || naturalHeight <= 0) return 1;
6043
+ let paddingX = 0;
6044
+ let paddingY = 0;
6045
+ try {
6046
+ const style = window.getComputedStyle(slot);
6047
+ paddingX = (Number.parseFloat(style.paddingLeft) || 0) + (Number.parseFloat(style.paddingRight) || 0);
6048
+ paddingY = (Number.parseFloat(style.paddingTop) || 0) + (Number.parseFloat(style.paddingBottom) || 0);
6049
+ } catch {}
6050
+ const availableWidth = Math.max(1, (slot.clientWidth || 0) - paddingX);
6051
+ const availableHeight = Math.max(1, (slot.clientHeight || 0) - paddingY);
6052
+ return clampStudioImageFocusZoom(Math.min(1, availableWidth / naturalWidth, availableHeight / naturalHeight));
6053
+ }
6054
+
6055
+ function getStudioImageFocusDisplayScale() {
6056
+ return studioImageFocusZoomMode === "fit"
6057
+ ? getStudioImageFocusFitScale()
6058
+ : clampStudioImageFocusZoom(studioImageFocusZoom);
6059
+ }
6060
+
6061
+ function syncStudioImageFocusZoom() {
6062
+ if (!studioImageFocusImgEl || !studioImageFocusSlotEl) return;
6063
+ const fitMode = studioImageFocusZoomMode === "fit";
6064
+ studioImageFocusSlotEl.classList.toggle("is-fit", fitMode);
6065
+ studioImageFocusSlotEl.classList.toggle("is-zoomed", !fitMode);
6066
+ if (fitMode) {
6067
+ studioImageFocusImgEl.style.width = "";
6068
+ studioImageFocusImgEl.style.height = "";
6069
+ studioImageFocusImgEl.style.maxWidth = "100%";
6070
+ studioImageFocusImgEl.style.maxHeight = "100%";
6071
+ } else {
6072
+ const zoom = clampStudioImageFocusZoom(studioImageFocusZoom);
6073
+ const naturalWidth = Number(studioImageFocusImgEl.naturalWidth) || 0;
6074
+ studioImageFocusImgEl.style.maxWidth = "none";
6075
+ studioImageFocusImgEl.style.maxHeight = "none";
6076
+ studioImageFocusImgEl.style.height = "auto";
6077
+ studioImageFocusImgEl.style.width = naturalWidth > 0 ? Math.max(1, Math.round(naturalWidth * zoom)) + "px" : Math.round(zoom * 100) + "%";
6078
+ }
6079
+ if (studioImageFocusZoomLabelEl) {
6080
+ studioImageFocusZoomLabelEl.textContent = Math.round(getStudioImageFocusDisplayScale() * 100) + "%";
6081
+ }
6082
+ }
6083
+
6084
+ function getStudioImageFocusViewportCenter() {
6085
+ const slot = studioImageFocusSlotEl;
6086
+ if (!slot) return { x: 0.5, y: 0.5 };
6087
+ const scrollWidth = Math.max(slot.scrollWidth || 0, slot.clientWidth || 0, 1);
6088
+ const scrollHeight = Math.max(slot.scrollHeight || 0, slot.clientHeight || 0, 1);
6089
+ return {
6090
+ x: Math.max(0, Math.min(1, (slot.scrollLeft + (slot.clientWidth || 0) / 2) / scrollWidth)),
6091
+ y: Math.max(0, Math.min(1, (slot.scrollTop + (slot.clientHeight || 0) / 2) / scrollHeight)),
6092
+ };
6093
+ }
6094
+
6095
+ function restoreStudioImageFocusViewportCenter(center) {
6096
+ const slot = studioImageFocusSlotEl;
6097
+ if (!slot || !center) return;
6098
+ const schedule = typeof window.requestAnimationFrame === "function"
6099
+ ? window.requestAnimationFrame.bind(window)
6100
+ : (callback) => window.setTimeout(callback, 0);
6101
+ schedule(() => {
6102
+ if (!slot.isConnected || studioImageFocusZoomMode === "fit") return;
6103
+ const maxLeft = Math.max(0, (slot.scrollWidth || 0) - (slot.clientWidth || 0));
6104
+ const maxTop = Math.max(0, (slot.scrollHeight || 0) - (slot.clientHeight || 0));
6105
+ slot.scrollLeft = Math.max(0, Math.min(maxLeft, (slot.scrollWidth || 0) * center.x - (slot.clientWidth || 0) / 2));
6106
+ slot.scrollTop = Math.max(0, Math.min(maxTop, (slot.scrollHeight || 0) * center.y - (slot.clientHeight || 0) / 2));
6107
+ });
6108
+ }
6109
+
6110
+ function getStudioImageFocusPointerCenter(event) {
6111
+ const slot = studioImageFocusSlotEl;
6112
+ if (!slot || !event || typeof slot.getBoundingClientRect !== "function") return getStudioImageFocusViewportCenter();
6113
+ const rect = slot.getBoundingClientRect();
6114
+ const scrollWidth = Math.max(slot.scrollWidth || 0, slot.clientWidth || 0, 1);
6115
+ const scrollHeight = Math.max(slot.scrollHeight || 0, slot.clientHeight || 0, 1);
6116
+ return {
6117
+ x: Math.max(0, Math.min(1, (slot.scrollLeft + (Number(event.clientX) || rect.left + rect.width / 2) - rect.left) / scrollWidth)),
6118
+ y: Math.max(0, Math.min(1, (slot.scrollTop + (Number(event.clientY) || rect.top + rect.height / 2) - rect.top) / scrollHeight)),
6119
+ };
6120
+ }
6121
+
6122
+ function setStudioImageFocusZoom(mode, zoom, options) {
6123
+ const center = options && options.center ? options.center : getStudioImageFocusViewportCenter();
6124
+ studioImageFocusZoomMode = mode === "fit" ? "fit" : "custom";
6125
+ studioImageFocusZoom = clampStudioImageFocusZoom(zoom);
6126
+ syncStudioImageFocusZoom();
6127
+ if (studioImageFocusZoomMode !== "fit") restoreStudioImageFocusViewportCenter(center);
6128
+ }
6129
+
6130
+ function zoomStudioImageFocus(factor, options) {
6131
+ const base = studioImageFocusZoomMode === "fit" ? getStudioImageFocusFitScale() : studioImageFocusZoom;
6132
+ setStudioImageFocusZoom("custom", clampStudioImageFocusZoom(base * factor), options);
6133
+ }
6134
+
6135
+ function handleStudioImageFocusWheel(event) {
6136
+ if (!isStudioImageFocusOpen() || !event) return;
6137
+ if (!event.altKey && !event.ctrlKey && !event.metaKey) return;
6138
+ event.preventDefault();
6139
+ event.stopPropagation();
6140
+ const delta = Number(event.deltaY) || 0;
6141
+ const factor = delta < 0 ? 1.12 : 1 / 1.12;
6142
+ zoomStudioImageFocus(factor, { center: getStudioImageFocusPointerCenter(event) });
6143
+ }
6144
+
6145
+ function handleStudioImageFocusShortcut(event) {
6146
+ if (!isStudioImageFocusOpen() || !event) return false;
6147
+ if (isTextEntryShortcutTarget(event.target)) return false;
6148
+ const key = typeof event.key === "string" ? event.key : "";
6149
+ const code = typeof event.code === "string" ? event.code : "";
6150
+ if (!event.altKey || event.metaKey || event.ctrlKey) return false;
6151
+ if (code === "Equal" || code === "NumpadAdd" || key === "=" || key === "+") {
6152
+ event.preventDefault();
6153
+ zoomStudioImageFocus(1.25);
6154
+ return true;
6155
+ }
6156
+ if (code === "Minus" || code === "NumpadSubtract" || key === "-" || key === "_") {
6157
+ event.preventDefault();
6158
+ zoomStudioImageFocus(1 / 1.25);
6159
+ return true;
6160
+ }
6161
+ if (code === "Digit0" || code === "Numpad0" || key === "0") {
6162
+ event.preventDefault();
6163
+ setStudioImageFocusZoom("fit", 1);
6164
+ return true;
6165
+ }
6166
+ return false;
6167
+ }
6168
+
6169
+ function syncStudioImageFocusFullscreenButton() {
6170
+ if (!studioImageFocusFullscreenBtn) return;
6171
+ const isFullscreen = Boolean(document.fullscreenElement && studioImageFocusDialogEl && document.fullscreenElement === studioImageFocusDialogEl);
6172
+ studioImageFocusFullscreenBtn.replaceChildren(makeStudioUiRefreshIcon(isFullscreen ? "fullscreen-exit" : "fullscreen"));
6173
+ const label = isFullscreen ? "Exit fullscreen" : "Fullscreen";
6174
+ studioImageFocusFullscreenBtn.title = isFullscreen
6175
+ ? "Exit browser fullscreen and keep the image focus viewer open."
6176
+ : "Ask the browser to make this image viewer fullscreen.";
6177
+ studioImageFocusFullscreenBtn.setAttribute("aria-label", label);
6178
+ studioImageFocusFullscreenBtn.setAttribute("aria-pressed", isFullscreen ? "true" : "false");
6179
+ }
6180
+
6181
+ async function toggleStudioImageFocusFullscreen() {
6182
+ const dialog = studioImageFocusDialogEl;
6183
+ if (!dialog) return;
6184
+ const isFullscreen = Boolean(document.fullscreenElement && document.fullscreenElement === dialog);
6185
+ if (isFullscreen) {
6186
+ try {
6187
+ if (typeof document.exitFullscreen === "function") await document.exitFullscreen();
6188
+ } catch (error) {
6189
+ setStatus("Could not exit image fullscreen: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
6190
+ } finally {
6191
+ syncStudioImageFocusFullscreenButton();
6192
+ }
6193
+ return;
6194
+ }
6195
+ if (typeof dialog.requestFullscreen !== "function") {
6196
+ setStatus("Browser fullscreen is not available for this image viewer.", "warning");
6197
+ return;
6198
+ }
6199
+ try {
6200
+ await dialog.requestFullscreen();
6201
+ } catch (error) {
6202
+ setStatus("Could not enter image fullscreen: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
6203
+ } finally {
6204
+ syncStudioImageFocusFullscreenButton();
6205
+ }
6206
+ }
6207
+
6208
+ function appendStudioImageFocusTextButton(parent, label, title, onClick) {
6209
+ const button = document.createElement("button");
6210
+ button.type = "button";
6211
+ button.className = "studio-pdf-focus-btn studio-image-focus-zoom-btn";
6212
+ button.textContent = label;
6213
+ button.title = title;
6214
+ button.addEventListener("click", onClick);
6215
+ parent.appendChild(button);
6216
+ return button;
6217
+ }
6218
+
6219
+ function ensureStudioImageFocusViewer() {
6220
+ if (studioImageFocusOverlayEl) return studioImageFocusOverlayEl;
6221
+
6222
+ const overlay = document.createElement("div");
6223
+ overlay.className = "studio-pdf-focus-overlay studio-image-focus-overlay";
6224
+ overlay.hidden = true;
6225
+ overlay.setAttribute("role", "dialog");
6226
+ overlay.setAttribute("aria-modal", "true");
6227
+ overlay.setAttribute("aria-labelledby", "studioImageFocusTitle");
6228
+
6229
+ const dialog = document.createElement("div");
6230
+ dialog.className = "studio-pdf-focus-dialog studio-image-focus-dialog";
6231
+
6232
+ const header = document.createElement("div");
6233
+ header.className = "studio-pdf-focus-header studio-image-focus-header";
6234
+
6235
+ const titleGroup = document.createElement("div");
6236
+ titleGroup.className = "studio-pdf-focus-title-group";
6237
+
6238
+ const closeBtn = document.createElement("button");
6239
+ closeBtn.type = "button";
6240
+ closeBtn.className = "studio-pdf-focus-btn studio-pdf-focus-close";
6241
+ closeBtn.title = "Exit image focus view.";
6242
+ closeBtn.setAttribute("aria-label", "Exit image focus view");
6243
+ closeBtn.appendChild(makeStudioUiRefreshIcon("focus-exit"));
6244
+ closeBtn.addEventListener("click", () => closeStudioImageFocusViewer());
6245
+ titleGroup.appendChild(closeBtn);
6246
+
6247
+ const titleEl = document.createElement("div");
6248
+ titleEl.id = "studioImageFocusTitle";
6249
+ titleEl.className = "studio-pdf-focus-title";
6250
+ titleEl.textContent = "Image preview";
6251
+ titleGroup.appendChild(titleEl);
6252
+ header.appendChild(titleGroup);
6253
+
6254
+ const actions = document.createElement("div");
6255
+ actions.className = "studio-pdf-focus-actions studio-image-focus-actions";
6256
+
6257
+ const openLink = document.createElement("a");
6258
+ openLink.className = "studio-pdf-focus-link";
6259
+ openLink.target = "_blank";
6260
+ openLink.rel = "noopener noreferrer";
6261
+ openLink.textContent = "Open image";
6262
+ actions.appendChild(openLink);
6263
+
6264
+ appendStudioImageFocusTextButton(actions, "Fit", "Fit the image to the viewer.", () => setStudioImageFocusZoom("fit", 1));
6265
+ appendStudioImageFocusTextButton(actions, "100%", "Show the image at its natural pixel size.", () => setStudioImageFocusZoom("custom", 1));
6266
+ appendStudioImageFocusTextButton(actions, "−", "Zoom out.", () => zoomStudioImageFocus(1 / 1.25));
6267
+ const zoomLabel = document.createElement("span");
6268
+ zoomLabel.className = "studio-image-focus-zoom-label";
6269
+ zoomLabel.textContent = "100%";
6270
+ actions.appendChild(zoomLabel);
6271
+ appendStudioImageFocusTextButton(actions, "+", "Zoom in.", () => zoomStudioImageFocus(1.25));
6272
+ appendStudioImageFocusTextButton(actions, "Reset", "Reset image zoom to fit.", () => setStudioImageFocusZoom("fit", 1));
6273
+
6274
+ const fullscreenBtn = document.createElement("button");
6275
+ fullscreenBtn.type = "button";
6276
+ fullscreenBtn.className = "studio-pdf-focus-btn studio-pdf-focus-fullscreen";
6277
+ fullscreenBtn.addEventListener("click", () => {
6278
+ void toggleStudioImageFocusFullscreen();
6279
+ });
6280
+ actions.appendChild(fullscreenBtn);
6281
+
6282
+ header.appendChild(actions);
6283
+ dialog.appendChild(header);
6284
+
6285
+ const slot = document.createElement("div");
6286
+ slot.className = "studio-image-focus-slot is-fit";
6287
+ const img = document.createElement("img");
6288
+ img.className = "studio-image-focus-img";
6289
+ img.alt = "Image preview";
6290
+ img.addEventListener("load", syncStudioImageFocusZoom);
6291
+ slot.addEventListener("wheel", handleStudioImageFocusWheel, { passive: false });
6292
+ slot.appendChild(img);
6293
+ dialog.appendChild(slot);
6294
+
6295
+ overlay.appendChild(dialog);
6296
+ overlay.addEventListener("click", (event) => {
6297
+ if (event.target === overlay) closeStudioImageFocusViewer();
6298
+ });
6299
+ document.addEventListener("fullscreenchange", syncStudioImageFocusFullscreenButton);
6300
+
6301
+ document.body.appendChild(overlay);
6302
+ studioImageFocusOverlayEl = overlay;
6303
+ studioImageFocusDialogEl = dialog;
6304
+ studioImageFocusSlotEl = slot;
6305
+ studioImageFocusImgEl = img;
6306
+ studioImageFocusTitleEl = titleEl;
6307
+ studioImageFocusOpenLinkEl = openLink;
6308
+ studioImageFocusFullscreenBtn = fullscreenBtn;
6309
+ studioImageFocusCloseBtn = closeBtn;
6310
+ studioImageFocusZoomLabelEl = zoomLabel;
6311
+ syncStudioImageFocusFullscreenButton();
6312
+ return overlay;
6313
+ }
6314
+
6315
+ function openStudioImageFocusViewer(src, title) {
6316
+ const imageSrc = String(src || "").trim();
6317
+ if (!isStudioImageFocusSrcAllowed(imageSrc)) return false;
6318
+ ensureStudioImageFocusViewer();
6319
+ studioImageFocusLastFocusedEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
6320
+ const label = String(title || "Image preview").trim() || "Image preview";
6321
+ if (studioImageFocusTitleEl) studioImageFocusTitleEl.textContent = label;
6322
+ if (studioImageFocusOpenLinkEl) studioImageFocusOpenLinkEl.href = imageSrc;
6323
+ if (studioImageFocusImgEl) {
6324
+ studioImageFocusImgEl.alt = label;
6325
+ studioImageFocusImgEl.src = imageSrc;
6326
+ }
6327
+ studioImageFocusZoomMode = "fit";
6328
+ studioImageFocusZoom = 1;
6329
+ syncStudioImageFocusZoom();
6330
+ if (document.body) document.body.classList.add("studio-image-focus-open");
6331
+ if (studioImageFocusOverlayEl) studioImageFocusOverlayEl.hidden = false;
6332
+ syncStudioImageFocusFullscreenButton();
6333
+ closeStudioUiRefreshMenus();
6334
+ closeExportPreviewMenu();
6335
+ closePreviewLinkMenu();
6336
+ window.setTimeout(() => {
6337
+ if (studioImageFocusCloseBtn && typeof studioImageFocusCloseBtn.focus === "function") {
6338
+ studioImageFocusCloseBtn.focus();
6339
+ }
6340
+ }, 0);
6341
+ return true;
6342
+ }
6343
+
6344
+ function closeStudioImageFocusViewer() {
6345
+ if (!isStudioImageFocusOpen()) return false;
6346
+ if (document.fullscreenElement && studioImageFocusDialogEl && studioImageFocusDialogEl.contains(document.fullscreenElement)) {
6347
+ try {
6348
+ const exitResult = document.exitFullscreen && document.exitFullscreen();
6349
+ if (exitResult && typeof exitResult.catch === "function") exitResult.catch(() => {});
6350
+ } catch {}
6351
+ }
6352
+ if (studioImageFocusOverlayEl) studioImageFocusOverlayEl.hidden = true;
6353
+ if (studioImageFocusImgEl) studioImageFocusImgEl.removeAttribute("src");
6354
+ if (studioImageFocusOpenLinkEl) studioImageFocusOpenLinkEl.removeAttribute("href");
6355
+ if (document.body) document.body.classList.remove("studio-image-focus-open");
6356
+ syncStudioImageFocusFullscreenButton();
6357
+ const focusTarget = studioImageFocusLastFocusedEl;
6358
+ studioImageFocusLastFocusedEl = null;
6359
+ if (focusTarget && typeof focusTarget.focus === "function" && document.contains(focusTarget)) {
6360
+ window.setTimeout(() => focusTarget.focus(), 0);
6361
+ }
6362
+ return true;
6363
+ }
6364
+
6365
+ function getPreviewImageElementTitle(imageEl) {
6366
+ if (!imageEl) return "Image preview";
6367
+ const alt = typeof imageEl.getAttribute === "function" ? String(imageEl.getAttribute("alt") || "").trim() : "";
6368
+ const title = typeof imageEl.getAttribute === "function" ? String(imageEl.getAttribute("title") || "").trim() : "";
6369
+ const src = typeof imageEl.getAttribute === "function" ? String(imageEl.getAttribute("src") || "").trim() : "";
6370
+ const srcLabel = /^data:image\//i.test(src) ? "" : (src.length > 120 ? src.slice(0, 117) + "…" : src);
6371
+ return alt || title || srcLabel || "Image preview";
6372
+ }
6373
+
6374
+ function openPreviewImageElementInFocus(imageEl) {
6375
+ if (!imageEl) return false;
6376
+ const src = String(imageEl.currentSrc || imageEl.src || imageEl.getAttribute("src") || "").trim();
6377
+ if (!src) return false;
6378
+ return openStudioImageFocusViewer(src, getPreviewImageElementTitle(imageEl));
6379
+ }
6380
+
6381
+ function decoratePreviewImages(targetEl) {
6382
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
6383
+ const images = Array.from(targetEl.querySelectorAll("img[src]"));
6384
+ images.forEach((imageEl) => {
6385
+ if (!(imageEl instanceof HTMLImageElement)) return;
6386
+ if (imageEl.dataset && imageEl.dataset.studioImageFocusDecorated === "1") return;
6387
+ if (imageEl.closest && imageEl.closest("a[href], button, .studio-html-artifact-shell, .studio-pdf-card")) return;
6388
+ if (!isStudioImageFocusSrcAllowed(imageEl.currentSrc || imageEl.src || imageEl.getAttribute("src") || "")) return;
6389
+ imageEl.classList.add("studio-image-focus-target");
6390
+ imageEl.tabIndex = imageEl.tabIndex >= 0 ? imageEl.tabIndex : 0;
6391
+ imageEl.setAttribute("role", "button");
6392
+ imageEl.setAttribute("aria-label", "Open image focus viewer");
6393
+ if (imageEl.dataset) imageEl.dataset.studioImageFocusDecorated = "1";
6394
+ imageEl.addEventListener("click", (event) => {
6395
+ event.preventDefault();
6396
+ event.stopPropagation();
6397
+ if (!openPreviewImageElementInFocus(imageEl)) setStatus("Could not open image focus view.", "warning");
6398
+ });
6399
+ imageEl.addEventListener("keydown", (event) => {
6400
+ if (event.key !== "Enter" && event.key !== " ") return;
6401
+ event.preventDefault();
6402
+ if (!openPreviewImageElementInFocus(imageEl)) setStatus("Could not open image focus view.", "warning");
6403
+ });
6404
+ });
6405
+ }
6406
+
5775
6407
  function createStudioPdfCard(block, useEditorResourceContext) {
5776
6408
  const options = block && block.options ? block.options : {};
5777
6409
  const path = String(options.path || "").trim();
@@ -7003,15 +7635,16 @@
7003
7635
  const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
7004
7636
  const resourceDir = (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "";
7005
7637
  const isEditorPreview = rightView === "editor-preview";
7006
- const editorPdfLanguage = isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "";
7638
+ const editorIsDelimitedPreview = isEditorPreview && Boolean(getDelimitedTextPreviewConfig(editorLanguage || ""));
7639
+ const editorPdfLanguage = isEditorPreview ? (editorIsDelimitedPreview ? "markdown" : normalizeFenceLanguage(editorLanguage || "")) : "";
7007
7640
  const isLatex = isEditorPreview
7008
7641
  ? editorPdfLanguage === "latex"
7009
7642
  : /\\documentclass\b|\\begin\{document\}/.test(markdown);
7010
- let filenameHint = exportingReplJournal ? "repl-studio.pdf" : (isEditorPreview ? "studio-editor-preview.pdf" : "studio-response-preview.pdf");
7643
+ let filenameHint = exportingReplJournal ? "repl-studio.pdf" : (isEditorPreview ? "studio-editor-preview.pdf" : ("studio-response-" + formatStudioExportTimestamp() + ".studio.pdf"));
7011
7644
  if (sourcePath) {
7012
7645
  const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
7013
7646
  const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
7014
- filenameHint = stem + "-preview.pdf";
7647
+ filenameHint = stem + ".studio.pdf";
7015
7648
  }
7016
7649
 
7017
7650
  previewExportInProgress = true;
@@ -7059,6 +7692,8 @@
7059
7692
 
7060
7693
  const exportWarning = typeof payload.warning === "string" ? payload.warning.trim() : "";
7061
7694
  const openError = typeof payload.openError === "string" ? payload.openError.trim() : "";
7695
+ const writeError = typeof payload.writeError === "string" ? payload.writeError.trim() : "";
7696
+ const exportPath = typeof payload.path === "string" ? payload.path.trim() : "";
7062
7697
  const openedExternal = payload.openedExternal === true;
7063
7698
  let downloadName = typeof payload.filename === "string" && payload.filename.trim()
7064
7699
  ? payload.filename.trim()
@@ -7068,10 +7703,12 @@
7068
7703
  }
7069
7704
 
7070
7705
  if (openedExternal) {
7071
- if (exportWarning) {
7706
+ if (writeError) {
7707
+ setStatus("Opened PDF in default viewer, but could not write project file: " + writeError, "warning");
7708
+ } else if (exportWarning) {
7072
7709
  setStatus("Opened PDF in default viewer with warning: " + exportWarning, "warning");
7073
7710
  } else {
7074
- setStatus("Opened PDF in default viewer: " + downloadName, "success");
7711
+ setStatus("Opened PDF in default viewer: " + (exportPath || downloadName), "success");
7075
7712
  }
7076
7713
  return;
7077
7714
  }
@@ -7090,10 +7727,12 @@
7090
7727
  } else {
7091
7728
  setStatus("Opened browser fallback because external viewer failed (" + openError + ").", "warning");
7092
7729
  }
7730
+ } else if (writeError) {
7731
+ setStatus("Exported PDF to browser fallback; could not write project file: " + writeError, "warning");
7093
7732
  } else if (exportWarning) {
7094
- setStatus("Exported PDF with warning: " + exportWarning, "warning");
7733
+ setStatus("Exported PDF with warning" + (exportPath ? " to " + exportPath : ": " + exportWarning), "warning");
7095
7734
  } else {
7096
- setStatus("Exported PDF: " + downloadName, "success");
7735
+ setStatus("Exported PDF: " + (exportPath || downloadName), "success");
7097
7736
  }
7098
7737
  return;
7099
7738
  }
@@ -7169,16 +7808,17 @@
7169
7808
  const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
7170
7809
  const resourceDir = (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "";
7171
7810
  const isEditorPreview = rightView === "editor-preview";
7172
- const editorHtmlLanguage = htmlArtifactSource ? "html" : (isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "");
7811
+ const editorIsDelimitedPreview = isEditorPreview && Boolean(getDelimitedTextPreviewConfig(editorLanguage || ""));
7812
+ const editorHtmlLanguage = htmlArtifactSource ? "html" : (isEditorPreview ? (editorIsDelimitedPreview ? "markdown" : normalizeFenceLanguage(editorLanguage || "")) : "");
7173
7813
  const isLatex = htmlArtifactSource ? false : (isEditorPreview
7174
7814
  ? editorHtmlLanguage === "latex"
7175
7815
  : /\\documentclass\b|\\begin\{document\}/.test(markdown));
7176
- let filenameHint = exportingReplJournal ? "repl-studio.html" : (isEditorPreview ? "studio-editor-preview.html" : "studio-response-preview.html");
7816
+ let filenameHint = exportingReplJournal ? "repl-studio.html" : (isEditorPreview ? "studio-editor-preview.html" : ("studio-response-" + formatStudioExportTimestamp() + ".studio.html"));
7177
7817
  let titleHint = exportingReplJournal ? "Studio REPL Record" : (isEditorPreview ? "Studio editor preview" : "Studio response preview");
7178
7818
  if (sourcePath) {
7179
7819
  const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
7180
7820
  const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
7181
- filenameHint = stem + "-preview.html";
7821
+ filenameHint = stem + ".studio.html";
7182
7822
  titleHint = stem + " preview";
7183
7823
  }
7184
7824
 
@@ -7228,6 +7868,8 @@
7228
7868
 
7229
7869
  const exportWarning = typeof payload.warning === "string" ? payload.warning.trim() : "";
7230
7870
  const openError = typeof payload.openError === "string" ? payload.openError.trim() : "";
7871
+ const writeError = typeof payload.writeError === "string" ? payload.writeError.trim() : "";
7872
+ const exportPath = typeof payload.path === "string" ? payload.path.trim() : "";
7231
7873
  const openedExternal = payload.openedExternal === true;
7232
7874
  let downloadName = typeof payload.filename === "string" && payload.filename.trim()
7233
7875
  ? payload.filename.trim()
@@ -7237,10 +7879,12 @@
7237
7879
  }
7238
7880
 
7239
7881
  if (openedExternal) {
7240
- if (exportWarning) {
7882
+ if (writeError) {
7883
+ setStatus("Opened HTML in default browser, but could not write project file: " + writeError, "warning");
7884
+ } else if (exportWarning) {
7241
7885
  setStatus("Opened HTML in default browser with warning: " + exportWarning, "warning");
7242
7886
  } else {
7243
- setStatus("Opened HTML in default browser: " + downloadName, "success");
7887
+ setStatus("Opened HTML in default browser: " + (exportPath || downloadName), "success");
7244
7888
  }
7245
7889
  return;
7246
7890
  }
@@ -7259,10 +7903,12 @@
7259
7903
  } else {
7260
7904
  setStatus("Opened browser fallback because external viewer failed (" + openError + ").", "warning");
7261
7905
  }
7906
+ } else if (writeError) {
7907
+ setStatus("Exported HTML to browser fallback; could not write project file: " + writeError, "warning");
7262
7908
  } else if (exportWarning) {
7263
- setStatus("Exported HTML with warning: " + exportWarning, "warning");
7909
+ setStatus("Exported HTML with warning" + (exportPath ? " to " + exportPath : ": " + exportWarning), "warning");
7264
7910
  } else {
7265
- setStatus("Exported HTML: " + downloadName, "success");
7911
+ setStatus("Exported HTML: " + (exportPath || downloadName), "success");
7266
7912
  }
7267
7913
  return;
7268
7914
  }
@@ -7554,6 +8200,7 @@
7554
8200
  decorateRenderedEditorPreviewComments(targetEl, sourceTextEl.value || "");
7555
8201
  }
7556
8202
  decorateCopyablePreviewBlocks(targetEl);
8203
+ decoratePreviewImages(targetEl);
7557
8204
 
7558
8205
  // Warn if relative images are present but unlikely to resolve (non-file-backed content)
7559
8206
  if (!sourceState.path && !getCurrentResourceDirValue()) {
@@ -7593,6 +8240,9 @@
7593
8240
  renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
7594
8241
  return;
7595
8242
  }
8243
+ if (renderDelimitedTextPreview(sourcePreviewEl, text, "source")) {
8244
+ return;
8245
+ }
7596
8246
  if (supportsCodePreviewCommentsForCurrentEditor()) {
7597
8247
  renderCodePreviewWithCommentBlocks(sourcePreviewEl, text, "source");
7598
8248
  return;
@@ -8141,6 +8791,7 @@
8141
8791
  const previousScrollTop = critiqueViewEl.scrollTop;
8142
8792
  finishPreviewRender(critiqueViewEl);
8143
8793
  critiqueViewEl.innerHTML = buildTracePanelHtml();
8794
+ decoratePreviewImages(critiqueViewEl);
8144
8795
  critiqueViewEl.classList.remove("response-scroll-resetting");
8145
8796
  if (shouldStick) {
8146
8797
  critiqueViewEl.scrollTop = critiqueViewEl.scrollHeight;
@@ -8197,6 +8848,7 @@
8197
8848
  function getFileBrowserKindLabel(entry) {
8198
8849
  if (!entry || entry.type === "directory") return "folder";
8199
8850
  if (entry.kind === "text") return "document";
8851
+ if (entry.kind === "office") return "document";
8200
8852
  if (entry.kind === "pdf") return "PDF";
8201
8853
  if (entry.kind === "image") return "image";
8202
8854
  return entry.extension ? entry.extension.replace(/^\./, "") : "file";
@@ -8213,18 +8865,21 @@
8213
8865
  ? entries.map((entry) => {
8214
8866
  const type = entry.type === "directory" ? "directory" : "file";
8215
8867
  const kind = entry.kind || (type === "directory" ? "directory" : "other");
8216
- const icon = type === "directory" ? "📁" : (kind === "pdf" ? "📄" : (kind === "image" ? "🖼️" : (kind === "text" ? "📝" : "📦")));
8868
+ const icon = type === "directory" ? "📁" : (kind === "pdf" ? "📄" : (kind === "image" ? "🖼️" : (kind === "text" || kind === "office" ? "📝" : "📦")));
8217
8869
  const metaParts = [];
8218
8870
  metaParts.push(getFileBrowserKindLabel(entry));
8219
8871
  if (type === "file") metaParts.push(formatFileBrowserSize(entry.size));
8220
8872
  const time = formatFileBrowserTime(entry.mtimeMs);
8221
8873
  if (time) metaParts.push(time);
8222
- const textActions = kind === "text"
8223
- ? "<button type='button' data-files-action='open-new' data-files-path='" + escapeHtml(entry.path) + "'>New tab</button>"
8874
+ const newTabAction = kind === "text" || kind === "office"
8875
+ ? "open-new"
8876
+ : ((kind === "pdf" || kind === "image") ? "open-preview-new" : "");
8877
+ const textActions = newTabAction
8878
+ ? "<button type='button' data-files-action='" + escapeHtml(newTabAction) + "' data-files-path='" + escapeHtml(entry.path) + "'>New tab</button>"
8224
8879
  : "";
8225
8880
  const openTitle = type === "directory"
8226
8881
  ? "Open folder"
8227
- : (kind === "text" ? "Open in editor" : (kind === "pdf" ? "Open PDF preview" : (kind === "image" ? "Open image preview" : "Copy or reveal this file")));
8882
+ : (kind === "text" ? "Open in editor" : (kind === "office" ? "Convert to Markdown in editor" : (kind === "pdf" ? "Open PDF preview" : (kind === "image" ? "Open image preview" : "Copy or reveal this file"))));
8228
8883
  return "<div class='files-row files-row-" + escapeHtml(type) + " files-kind-" + escapeHtml(kind) + "'>"
8229
8884
  + "<button type='button' class='files-open-btn' data-files-action='" + (type === "directory" ? "open-dir" : "open") + "' data-files-path='" + escapeHtml(entry.path) + "' data-files-kind='" + escapeHtml(kind) + "' title='" + escapeHtml(openTitle) + "'>"
8230
8885
  + "<span class='files-icon' aria-hidden='true'>" + icon + "</span>"
@@ -8249,6 +8904,8 @@
8249
8904
  + "<div class='files-toolbar-actions'>"
8250
8905
  + "<button type='button' data-files-action='parent'" + parentDisabled + ">Parent</button>"
8251
8906
  + "<button type='button' data-files-action='refresh'>Refresh</button>"
8907
+ + (currentDir ? "<button type='button' data-files-action='copy-current' data-files-path='" + escapeHtml(currentDir) + "'>Copy path</button>" : "")
8908
+ + (currentDir ? "<button type='button' data-files-action='use-working-dir' data-files-path='" + escapeHtml(currentDir) + "'>Use as working dir</button>" : "")
8252
8909
  + (rootDir ? "<button type='button' data-files-action='copy-root' data-files-path='" + escapeHtml(rootDir) + "'>Copy root</button>" : "")
8253
8910
  + "</div>"
8254
8911
  + "</div>"
@@ -8343,7 +9000,7 @@
8343
9000
 
8344
9001
  async function openFileBrowserEntry(path, kind) {
8345
9002
  const context = getFileBrowserLocalLinkContext();
8346
- if (kind === "text") {
9003
+ if (kind === "text" || kind === "office") {
8347
9004
  await openPreviewDocumentHere(path, context);
8348
9005
  return;
8349
9006
  }
@@ -8358,6 +9015,19 @@
8358
9015
  setStatus("No Studio preview for this file type. Use Copy path or Reveal.", "warning");
8359
9016
  }
8360
9017
 
9018
+ function setFileBrowserCurrentDirectoryAsWorkingDir(path) {
9019
+ const nextDir = normalizeStudioResourceDirValue(path || fileBrowserState.currentDir || "");
9020
+ if (!nextDir) {
9021
+ setStatus("No current folder to use as working directory.", "warning");
9022
+ return;
9023
+ }
9024
+ if (resourceDirInput) resourceDirInput.value = nextDir;
9025
+ applyResourceDir();
9026
+ fileBrowserState = { ...fileBrowserState, contextKey: "" };
9027
+ if (rightView === "files") renderFilesView();
9028
+ setStatus("Working dir set to current folder.", "success");
9029
+ }
9030
+
8361
9031
  async function handleFilesPaneClick(event) {
8362
9032
  if (rightView !== "files") return;
8363
9033
  const target = event.target;
@@ -8388,11 +9058,19 @@
8388
9058
  await openPreviewDocumentInNewEditor(path, null, getFileBrowserLocalLinkContext());
8389
9059
  return;
8390
9060
  }
8391
- if (action === "copy-path" || action === "copy-root") {
9061
+ if (action === "open-preview-new") {
9062
+ await openPreviewResourceInNewEditor(path, null, getFileBrowserLocalLinkContext());
9063
+ return;
9064
+ }
9065
+ if (action === "copy-path" || action === "copy-root" || action === "copy-current") {
8392
9066
  const ok = await writeTextToClipboard(path);
8393
9067
  setStatus(ok ? "Copied path." : "Clipboard write failed.", ok ? "success" : "warning");
8394
9068
  return;
8395
9069
  }
9070
+ if (action === "use-working-dir") {
9071
+ setFileBrowserCurrentDirectoryAsWorkingDir(path);
9072
+ return;
9073
+ }
8396
9074
  if (action === "reveal") {
8397
9075
  await revealPreviewLocalLink(path, getFileBrowserLocalLinkContext());
8398
9076
  }
@@ -8429,6 +9107,9 @@
8429
9107
  renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
8430
9108
  return;
8431
9109
  }
9110
+ if (renderDelimitedTextPreview(critiqueViewEl, editorText, "response")) {
9111
+ return;
9112
+ }
8432
9113
  if (supportsCodePreviewCommentsForCurrentEditor()) {
8433
9114
  renderCodePreviewWithCommentBlocks(critiqueViewEl, editorText, "response");
8434
9115
  return;
@@ -8888,7 +9569,10 @@
8888
9569
  resourceDirInput.value = nextResourceDir;
8889
9570
  updateSourceBadge();
8890
9571
  }
8891
- if (typeof state.editorLanguage === "string" && state.editorLanguage.trim()) {
9572
+ const detectedPersistedPathLanguage = detectLanguageFromName(nextSourceState.path || nextSourceState.label || "");
9573
+ if (getDelimitedTextPreviewConfig(detectedPersistedPathLanguage)) {
9574
+ setEditorLanguage(detectedPersistedPathLanguage);
9575
+ } else if (typeof state.editorLanguage === "string" && state.editorLanguage.trim()) {
8892
9576
  setEditorLanguage(state.editorLanguage.trim());
8893
9577
  }
8894
9578
  editorView = state.editorView === "preview" ? "preview" : "markdown";
@@ -9791,6 +10475,7 @@
9791
10475
  ".diff", ".patch",
9792
10476
  ]);
9793
10477
  const PREVIEW_LOCAL_IMAGE_LINK_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
10478
+ const PREVIEW_LOCAL_OFFICE_LINK_EXTENSIONS = new Set([".docx", ".odt"]);
9794
10479
  const PREVIEW_LOCAL_TEXT_LINK_FILENAMES = new Set([
9795
10480
  ".dockerignore", ".editorconfig", ".env", ".env.example", ".eslintignore", ".gitattributes",
9796
10481
  ".gitignore", ".gitmodules", ".npmignore", ".prettierignore", "dockerfile", "gemfile",
@@ -9855,6 +10540,7 @@
9855
10540
  if (ext === ".pdf") return "pdf";
9856
10541
  if (PREVIEW_LOCAL_TEXT_LINK_EXTENSIONS.has(ext) || PREVIEW_LOCAL_TEXT_LINK_FILENAMES.has(name)) return "text";
9857
10542
  if (PREVIEW_LOCAL_IMAGE_LINK_EXTENSIONS.has(ext)) return "image";
10543
+ if (PREVIEW_LOCAL_OFFICE_LINK_EXTENSIONS.has(ext)) return "office";
9858
10544
  return "other";
9859
10545
  }
9860
10546
 
@@ -9948,11 +10634,16 @@
9948
10634
  };
9949
10635
  if (kind === "pdf") {
9950
10636
  appendPreviewLinkMenuButton(menu, "Open PDF preview", "open-pdf");
10637
+ appendPreviewLinkMenuButton(menu, "Open in new Studio tab", "open-preview-new");
9951
10638
  } else if (kind === "text") {
9952
10639
  appendPreviewLinkMenuButton(menu, "Open in new editor", "open-new");
9953
10640
  appendPreviewLinkMenuButton(menu, "Open here", "open-here");
10641
+ } else if (kind === "office") {
10642
+ appendPreviewLinkMenuButton(menu, "Convert in new editor", "open-new");
10643
+ appendPreviewLinkMenuButton(menu, "Convert here", "open-here");
9954
10644
  } else if (kind === "image") {
9955
10645
  appendPreviewLinkMenuButton(menu, "Open image preview", "open-image");
10646
+ appendPreviewLinkMenuButton(menu, "Open in new Studio tab", "open-preview-new");
9956
10647
  }
9957
10648
  appendPreviewLinkMenuButton(menu, "Reveal in file manager", "reveal");
9958
10649
  appendPreviewLinkMenuButton(menu, "Copy path", "copy-path");
@@ -9989,40 +10680,18 @@
9989
10680
  }
9990
10681
 
9991
10682
  async function openPreviewImageLink(href, title, contextOverride, pendingWindow) {
9992
- const popup = pendingWindow || window.open("", "_blank");
9993
- try {
9994
- if (popup && popup.document && popup.document.body) {
9995
- popup.document.title = "Opening image…";
9996
- popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Opening image…</p>";
9997
- }
9998
- } catch {}
9999
- try {
10000
- const payload = await fetchStudioJson("/html-preview-resource", {
10001
- query: getPreviewLinkResourceQuery(href, contextOverride),
10002
- });
10003
- const dataUrl = payload && typeof payload.dataUrl === "string" ? payload.dataUrl : "";
10004
- if (!dataUrl) throw new Error("Studio did not return image data.");
10005
- const safeTitle = escapeHtml(String(title || href || "Local image"));
10006
- const safeSrc = escapeHtml(dataUrl);
10007
- const html = "<!doctype html><html><head><meta charset='utf-8'><title>" + safeTitle + "</title>"
10008
- + "<style>body{margin:0;min-height:100vh;display:grid;place-items:center;background:#111;color:#eee;font:13px -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;}img{max-width:100vw;max-height:100vh;object-fit:contain;}header{position:fixed;left:0;right:0;top:0;padding:8px 10px;background:rgba(0,0,0,.55);backdrop-filter:blur(6px);}</style>"
10009
- + "</head><body><header>" + safeTitle + "</header><img src='" + safeSrc + "' alt='" + safeTitle + "'></body></html>";
10010
- if (popup && !popup.closed && popup.document) {
10011
- popup.document.open();
10012
- popup.document.write(html);
10013
- popup.document.close();
10014
- setStatus("Opened local image preview.", "success");
10015
- return;
10016
- }
10017
- const opened = window.open(dataUrl, "_blank");
10018
- if (!opened) throw new Error("Popup blocked while opening image preview.");
10019
- setStatus("Opened local image preview.", "success");
10020
- } catch (error) {
10021
- if (popup && !popup.closed) {
10022
- try { popup.close(); } catch {}
10023
- }
10024
- throw error;
10683
+ if (pendingWindow && !pendingWindow.closed) {
10684
+ try { pendingWindow.close(); } catch {}
10025
10685
  }
10686
+ const payload = await fetchStudioJson("/html-preview-resource", {
10687
+ query: getPreviewLinkResourceQuery(href, contextOverride),
10688
+ });
10689
+ const dataUrl = payload && typeof payload.dataUrl === "string" ? payload.dataUrl : "";
10690
+ if (!dataUrl) throw new Error("Studio did not return image data.");
10691
+ if (!openStudioImageFocusViewer(dataUrl, title || href || "Local image")) {
10692
+ throw new Error("Could not open image focus view.");
10693
+ }
10694
+ setStatus("Opened local image preview.", "success");
10026
10695
  }
10027
10696
 
10028
10697
  function editorHasPotentialUnsavedContent() {
@@ -10032,7 +10701,33 @@
10032
10701
  return true;
10033
10702
  }
10034
10703
 
10704
+ function getPreviewOfficeConversionLabel(href) {
10705
+ const cleanPath = stripPreviewLocalLinkUrlSuffix(href || "");
10706
+ const rawName = cleanPath.split(/[\\/]/).pop() || cleanPath || "this document";
10707
+ try {
10708
+ return decodeURIComponent(rawName) || rawName;
10709
+ } catch {
10710
+ return rawName;
10711
+ }
10712
+ }
10713
+
10714
+ function confirmPreviewOfficeConversion(href, destination) {
10715
+ if (getPreviewLocalLinkKind(href) !== "office") return true;
10716
+ const label = getPreviewOfficeConversionLabel(href);
10717
+ const target = destination === "here"
10718
+ ? "replace the current editor contents with an editable Markdown copy"
10719
+ : "open an editable Markdown copy in a new Studio tab";
10720
+ const confirmed = window.confirm(
10721
+ "Convert " + label + " to Markdown?\n\n"
10722
+ + "Studio will use Pandoc to " + target + ". Some layout or formatting may change. "
10723
+ + "The original DOCX/ODT file will not be overwritten, and edits will not round-trip back to it."
10724
+ );
10725
+ if (!confirmed) setStatus("Document conversion cancelled.", "warning");
10726
+ return confirmed;
10727
+ }
10728
+
10035
10729
  async function openPreviewDocumentHere(href, contextOverride) {
10730
+ if (!confirmPreviewOfficeConversion(href, "here")) return;
10036
10731
  if (editorHasPotentialUnsavedContent()) {
10037
10732
  const confirmed = window.confirm("Replace the current editor contents with this linked file? Unsaved editor changes may be lost.");
10038
10733
  if (!confirmed) return;
@@ -10042,18 +10737,29 @@
10042
10737
  const path = typeof payload.path === "string" ? payload.path : "";
10043
10738
  const label = typeof payload.label === "string" && payload.label.trim() ? payload.label.trim() : (path || "linked file");
10044
10739
  const nextResourceDir = typeof payload.resourceDir === "string" ? normalizeStudioResourceDirValue(payload.resourceDir) : "";
10740
+ const converted = payload && payload.converted === true;
10045
10741
  if (resourceDirInput && nextResourceDir) resourceDirInput.value = nextResourceDir;
10046
10742
  setEditorText(payload.text, { preserveScroll: false, preserveSelection: false });
10047
- setSourceState({ source: "file", label, path });
10048
- markFileBackedBaseline(payload.text);
10049
- const detected = detectLanguageFromName(path || label);
10743
+ if (converted) {
10744
+ setSourceState({ source: "blank", label, path: null });
10745
+ } else {
10746
+ setSourceState({ source: "file", label, path });
10747
+ markFileBackedBaseline(payload.text);
10748
+ }
10749
+ const detected = converted ? "markdown" : detectLanguageFromName(path || label);
10050
10750
  if (detected) setEditorLanguage(detected);
10051
10751
  setEditorView("markdown");
10052
10752
  setActivePane("left");
10053
- setStatus("Opened linked file in editor: " + label, "success");
10753
+ setStatus(converted ? ("Converted document into editor: " + label) : ("Opened linked file in editor: " + label), "success");
10054
10754
  }
10055
10755
 
10056
10756
  async function openPreviewDocumentInNewEditor(href, pendingWindow, contextOverride) {
10757
+ if (!confirmPreviewOfficeConversion(href, "new")) {
10758
+ if (pendingWindow && !pendingWindow.closed) {
10759
+ try { pendingWindow.close(); } catch {}
10760
+ }
10761
+ return;
10762
+ }
10057
10763
  const popup = pendingWindow || window.open("", "_blank");
10058
10764
  try {
10059
10765
  if (popup && popup.document && popup.document.body) {
@@ -10071,12 +10777,44 @@
10071
10777
  try {
10072
10778
  popup.opener = null;
10073
10779
  popup.location.href = targetUrl;
10074
- setStatus("Opening linked file in a new editor.", "success");
10780
+ setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening linked file in a new editor.", "success");
10075
10781
  return;
10076
10782
  } catch {}
10077
10783
  }
10078
10784
  window.open(targetUrl, "_blank", "noopener");
10079
- setStatus("Opening linked file in a new editor.", "success");
10785
+ setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening linked file in a new editor.", "success");
10786
+ } catch (error) {
10787
+ if (popup && !popup.closed) {
10788
+ try { popup.close(); } catch {}
10789
+ }
10790
+ throw error;
10791
+ }
10792
+ }
10793
+
10794
+ async function openPreviewResourceInNewEditor(href, pendingWindow, contextOverride) {
10795
+ const popup = pendingWindow || window.open("", "_blank");
10796
+ try {
10797
+ if (popup && popup.document && popup.document.body) {
10798
+ popup.document.title = "Opening preview…";
10799
+ popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Opening preview…</p>";
10800
+ }
10801
+ } catch {}
10802
+ try {
10803
+ const payload = await fetchPreviewLocalLink("preview-url", href, contextOverride);
10804
+ const targetUrl = payload && typeof payload.relativeUrl === "string"
10805
+ ? new URL(payload.relativeUrl, window.location.href).href
10806
+ : (payload && typeof payload.url === "string" ? payload.url : "");
10807
+ if (!targetUrl) throw new Error("Studio did not return a preview URL.");
10808
+ if (popup && !popup.closed) {
10809
+ try {
10810
+ popup.opener = null;
10811
+ popup.location.href = targetUrl;
10812
+ setStatus("Opening preview in a new Studio tab.", "success");
10813
+ return;
10814
+ } catch {}
10815
+ }
10816
+ window.open(targetUrl, "_blank", "noopener");
10817
+ setStatus("Opening preview in a new Studio tab.", "success");
10080
10818
  } catch (error) {
10081
10819
  if (popup && !popup.closed) {
10082
10820
  try { popup.close(); } catch {}
@@ -10115,6 +10853,10 @@
10115
10853
  await openPreviewDocumentInNewEditor(href, null, context);
10116
10854
  return;
10117
10855
  }
10856
+ if (action === "open-preview-new") {
10857
+ await openPreviewResourceInNewEditor(href, null, context);
10858
+ return;
10859
+ }
10118
10860
  if (action === "open-here") {
10119
10861
  await openPreviewDocumentHere(href, context);
10120
10862
  return;
@@ -10149,14 +10891,13 @@
10149
10891
  return;
10150
10892
  }
10151
10893
  if (kind === "image") {
10152
- const pendingWindow = window.open("", "_blank");
10153
- void openPreviewImageLink(href, title, null, pendingWindow).catch((error) => {
10894
+ void openPreviewImageLink(href, title).catch((error) => {
10154
10895
  setStatus((error && error.message) ? error.message : String(error || "Could not open linked image."), "warning");
10155
10896
  });
10156
10897
  return;
10157
10898
  }
10158
- if (kind === "text") {
10159
- const pendingWindow = window.open("", "_blank");
10899
+ if (kind === "text" || kind === "office") {
10900
+ const pendingWindow = kind === "office" ? null : window.open("", "_blank");
10160
10901
  void openPreviewDocumentInNewEditor(href, pendingWindow).catch((error) => {
10161
10902
  setStatus((error && error.message) ? error.message : String(error || "Could not open linked file."), "warning");
10162
10903
  });
@@ -11405,7 +12146,7 @@
11405
12146
  }
11406
12147
 
11407
12148
  function supportsCodePreviewCommentsForCurrentEditor() {
11408
- return Boolean(editorLanguage) && editorLanguage !== "markdown" && editorLanguage !== "latex";
12149
+ return Boolean(editorLanguage) && editorLanguage !== "markdown" && editorLanguage !== "latex" && !getDelimitedTextPreviewConfig(editorLanguage);
11409
12150
  }
11410
12151
 
11411
12152
  function getCodePreviewCommentKind(language) {
@@ -11606,6 +12347,26 @@
11606
12347
  return Boolean(shortcutsOverlayEl && !shortcutsOverlayEl.hidden);
11607
12348
  }
11608
12349
 
12350
+ function handleShortcutsScrollShortcut(event) {
12351
+ if (!isShortcutsOpen() || !shortcutsBodyEl || !event) return false;
12352
+ if (isTextEntryShortcutTarget(event.target)) return false;
12353
+ const key = typeof event.key === "string" ? event.key : "";
12354
+ let delta = 0;
12355
+ let targetTop = null;
12356
+ if (key === "ArrowDown") delta = 42;
12357
+ else if (key === "ArrowUp") delta = -42;
12358
+ else if (key === "PageDown") delta = Math.max(120, Math.round((shortcutsBodyEl.clientHeight || 0) * 0.85));
12359
+ else if (key === "PageUp") delta = -Math.max(120, Math.round((shortcutsBodyEl.clientHeight || 0) * 0.85));
12360
+ else if (key === "Home") targetTop = 0;
12361
+ else if (key === "End") targetTop = shortcutsBodyEl.scrollHeight;
12362
+ else return false;
12363
+ event.preventDefault();
12364
+ event.stopPropagation();
12365
+ if (targetTop !== null) shortcutsBodyEl.scrollTop = targetTop;
12366
+ else shortcutsBodyEl.scrollTop += delta;
12367
+ return true;
12368
+ }
12369
+
11609
12370
  function isScratchpadOpen() {
11610
12371
  return Boolean(scratchpadOverlayEl && !scratchpadOverlayEl.hidden);
11611
12372
  }
@@ -15841,7 +16602,8 @@
15841
16602
  ? window.requestAnimationFrame.bind(window)
15842
16603
  : (cb) => window.setTimeout(cb, 16);
15843
16604
  schedule(() => {
15844
- if (shortcutsCloseBtn && typeof shortcutsCloseBtn.focus === "function") shortcutsCloseBtn.focus();
16605
+ if (shortcutsBodyEl && typeof shortcutsBodyEl.focus === "function") shortcutsBodyEl.focus({ preventScroll: true });
16606
+ else if (shortcutsCloseBtn && typeof shortcutsCloseBtn.focus === "function") shortcutsCloseBtn.focus();
15845
16607
  });
15846
16608
  }
15847
16609
 
@@ -16039,6 +16801,9 @@
16039
16801
  if (editorView === "preview") {
16040
16802
  scheduleSourcePreviewRender(0);
16041
16803
  }
16804
+ if (rightView === "editor-preview") {
16805
+ scheduleResponseEditorPreviewRender(0);
16806
+ }
16042
16807
  updateOutlineUi();
16043
16808
  scheduleWorkspacePersistence();
16044
16809
  }