pi-studio 0.9.1 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -104,6 +104,7 @@
104
104
  const sendEditorBtn = document.getElementById("sendEditorBtn");
105
105
  const openCompanionBtn = document.getElementById("openCompanionBtn");
106
106
  const getEditorBtn = document.getElementById("getEditorBtn");
107
+ const zenModeBtn = document.getElementById("zenModeBtn");
107
108
  const loadGitDiffBtn = document.getElementById("loadGitDiffBtn");
108
109
  const sendRunBtn = document.getElementById("sendRunBtn");
109
110
  const queueSteerBtn = document.getElementById("queueSteerBtn");
@@ -180,6 +181,16 @@
180
181
  let statusLevel = "";
181
182
  let reconnectTimer = null;
182
183
  let reconnectAttempt = 0;
184
+ let studioPdfFocusOverlayEl = null;
185
+ let studioPdfFocusDialogEl = null;
186
+ let studioPdfFocusFrameSlotEl = null;
187
+ let studioPdfFocusFrameEl = null;
188
+ let studioPdfFocusTitleEl = null;
189
+ let studioPdfFocusOpenLinkEl = null;
190
+ let studioPdfFocusFullscreenBtn = null;
191
+ let studioPdfFocusCloseBtn = null;
192
+ let studioPdfFocusLastFocusedEl = null;
193
+ let studioPdfFocusMovedFrameState = null;
183
194
  let pendingRequestId = null;
184
195
  let pendingKind = null;
185
196
  let stickyStudioKind = null;
@@ -221,6 +232,8 @@
221
232
  const REPL_TRANSCRIPT_MAX_CHARS = 200_000;
222
233
  const REPL_JOURNAL_OUTPUT_MAX_CHARS = 80_000;
223
234
  const REPL_JOURNAL_MAX_ENTRIES = 80;
235
+ const PDF_EXPORT_FETCH_TIMEOUT_MS = 180_000;
236
+ const HTML_EXPORT_FETCH_TIMEOUT_MS = 180_000;
224
237
  const EDITOR_TAB_TEXT = " ";
225
238
  let replTmuxAvailable = null;
226
239
  let replSessions = [];
@@ -1666,7 +1679,7 @@
1666
1679
  let lineNumbersEnabled = false;
1667
1680
  let lineNumbersRenderRaf = null;
1668
1681
  let annotationsEnabled = true;
1669
- const STUDIO_UI_REFRESH_STORAGE_KEY = "piStudio.uiRefresh";
1682
+ const STUDIO_ZEN_MODE_STORAGE_KEY = "piStudio.zenMode";
1670
1683
  const studioUiRefreshEnabled = readStudioUiRefreshEnabled();
1671
1684
  const EDITOR_FONT_SIZE_OPTIONS = [10, 11, 12, 13, 14, 15, 16, 18];
1672
1685
  const RESPONSE_FONT_SIZE_OPTIONS = [11, 12, 12.5, 13, 13.5, 14, 14.5, 15, 15.5, 16, 18, 20];
@@ -1675,9 +1688,13 @@
1675
1688
  let editorFontSize = DEFAULT_EDITOR_FONT_SIZE;
1676
1689
  let responseFontSize = DEFAULT_RESPONSE_FONT_SIZE;
1677
1690
  let studioUiRefreshUi = null;
1691
+ let studioZenModeEnabled = readStudioZenModeEnabled();
1678
1692
  if (studioUiRefreshEnabled && document.body) {
1679
1693
  document.body.classList.add("studio-ui-refresh");
1680
1694
  }
1695
+ if (studioZenModeEnabled && document.body) {
1696
+ document.body.classList.add("studio-zen-mode");
1697
+ }
1681
1698
  let scratchpadText = "";
1682
1699
  let scratchpadReturnFocusEl = null;
1683
1700
  let scratchpadPersistTimer = null;
@@ -1704,19 +1721,51 @@
1704
1721
  : (initialQueryParams.has("studioUiRefresh") ? initialQueryParams.get("studioUiRefresh") : null);
1705
1722
  const isTruthy = (value) => ["1", "true", "yes", "on", "v2", "refresh", "fresh"].indexOf(normalize(value)) !== -1;
1706
1723
  const isFalsey = (value) => ["0", "false", "no", "off", "classic"].indexOf(normalize(value)) !== -1;
1724
+ if (queryValue !== null) {
1725
+ const normalizedQuery = normalize(queryValue);
1726
+ return isTruthy(queryValue) || (!isFalsey(queryValue) && normalizedQuery !== "");
1727
+ }
1728
+ return true;
1729
+ }
1730
+
1731
+ function readStudioZenModeEnabled() {
1732
+ const normalize = (value) => String(value == null ? "" : value).trim().toLowerCase();
1733
+ const isTruthy = (value) => ["1", "true", "yes", "on", "zen"].indexOf(normalize(value)) !== -1;
1734
+ const isFalsey = (value) => ["0", "false", "no", "off"].indexOf(normalize(value)) !== -1;
1735
+ const queryValue = initialQueryParams.has("zen") ? initialQueryParams.get("zen") : null;
1707
1736
  if (queryValue !== null) {
1708
1737
  const normalizedQuery = normalize(queryValue);
1709
1738
  const enabled = isTruthy(queryValue) || (!isFalsey(queryValue) && normalizedQuery !== "");
1710
1739
  try {
1711
- window.localStorage && window.localStorage.setItem(STUDIO_UI_REFRESH_STORAGE_KEY, enabled ? "1" : "0");
1740
+ window.localStorage && window.localStorage.setItem(STUDIO_ZEN_MODE_STORAGE_KEY, enabled ? "1" : "0");
1712
1741
  } catch {}
1713
1742
  return enabled;
1714
1743
  }
1715
1744
  try {
1716
- const stored = window.localStorage ? window.localStorage.getItem(STUDIO_UI_REFRESH_STORAGE_KEY) : null;
1717
- if (stored !== null) return stored !== "0" && !isFalsey(stored);
1745
+ const stored = window.localStorage ? window.localStorage.getItem(STUDIO_ZEN_MODE_STORAGE_KEY) : null;
1746
+ if (stored === null) return false;
1747
+ return isTruthy(stored) || (!isFalsey(stored) && normalize(stored) !== "");
1748
+ } catch {
1749
+ return false;
1750
+ }
1751
+ }
1752
+
1753
+ function syncStudioZenModeUi() {
1754
+ if (document.body) document.body.classList.toggle("studio-zen-mode", studioZenModeEnabled);
1755
+ if (!zenModeBtn) return;
1756
+ zenModeBtn.textContent = studioZenModeEnabled ? "Exit Zen" : "Zen";
1757
+ zenModeBtn.title = studioZenModeEnabled ? "Show full Studio controls." : "Hide secondary Studio controls.";
1758
+ zenModeBtn.setAttribute("aria-pressed", studioZenModeEnabled ? "true" : "false");
1759
+ }
1760
+
1761
+ function setStudioZenMode(enabled) {
1762
+ studioZenModeEnabled = Boolean(enabled);
1763
+ try {
1764
+ window.localStorage && window.localStorage.setItem(STUDIO_ZEN_MODE_STORAGE_KEY, studioZenModeEnabled ? "1" : "0");
1718
1765
  } catch {}
1719
- return true;
1766
+ closeStudioUiRefreshMenus();
1767
+ closeExportPreviewMenu();
1768
+ syncStudioZenModeUi();
1720
1769
  }
1721
1770
 
1722
1771
  function makeStudioUiRefreshElement(tagName, className, text) {
@@ -1735,9 +1784,16 @@
1735
1784
  svg.setAttribute("viewBox", "0 0 24 24");
1736
1785
  svg.setAttribute("aria-hidden", "true");
1737
1786
  svg.classList.add("studio-refresh-icon");
1738
- const paths = kind === "focus-exit"
1739
- ? ["M4 4l6 6", "M10 4v6H4", "M20 20l-6-6", "M14 20v-6h6"]
1740
- : ["M14 4h6v6", "M20 4l-6 6", "M10 20H4v-6", "M4 20l6-6"];
1787
+ let paths;
1788
+ if (kind === "focus-exit") {
1789
+ paths = ["M4 4l6 6", "M10 4v6H4", "M20 20l-6-6", "M14 20v-6h6"];
1790
+ } else if (kind === "fullscreen") {
1791
+ paths = ["M8 4H4v4", "M16 4h4v4", "M20 16v4h-4", "M4 16v4h4"];
1792
+ } else if (kind === "fullscreen-exit") {
1793
+ paths = ["M9 5v4H5", "M15 5v4h4", "M19 15h-4v4", "M5 15h4v4"];
1794
+ } else {
1795
+ paths = ["M14 4h6v6", "M20 4l-6 6", "M10 20H4v-6", "M4 20l6-6"];
1796
+ }
1741
1797
  for (const d of paths) {
1742
1798
  const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
1743
1799
  path.setAttribute("d", d);
@@ -1936,37 +1992,6 @@
1936
1992
  return { name, anchor: anchorEl, button: buttonEl, menu: menuEl };
1937
1993
  }
1938
1994
 
1939
- function setStudioUiRefreshPreference(enabled) {
1940
- try {
1941
- window.localStorage && window.localStorage.setItem(STUDIO_UI_REFRESH_STORAGE_KEY, enabled ? "1" : "0");
1942
- } catch {}
1943
- try {
1944
- const url = new URL(window.location.href);
1945
- url.searchParams.set("uiRefresh", enabled ? "1" : "0");
1946
- window.location.assign(url.toString());
1947
- } catch {
1948
- window.location.reload();
1949
- }
1950
- }
1951
-
1952
- function setupStudioUiRefreshToggleButton() {
1953
- if (!footerMetaEl || document.getElementById("studioUiRefreshToggleBtn")) return;
1954
- const button = makeStudioUiRefreshElement("button", "footer-compact-btn studio-ui-refresh-toggle", studioUiRefreshEnabled ? "UI: Fresh" : "UI: Classic");
1955
- button.id = "studioUiRefreshToggleBtn";
1956
- button.type = "button";
1957
- button.title = studioUiRefreshEnabled
1958
- ? "Switch Studio to the classic layout."
1959
- : "Switch Studio to the refreshed layout.";
1960
- button.addEventListener("click", () => {
1961
- setStudioUiRefreshPreference(!studioUiRefreshEnabled);
1962
- });
1963
- if (compactBtn && compactBtn.parentNode === footerMetaEl) {
1964
- compactBtn.insertAdjacentElement("afterend", button);
1965
- } else {
1966
- footerMetaEl.appendChild(button);
1967
- }
1968
- }
1969
-
1970
1995
  function setupStudioUiRefreshPrototype() {
1971
1996
  if (!studioUiRefreshEnabled || studioUiRefreshUi) return;
1972
1997
  const leftHeaderEl = document.getElementById("leftSectionHeader");
@@ -2096,8 +2121,8 @@
2096
2121
  syncStudioUiRefreshSummaries();
2097
2122
  }
2098
2123
 
2099
- setupStudioUiRefreshToggleButton();
2100
2124
  setupStudioUiRefreshPrototype();
2125
+ syncStudioZenModeUi();
2101
2126
  const annotationHelpers = globalThis.PiStudioAnnotationHelpers;
2102
2127
  if (!annotationHelpers || typeof annotationHelpers.collectInlineAnnotationMarkers !== "function") {
2103
2128
  throw new Error("Studio annotation helpers failed to load.");
@@ -2979,6 +3004,18 @@
2979
3004
  && typeof outlineDialogEl.contains === "function"
2980
3005
  && outlineDialogEl.contains(event.target)
2981
3006
  );
3007
+ const pdfFocusOwnsEvent = Boolean(
3008
+ studioPdfFocusDialogEl
3009
+ && event.target
3010
+ && typeof studioPdfFocusDialogEl.contains === "function"
3011
+ && studioPdfFocusDialogEl.contains(event.target)
3012
+ );
3013
+
3014
+ if (isStudioPdfFocusOpen() && plainEscape) {
3015
+ event.preventDefault();
3016
+ closeStudioPdfFocusViewer();
3017
+ return;
3018
+ }
2982
3019
 
2983
3020
  if (isScratchpadOpen() && plainEscape) {
2984
3021
  event.preventDefault();
@@ -2998,7 +3035,7 @@
2998
3035
  return;
2999
3036
  }
3000
3037
 
3001
- if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent) {
3038
+ if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || pdfFocusOwnsEvent) {
3002
3039
  return;
3003
3040
  }
3004
3041
 
@@ -3847,50 +3884,316 @@
3847
3884
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
3848
3885
  }
3849
3886
 
3850
- function buildStudioPdfResourceUrl(options) {
3887
+ function isStudioPdfFocusOpen() {
3888
+ return Boolean(studioPdfFocusOverlayEl && studioPdfFocusOverlayEl.hidden === false);
3889
+ }
3890
+
3891
+ function ensureStudioPdfFocusViewer() {
3892
+ if (studioPdfFocusOverlayEl) return studioPdfFocusOverlayEl;
3893
+
3894
+ const overlay = document.createElement("div");
3895
+ overlay.className = "studio-pdf-focus-overlay";
3896
+ overlay.hidden = true;
3897
+ overlay.setAttribute("role", "dialog");
3898
+ overlay.setAttribute("aria-modal", "true");
3899
+ overlay.setAttribute("aria-labelledby", "studioPdfFocusTitle");
3900
+
3901
+ const dialog = document.createElement("div");
3902
+ dialog.className = "studio-pdf-focus-dialog";
3903
+
3904
+ const header = document.createElement("div");
3905
+ header.className = "studio-pdf-focus-header";
3906
+
3907
+ const titleGroup = document.createElement("div");
3908
+ titleGroup.className = "studio-pdf-focus-title-group";
3909
+
3910
+ const closeBtn = document.createElement("button");
3911
+ closeBtn.type = "button";
3912
+ closeBtn.className = "studio-pdf-focus-btn studio-pdf-focus-close";
3913
+ closeBtn.title = "Exit PDF focus view.";
3914
+ closeBtn.setAttribute("aria-label", "Exit PDF focus view");
3915
+ closeBtn.appendChild(makeStudioUiRefreshIcon("focus-exit"));
3916
+ closeBtn.addEventListener("click", () => closeStudioPdfFocusViewer());
3917
+ titleGroup.appendChild(closeBtn);
3918
+
3919
+ const titleEl = document.createElement("div");
3920
+ titleEl.id = "studioPdfFocusTitle";
3921
+ titleEl.className = "studio-pdf-focus-title";
3922
+ titleEl.textContent = "PDF preview";
3923
+ titleGroup.appendChild(titleEl);
3924
+ header.appendChild(titleGroup);
3925
+
3926
+ const actions = document.createElement("div");
3927
+ actions.className = "studio-pdf-focus-actions";
3928
+
3929
+ const openLink = document.createElement("a");
3930
+ openLink.className = "studio-pdf-focus-link";
3931
+ openLink.target = "_blank";
3932
+ openLink.rel = "noopener noreferrer";
3933
+ openLink.textContent = "Open PDF";
3934
+ actions.appendChild(openLink);
3935
+
3936
+ const fullscreenBtn = document.createElement("button");
3937
+ fullscreenBtn.type = "button";
3938
+ fullscreenBtn.className = "studio-pdf-focus-btn studio-pdf-focus-fullscreen";
3939
+ fullscreenBtn.addEventListener("click", async () => {
3940
+ const isFullscreen = Boolean(document.fullscreenElement && studioPdfFocusDialogEl && document.fullscreenElement === studioPdfFocusDialogEl);
3941
+ if (isFullscreen) {
3942
+ try {
3943
+ if (typeof document.exitFullscreen === "function") await document.exitFullscreen();
3944
+ } catch (error) {
3945
+ setStatus("Could not exit PDF fullscreen: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
3946
+ } finally {
3947
+ syncStudioPdfFocusFullscreenButton();
3948
+ }
3949
+ return;
3950
+ }
3951
+ if (!studioPdfFocusDialogEl || typeof studioPdfFocusDialogEl.requestFullscreen !== "function") {
3952
+ setStatus("Browser fullscreen is not available for this PDF viewer.", "warning");
3953
+ return;
3954
+ }
3955
+ try {
3956
+ await studioPdfFocusDialogEl.requestFullscreen();
3957
+ } catch (error) {
3958
+ setStatus("Could not enter PDF fullscreen: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
3959
+ } finally {
3960
+ syncStudioPdfFocusFullscreenButton();
3961
+ }
3962
+ });
3963
+ actions.appendChild(fullscreenBtn);
3964
+
3965
+ header.appendChild(actions);
3966
+ dialog.appendChild(header);
3967
+
3968
+ const frameSlot = document.createElement("div");
3969
+ frameSlot.className = "studio-pdf-focus-frame-slot";
3970
+ const frame = document.createElement("iframe");
3971
+ frame.className = "studio-pdf-focus-frame";
3972
+ frame.title = "PDF focus viewer";
3973
+ frame.loading = "eager";
3974
+ frameSlot.appendChild(frame);
3975
+ dialog.appendChild(frameSlot);
3976
+
3977
+ overlay.appendChild(dialog);
3978
+ overlay.addEventListener("click", (event) => {
3979
+ if (event.target === overlay) closeStudioPdfFocusViewer();
3980
+ });
3981
+ document.addEventListener("fullscreenchange", syncStudioPdfFocusFullscreenButton);
3982
+
3983
+ document.body.appendChild(overlay);
3984
+ studioPdfFocusOverlayEl = overlay;
3985
+ studioPdfFocusDialogEl = dialog;
3986
+ studioPdfFocusFrameSlotEl = frameSlot;
3987
+ studioPdfFocusFrameEl = frame;
3988
+ studioPdfFocusTitleEl = titleEl;
3989
+ studioPdfFocusOpenLinkEl = openLink;
3990
+ studioPdfFocusFullscreenBtn = fullscreenBtn;
3991
+ studioPdfFocusCloseBtn = closeBtn;
3992
+ syncStudioPdfFocusFullscreenButton();
3993
+ return overlay;
3994
+ }
3995
+
3996
+ function openStudioPdfFocusViewer(viewerUrl, title, sourceFrame) {
3997
+ const src = String(viewerUrl || "").trim();
3998
+ if (!src) return;
3999
+ ensureStudioPdfFocusViewer();
4000
+ studioPdfFocusLastFocusedEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
4001
+ if (studioPdfFocusTitleEl) studioPdfFocusTitleEl.textContent = String(title || "PDF preview").trim() || "PDF preview";
4002
+ if (studioPdfFocusOpenLinkEl) studioPdfFocusOpenLinkEl.href = src;
4003
+ setStudioPdfFocusFrameSource(src, title, sourceFrame);
4004
+ if (document.body) document.body.classList.add("studio-pdf-focus-open");
4005
+ if (studioPdfFocusOverlayEl) studioPdfFocusOverlayEl.hidden = false;
4006
+ syncStudioPdfFocusFullscreenButton();
4007
+ closeStudioUiRefreshMenus();
4008
+ closeExportPreviewMenu();
4009
+ window.setTimeout(() => {
4010
+ if (studioPdfFocusCloseBtn && typeof studioPdfFocusCloseBtn.focus === "function") {
4011
+ studioPdfFocusCloseBtn.focus();
4012
+ }
4013
+ }, 0);
4014
+ }
4015
+
4016
+ function closeStudioPdfFocusViewer() {
4017
+ if (!isStudioPdfFocusOpen()) return false;
4018
+ if (document.fullscreenElement && studioPdfFocusDialogEl && studioPdfFocusDialogEl.contains(document.fullscreenElement)) {
4019
+ try {
4020
+ const exitResult = document.exitFullscreen && document.exitFullscreen();
4021
+ if (exitResult && typeof exitResult.catch === "function") exitResult.catch(() => {});
4022
+ } catch {}
4023
+ }
4024
+ if (studioPdfFocusOverlayEl) studioPdfFocusOverlayEl.hidden = true;
4025
+ restoreStudioPdfFocusMovedFrame();
4026
+ if (studioPdfFocusFrameEl) studioPdfFocusFrameEl.src = "about:blank";
4027
+ if (document.body) document.body.classList.remove("studio-pdf-focus-open");
4028
+ syncStudioPdfFocusFullscreenButton();
4029
+ const focusTarget = studioPdfFocusLastFocusedEl;
4030
+ studioPdfFocusLastFocusedEl = null;
4031
+ if (focusTarget && typeof focusTarget.focus === "function" && document.contains(focusTarget)) {
4032
+ window.setTimeout(() => focusTarget.focus(), 0);
4033
+ }
4034
+ return true;
4035
+ }
4036
+
4037
+ function buildStudioPdfResourceUrl(options, useEditorResourceContext) {
3851
4038
  const token = getToken();
3852
4039
  if (!token) return "";
3853
4040
  const pdfPath = String(options && options.path ? options.path : "").trim();
3854
4041
  if (!pdfPath) return "";
3855
4042
  const effectivePath = getEffectiveSavePath();
3856
- const sourcePath = effectivePath || sourceState.path || "";
4043
+ const sourcePath = useEditorResourceContext ? (effectivePath || sourceState.path || "") : "";
4044
+ const resourceDir = resourceDirInput && resourceDirInput.value.trim() ? resourceDirInput.value.trim() : "";
3857
4045
  const params = new URLSearchParams({ token, path: pdfPath });
3858
4046
  if (sourcePath) {
3859
4047
  params.set("sourcePath", sourcePath);
3860
- } else if (resourceDirInput && resourceDirInput.value.trim()) {
3861
- params.set("resourceDir", resourceDirInput.value.trim());
4048
+ } else if (resourceDir) {
4049
+ params.set("resourceDir", resourceDir);
3862
4050
  }
3863
4051
  return "/pdf-resource?" + params.toString();
3864
4052
  }
3865
4053
 
3866
- function createStudioPdfCard(block) {
4054
+ function syncStudioPdfFocusFullscreenButton() {
4055
+ if (!studioPdfFocusFullscreenBtn) return;
4056
+ const isFullscreen = Boolean(document.fullscreenElement && studioPdfFocusDialogEl && document.fullscreenElement === studioPdfFocusDialogEl);
4057
+ studioPdfFocusFullscreenBtn.replaceChildren(makeStudioUiRefreshIcon(isFullscreen ? "fullscreen-exit" : "fullscreen"));
4058
+ const label = isFullscreen ? "Exit fullscreen" : "Fullscreen";
4059
+ studioPdfFocusFullscreenBtn.title = isFullscreen
4060
+ ? "Exit browser fullscreen and keep the PDF focus viewer open."
4061
+ : "Ask the browser to make this PDF viewer fullscreen.";
4062
+ studioPdfFocusFullscreenBtn.setAttribute("aria-label", label);
4063
+ studioPdfFocusFullscreenBtn.setAttribute("aria-pressed", isFullscreen ? "true" : "false");
4064
+ }
4065
+
4066
+ function restoreStudioPdfFocusMovedFrame() {
4067
+ const state = studioPdfFocusMovedFrameState;
4068
+ studioPdfFocusMovedFrameState = null;
4069
+ if (!state || !state.frame) return;
4070
+ const frame = state.frame;
4071
+ frame.className = state.className;
4072
+ frame.style.cssText = state.styleCssText;
4073
+ if (state.title !== null) frame.setAttribute("title", state.title);
4074
+ else frame.removeAttribute("title");
4075
+ if (state.placeholder && state.placeholder.parentNode) {
4076
+ state.placeholder.parentNode.insertBefore(frame, state.placeholder);
4077
+ state.placeholder.remove();
4078
+ } else if (state.parent && state.parent.isConnected) {
4079
+ state.parent.insertBefore(frame, state.nextSibling && state.nextSibling.parentNode === state.parent ? state.nextSibling : null);
4080
+ }
4081
+ }
4082
+
4083
+ function setStudioPdfFocusFrameSource(src, title, sourceFrame) {
4084
+ if (!studioPdfFocusFrameSlotEl || !studioPdfFocusFrameEl) return;
4085
+ restoreStudioPdfFocusMovedFrame();
4086
+ const sourceIframe = sourceFrame instanceof HTMLIFrameElement ? sourceFrame : null;
4087
+ if (sourceIframe && sourceIframe.isConnected) {
4088
+ const placeholder = document.createElement("span");
4089
+ placeholder.hidden = true;
4090
+ const parent = sourceIframe.parentNode;
4091
+ parent && parent.insertBefore(placeholder, sourceIframe);
4092
+ studioPdfFocusMovedFrameState = {
4093
+ frame: sourceIframe,
4094
+ parent,
4095
+ nextSibling: placeholder.nextSibling,
4096
+ placeholder,
4097
+ className: sourceIframe.className,
4098
+ styleCssText: sourceIframe.style.cssText,
4099
+ title: sourceIframe.getAttribute("title"),
4100
+ };
4101
+ if (studioPdfFocusFrameEl.parentNode) studioPdfFocusFrameEl.parentNode.removeChild(studioPdfFocusFrameEl);
4102
+ sourceIframe.classList.add("studio-pdf-focus-frame");
4103
+ sourceIframe.style.height = "auto";
4104
+ sourceIframe.style.flex = "1 1 auto";
4105
+ sourceIframe.title = String(title || "PDF focus viewer").trim() || "PDF focus viewer";
4106
+ studioPdfFocusFrameSlotEl.appendChild(sourceIframe);
4107
+ return;
4108
+ }
4109
+ if (!studioPdfFocusFrameEl.parentNode) studioPdfFocusFrameSlotEl.appendChild(studioPdfFocusFrameEl);
4110
+ studioPdfFocusFrameEl.src = src;
4111
+ studioPdfFocusFrameEl.title = String(title || "PDF focus viewer").trim() || "PDF focus viewer";
4112
+ }
4113
+
4114
+ function openStudioPdfFocusFromButton(buttonEl) {
4115
+ if (!buttonEl) return false;
4116
+ const card = buttonEl.closest && buttonEl.closest(".studio-pdf-card");
4117
+ const viewerUrl = String(buttonEl.dataset && buttonEl.dataset.studioPdfViewerUrl ? buttonEl.dataset.studioPdfViewerUrl : "").trim()
4118
+ || String(card && card.dataset ? (card.dataset.studioPdfViewerUrl || "") : "").trim();
4119
+ const title = String(buttonEl.dataset && buttonEl.dataset.studioPdfTitle ? buttonEl.dataset.studioPdfTitle : "").trim()
4120
+ || String(card && card.dataset ? (card.dataset.studioPdfTitle || "") : "").trim()
4121
+ || "PDF preview";
4122
+ const sourceFrame = card && typeof card.querySelector === "function" ? card.querySelector("iframe.studio-pdf-frame") : null;
4123
+ if (!viewerUrl) return false;
4124
+ openStudioPdfFocusViewer(viewerUrl, title, sourceFrame);
4125
+ return true;
4126
+ }
4127
+
4128
+ function handleStudioPdfFocusButtonClick(event) {
4129
+ const target = event && event.target;
4130
+ const buttonEl = target instanceof Element ? target.closest(".studio-pdf-card-focus") : null;
4131
+ if (!buttonEl) return;
4132
+ event.preventDefault();
4133
+ event.stopPropagation();
4134
+ if (typeof event.stopImmediatePropagation === "function") {
4135
+ event.stopImmediatePropagation();
4136
+ }
4137
+ if (!openStudioPdfFocusFromButton(buttonEl)) {
4138
+ setStatus("Could not open PDF focus view for this card.", "warning");
4139
+ }
4140
+ }
4141
+
4142
+ function createStudioPdfCard(block, useEditorResourceContext) {
3867
4143
  const options = block && block.options ? block.options : {};
3868
4144
  const path = String(options.path || "").trim();
3869
4145
  const title = String(options.title || path || "Embedded PDF").trim();
3870
4146
  const caption = String(options.caption || "").trim();
3871
4147
  const height = normalizeStudioPdfHeight(options.height);
3872
4148
  const page = normalizeStudioPdfPage(options.page);
3873
- const resourceUrl = buildStudioPdfResourceUrl(options);
4149
+ const resourceUrl = buildStudioPdfResourceUrl(options, useEditorResourceContext);
3874
4150
  const viewerUrl = resourceUrl && page ? resourceUrl + "#page=" + encodeURIComponent(String(page)) : resourceUrl;
3875
4151
 
3876
4152
  const card = document.createElement("figure");
3877
4153
  card.className = "studio-pdf-card";
4154
+ if (card.dataset) {
4155
+ card.dataset.studioPdfViewerUrl = viewerUrl || "";
4156
+ card.dataset.studioPdfTitle = title;
4157
+ }
3878
4158
 
3879
4159
  const header = document.createElement("figcaption");
3880
4160
  header.className = "studio-pdf-card-header";
4161
+
4162
+ const titleGroup = document.createElement("div");
4163
+ titleGroup.className = "studio-pdf-card-title-group";
4164
+ if (resourceUrl) {
4165
+ const focusBtn = document.createElement("button");
4166
+ focusBtn.type = "button";
4167
+ focusBtn.className = "studio-pdf-card-action studio-pdf-card-focus";
4168
+ focusBtn.title = "Open this PDF in a larger Studio overlay.";
4169
+ focusBtn.setAttribute("aria-label", "Focus PDF");
4170
+ if (focusBtn.dataset) {
4171
+ focusBtn.dataset.studioPdfViewerUrl = viewerUrl;
4172
+ focusBtn.dataset.studioPdfTitle = title;
4173
+ }
4174
+ focusBtn.appendChild(makeStudioUiRefreshIcon("focus"));
4175
+ focusBtn.addEventListener("click", handleStudioPdfFocusButtonClick);
4176
+ titleGroup.appendChild(focusBtn);
4177
+ }
3881
4178
  const label = document.createElement("div");
3882
4179
  label.className = "studio-pdf-card-title";
3883
4180
  label.textContent = title;
3884
- header.appendChild(label);
4181
+ titleGroup.appendChild(label);
4182
+ header.appendChild(titleGroup);
3885
4183
 
3886
4184
  if (resourceUrl) {
4185
+ const actions = document.createElement("div");
4186
+ actions.className = "studio-pdf-card-actions";
4187
+
3887
4188
  const openLink = document.createElement("a");
3888
- openLink.className = "studio-pdf-card-link";
4189
+ openLink.className = "studio-pdf-card-link studio-pdf-card-action";
3889
4190
  openLink.href = viewerUrl;
3890
4191
  openLink.target = "_blank";
3891
4192
  openLink.rel = "noopener noreferrer";
3892
4193
  openLink.textContent = "Open PDF";
3893
- header.appendChild(openLink);
4194
+ actions.appendChild(openLink);
4195
+
4196
+ header.appendChild(actions);
3894
4197
  }
3895
4198
  card.appendChild(header);
3896
4199
 
@@ -3919,7 +4222,7 @@
3919
4222
  return card;
3920
4223
  }
3921
4224
 
3922
- function renderStudioPdfBlocksInElement(targetEl, blocks) {
4225
+ function renderStudioPdfBlocksInElement(targetEl, blocks, useEditorResourceContext) {
3923
4226
  if (!targetEl || !Array.isArray(blocks) || blocks.length === 0) return;
3924
4227
  const candidates = Array.from(targetEl.querySelectorAll("p, pre, div"));
3925
4228
  blocks.forEach((block) => {
@@ -3927,7 +4230,7 @@
3927
4230
  if (!placeholder) return;
3928
4231
  const match = candidates.find((el) => String(el.textContent || "").trim() === placeholder);
3929
4232
  if (match && match.parentNode) {
3930
- match.replaceWith(createStudioPdfCard(block));
4233
+ match.replaceWith(createStudioPdfCard(block, useEditorResourceContext));
3931
4234
  }
3932
4235
  });
3933
4236
  }
@@ -4952,6 +5255,22 @@
4952
5255
  return "";
4953
5256
  }
4954
5257
 
5258
+ async function fetchWithTimeout(url, options, timeoutMs, timeoutLabel) {
5259
+ if (typeof AbortController === "undefined") return fetch(url, options);
5260
+ const controller = new AbortController();
5261
+ const timer = window.setTimeout(() => controller.abort(), Math.max(1000, Number(timeoutMs) || PDF_EXPORT_FETCH_TIMEOUT_MS));
5262
+ try {
5263
+ return await fetch(url, { ...(options || {}), signal: controller.signal });
5264
+ } catch (error) {
5265
+ if (error && error.name === "AbortError") {
5266
+ throw new Error((timeoutLabel || "Request") + " timed out. Try a smaller export or check the PDF toolchain.");
5267
+ }
5268
+ throw error;
5269
+ } finally {
5270
+ window.clearTimeout(timer);
5271
+ }
5272
+ }
5273
+
4955
5274
  async function exportRightPanePdf() {
4956
5275
  if (uiBusy || previewExportInProgress) {
4957
5276
  setStatus("Studio is busy.", "warning");
@@ -5011,7 +5330,7 @@
5011
5330
  setStatus("Exporting PDF…", "warning");
5012
5331
 
5013
5332
  try {
5014
- const response = await fetch("/export-pdf?token=" + encodeURIComponent(token), {
5333
+ const response = await fetchWithTimeout("/export-pdf?token=" + encodeURIComponent(token), {
5015
5334
  method: "POST",
5016
5335
  headers: {
5017
5336
  "Content-Type": "application/json",
@@ -5024,7 +5343,7 @@
5024
5343
  editorPdfLanguage: editorPdfLanguage,
5025
5344
  filenameHint: filenameHint,
5026
5345
  }),
5027
- });
5346
+ }, PDF_EXPORT_FETCH_TIMEOUT_MS, "PDF export");
5028
5347
 
5029
5348
  const contentType = String(response.headers.get("content-type") || "").toLowerCase();
5030
5349
  if (!response.ok) {
@@ -5178,7 +5497,7 @@
5178
5497
  setStatus("Exporting HTML…", "warning");
5179
5498
 
5180
5499
  try {
5181
- const response = await fetch("/export-html?token=" + encodeURIComponent(token), {
5500
+ const response = await fetchWithTimeout("/export-html?token=" + encodeURIComponent(token), {
5182
5501
  method: "POST",
5183
5502
  headers: {
5184
5503
  "Content-Type": "application/json",
@@ -5192,7 +5511,7 @@
5192
5511
  filenameHint: filenameHint,
5193
5512
  title: titleHint,
5194
5513
  }),
5195
- });
5514
+ }, HTML_EXPORT_FETCH_TIMEOUT_MS, "HTML export");
5196
5515
 
5197
5516
  const contentType = String(response.headers.get("content-type") || "").toLowerCase();
5198
5517
  if (!response.ok) {
@@ -5521,7 +5840,7 @@
5521
5840
  clearPreviewJumpHighlight(targetEl);
5522
5841
  finishPreviewRender(targetEl);
5523
5842
  targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown, previewFallbackOptions);
5524
- renderStudioPdfBlocksInElement(targetEl, pdfPrepared.blocks);
5843
+ renderStudioPdfBlocksInElement(targetEl, pdfPrepared.blocks, previewingEditorText);
5525
5844
  applyPreviewAnnotationPlaceholdersToElement(targetEl, previewPrepared.placeholders);
5526
5845
  await renderAnnotationMathInElement(targetEl);
5527
5846
  decoratePdfEmbeds(targetEl);
@@ -13876,6 +14195,12 @@
13876
14195
  });
13877
14196
  }
13878
14197
 
14198
+ if (zenModeBtn) {
14199
+ zenModeBtn.addEventListener("click", () => {
14200
+ setStudioZenMode(!studioZenModeEnabled);
14201
+ });
14202
+ }
14203
+
13879
14204
  sendRunBtn.addEventListener("click", () => {
13880
14205
  if (getAbortablePendingKind() === "direct") {
13881
14206
  requestCancelForPendingRequest("direct");
@@ -14057,6 +14382,13 @@
14057
14382
  });
14058
14383
  }
14059
14384
 
14385
+ document.addEventListener("click", (event) => {
14386
+ const target = event.target;
14387
+ const focusBtn = target instanceof Element ? target.closest(".studio-pdf-card-focus") : null;
14388
+ if (!focusBtn) return;
14389
+ handleStudioPdfFocusButtonClick(event);
14390
+ }, true);
14391
+
14060
14392
  document.addEventListener("click", (event) => {
14061
14393
  const target = event.target;
14062
14394
  const copyBtn = target instanceof Element ? target.closest(".studio-copy-block-btn") : null;