pi-studio 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +3 -1
- package/client/studio-client.js +393 -20
- package/client/studio.css +286 -2
- package/index.ts +288 -323
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.2] — 2026-05-16
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Added a global **⊙ Zen** / **Exit Zen** toggle that hides secondary Studio chrome while preserving the current pane layout and panel focus state.
|
|
11
|
+
- Added a Focus action for explicit `studio-pdf` preview cards, opening the embedded PDF in a larger Studio overlay with optional browser fullscreen.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- HTML export no longer lets Pandoc's standalone template/CSS leak into the exported document for local-resource previews, fixing narrow/mobile-like exports for larger documents with embedded assets.
|
|
15
|
+
- HTML/PDF export subprocess handling now uses bounded output capture and explicit timeout paths without truncating successful embedded-asset HTML renders.
|
|
16
|
+
- PDF Focus now works from response previews before the same card has been rendered in editor preview, and its controls use theme-consistent focus/fullscreen icons.
|
|
17
|
+
|
|
7
18
|
## [0.9.1] — 2026-05-15
|
|
8
19
|
|
|
9
20
|
### Added
|
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
|
|
|
20
20
|
|
|
21
21
|
- Opens a two-pane browser workspace: **Editor** (left) + **Response/Working/Editor Preview** (right)
|
|
22
22
|
- Supports one canonical full Studio view per Pi session, plus additional editor-only companion views when you just want extra editing/preview surfaces; the editor toolbar can open a detached copy of the current editor text as a companion view
|
|
23
|
+
- Includes a global **⊙ Zen** mode for hiding secondary Studio chrome without changing the current left/right pane layout
|
|
23
24
|
- Runs editor text directly, or asks for structured critique (auto/writing/code focus)
|
|
24
25
|
- Includes a live **Working** view for following current model/tool activity, with `All` / `Thinking` / `Tools` filters, image previews for image-producing tool outputs, plus **Load visible into editor** and **Copy visible** actions; when cycling response history, Working follows saved working details for the selected response when available
|
|
25
26
|
- Includes an optional tmux-backed **REPL** view for Shell, Python, IPython, Julia, R, GHCi, and Clojure sessions, with Raw/Literate send modes, `Cmd/Ctrl+Shift+Enter` **Send to REPL**, session start/new/stop/interrupt controls, a compact refresh-persistent **REPL Studio** record of user and Pi-sent code, a secondary raw tmux mirror, agent-facing `studio_repl_status` / `studio_repl_send` tools, and Markdown/PDF/HTML export
|
|
@@ -37,7 +38,7 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
|
|
|
37
38
|
- saves `.annotated.md`
|
|
38
39
|
- Renders Markdown/LaTeX/code previews (math + Mermaid), theme-synced with pi, with copy buttons for code blocks and blockquotes
|
|
39
40
|
- Renders straight, unfenced interactive HTML in preview via a sandboxed browser iframe with zoom controls, while fenced `html` blocks remain source code
|
|
40
|
-
- Embeds local PDFs in Studio Markdown previews via explicit `studio-pdf` fenced blocks
|
|
41
|
+
- Embeds local PDFs in Studio Markdown previews via explicit `studio-pdf` fenced blocks, with a Focus action for temporarily enlarging the embedded viewer
|
|
41
42
|
- Ships optional `pi-studio-dark` and `pi-studio-light` themes tuned for Studio's browser workspace
|
|
42
43
|
- Exports right-pane preview as PDF (pandoc + LaTeX) or standalone HTML, preserving authored HTML previews as HTML
|
|
43
44
|
- Exports local files headlessly via `/studio-pdf <path>` to `<name>.studio.pdf` or `/studio-html <path>` to `<name>.studio.html`; without a path, those commands export the last model response to a timestamped file
|
|
@@ -110,6 +111,7 @@ caption: Optional caption
|
|
|
110
111
|
- Full preview/PDF quality depends on `pandoc` (and `xelatex` for PDF):
|
|
111
112
|
- `brew install pandoc`
|
|
112
113
|
- install TeX Live/MacTeX for PDF export
|
|
114
|
+
- Export subprocess timeouts default to bounded values and can be tuned with `PI_STUDIO_PANDOC_TIMEOUT_MS`, `PI_STUDIO_LATEX_TIMEOUT_MS`, `PI_STUDIO_MERMAID_TIMEOUT_MS`, and `PI_STUDIO_HTML_RENDER_OUTPUT_MAX_BYTES` for unusually large embedded-asset HTML exports.
|
|
113
115
|
- Mermaid diagrams in exported PDFs may also require Mermaid CLI (`mmdc` / `@mermaid-js/mermaid-cli`) when you want diagram blocks rendered as diagrams rather than left as code.
|
|
114
116
|
|
|
115
117
|
## License
|
package/client/studio-client.js
CHANGED
|
@@ -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 = [];
|
|
@@ -1667,6 +1680,7 @@
|
|
|
1667
1680
|
let lineNumbersRenderRaf = null;
|
|
1668
1681
|
let annotationsEnabled = true;
|
|
1669
1682
|
const STUDIO_UI_REFRESH_STORAGE_KEY = "piStudio.uiRefresh";
|
|
1683
|
+
const STUDIO_ZEN_MODE_STORAGE_KEY = "piStudio.zenMode";
|
|
1670
1684
|
const studioUiRefreshEnabled = readStudioUiRefreshEnabled();
|
|
1671
1685
|
const EDITOR_FONT_SIZE_OPTIONS = [10, 11, 12, 13, 14, 15, 16, 18];
|
|
1672
1686
|
const RESPONSE_FONT_SIZE_OPTIONS = [11, 12, 12.5, 13, 13.5, 14, 14.5, 15, 15.5, 16, 18, 20];
|
|
@@ -1675,9 +1689,13 @@
|
|
|
1675
1689
|
let editorFontSize = DEFAULT_EDITOR_FONT_SIZE;
|
|
1676
1690
|
let responseFontSize = DEFAULT_RESPONSE_FONT_SIZE;
|
|
1677
1691
|
let studioUiRefreshUi = null;
|
|
1692
|
+
let studioZenModeEnabled = readStudioZenModeEnabled();
|
|
1678
1693
|
if (studioUiRefreshEnabled && document.body) {
|
|
1679
1694
|
document.body.classList.add("studio-ui-refresh");
|
|
1680
1695
|
}
|
|
1696
|
+
if (studioZenModeEnabled && document.body) {
|
|
1697
|
+
document.body.classList.add("studio-zen-mode");
|
|
1698
|
+
}
|
|
1681
1699
|
let scratchpadText = "";
|
|
1682
1700
|
let scratchpadReturnFocusEl = null;
|
|
1683
1701
|
let scratchpadPersistTimer = null;
|
|
@@ -1719,6 +1737,46 @@
|
|
|
1719
1737
|
return true;
|
|
1720
1738
|
}
|
|
1721
1739
|
|
|
1740
|
+
function readStudioZenModeEnabled() {
|
|
1741
|
+
const normalize = (value) => String(value == null ? "" : value).trim().toLowerCase();
|
|
1742
|
+
const isTruthy = (value) => ["1", "true", "yes", "on", "zen"].indexOf(normalize(value)) !== -1;
|
|
1743
|
+
const isFalsey = (value) => ["0", "false", "no", "off"].indexOf(normalize(value)) !== -1;
|
|
1744
|
+
const queryValue = initialQueryParams.has("zen") ? initialQueryParams.get("zen") : null;
|
|
1745
|
+
if (queryValue !== null) {
|
|
1746
|
+
const normalizedQuery = normalize(queryValue);
|
|
1747
|
+
const enabled = isTruthy(queryValue) || (!isFalsey(queryValue) && normalizedQuery !== "");
|
|
1748
|
+
try {
|
|
1749
|
+
window.localStorage && window.localStorage.setItem(STUDIO_ZEN_MODE_STORAGE_KEY, enabled ? "1" : "0");
|
|
1750
|
+
} catch {}
|
|
1751
|
+
return enabled;
|
|
1752
|
+
}
|
|
1753
|
+
try {
|
|
1754
|
+
const stored = window.localStorage ? window.localStorage.getItem(STUDIO_ZEN_MODE_STORAGE_KEY) : null;
|
|
1755
|
+
if (stored === null) return false;
|
|
1756
|
+
return isTruthy(stored) || (!isFalsey(stored) && normalize(stored) !== "");
|
|
1757
|
+
} catch {
|
|
1758
|
+
return false;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
function syncStudioZenModeUi() {
|
|
1763
|
+
if (document.body) document.body.classList.toggle("studio-zen-mode", studioZenModeEnabled);
|
|
1764
|
+
if (!zenModeBtn) return;
|
|
1765
|
+
zenModeBtn.textContent = studioZenModeEnabled ? "Exit Zen" : "⊙ Zen";
|
|
1766
|
+
zenModeBtn.title = studioZenModeEnabled ? "Show full Studio controls." : "Hide secondary Studio controls.";
|
|
1767
|
+
zenModeBtn.setAttribute("aria-pressed", studioZenModeEnabled ? "true" : "false");
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function setStudioZenMode(enabled) {
|
|
1771
|
+
studioZenModeEnabled = Boolean(enabled);
|
|
1772
|
+
try {
|
|
1773
|
+
window.localStorage && window.localStorage.setItem(STUDIO_ZEN_MODE_STORAGE_KEY, studioZenModeEnabled ? "1" : "0");
|
|
1774
|
+
} catch {}
|
|
1775
|
+
closeStudioUiRefreshMenus();
|
|
1776
|
+
closeExportPreviewMenu();
|
|
1777
|
+
syncStudioZenModeUi();
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1722
1780
|
function makeStudioUiRefreshElement(tagName, className, text) {
|
|
1723
1781
|
const element = document.createElement(tagName);
|
|
1724
1782
|
if (className) element.className = className;
|
|
@@ -1735,9 +1793,16 @@
|
|
|
1735
1793
|
svg.setAttribute("viewBox", "0 0 24 24");
|
|
1736
1794
|
svg.setAttribute("aria-hidden", "true");
|
|
1737
1795
|
svg.classList.add("studio-refresh-icon");
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1796
|
+
let paths;
|
|
1797
|
+
if (kind === "focus-exit") {
|
|
1798
|
+
paths = ["M4 4l6 6", "M10 4v6H4", "M20 20l-6-6", "M14 20v-6h6"];
|
|
1799
|
+
} else if (kind === "fullscreen") {
|
|
1800
|
+
paths = ["M8 4H4v4", "M16 4h4v4", "M20 16v4h-4", "M4 16v4h4"];
|
|
1801
|
+
} else if (kind === "fullscreen-exit") {
|
|
1802
|
+
paths = ["M9 5v4H5", "M15 5v4h4", "M19 15h-4v4", "M5 15h4v4"];
|
|
1803
|
+
} else {
|
|
1804
|
+
paths = ["M14 4h6v6", "M20 4l-6 6", "M10 20H4v-6", "M4 20l6-6"];
|
|
1805
|
+
}
|
|
1741
1806
|
for (const d of paths) {
|
|
1742
1807
|
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1743
1808
|
path.setAttribute("d", d);
|
|
@@ -2098,6 +2163,7 @@
|
|
|
2098
2163
|
|
|
2099
2164
|
setupStudioUiRefreshToggleButton();
|
|
2100
2165
|
setupStudioUiRefreshPrototype();
|
|
2166
|
+
syncStudioZenModeUi();
|
|
2101
2167
|
const annotationHelpers = globalThis.PiStudioAnnotationHelpers;
|
|
2102
2168
|
if (!annotationHelpers || typeof annotationHelpers.collectInlineAnnotationMarkers !== "function") {
|
|
2103
2169
|
throw new Error("Studio annotation helpers failed to load.");
|
|
@@ -2979,6 +3045,18 @@
|
|
|
2979
3045
|
&& typeof outlineDialogEl.contains === "function"
|
|
2980
3046
|
&& outlineDialogEl.contains(event.target)
|
|
2981
3047
|
);
|
|
3048
|
+
const pdfFocusOwnsEvent = Boolean(
|
|
3049
|
+
studioPdfFocusDialogEl
|
|
3050
|
+
&& event.target
|
|
3051
|
+
&& typeof studioPdfFocusDialogEl.contains === "function"
|
|
3052
|
+
&& studioPdfFocusDialogEl.contains(event.target)
|
|
3053
|
+
);
|
|
3054
|
+
|
|
3055
|
+
if (isStudioPdfFocusOpen() && plainEscape) {
|
|
3056
|
+
event.preventDefault();
|
|
3057
|
+
closeStudioPdfFocusViewer();
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
2982
3060
|
|
|
2983
3061
|
if (isScratchpadOpen() && plainEscape) {
|
|
2984
3062
|
event.preventDefault();
|
|
@@ -2998,7 +3076,7 @@
|
|
|
2998
3076
|
return;
|
|
2999
3077
|
}
|
|
3000
3078
|
|
|
3001
|
-
if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent) {
|
|
3079
|
+
if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || pdfFocusOwnsEvent) {
|
|
3002
3080
|
return;
|
|
3003
3081
|
}
|
|
3004
3082
|
|
|
@@ -3847,50 +3925,316 @@
|
|
|
3847
3925
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
3848
3926
|
}
|
|
3849
3927
|
|
|
3850
|
-
function
|
|
3928
|
+
function isStudioPdfFocusOpen() {
|
|
3929
|
+
return Boolean(studioPdfFocusOverlayEl && studioPdfFocusOverlayEl.hidden === false);
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3932
|
+
function ensureStudioPdfFocusViewer() {
|
|
3933
|
+
if (studioPdfFocusOverlayEl) return studioPdfFocusOverlayEl;
|
|
3934
|
+
|
|
3935
|
+
const overlay = document.createElement("div");
|
|
3936
|
+
overlay.className = "studio-pdf-focus-overlay";
|
|
3937
|
+
overlay.hidden = true;
|
|
3938
|
+
overlay.setAttribute("role", "dialog");
|
|
3939
|
+
overlay.setAttribute("aria-modal", "true");
|
|
3940
|
+
overlay.setAttribute("aria-labelledby", "studioPdfFocusTitle");
|
|
3941
|
+
|
|
3942
|
+
const dialog = document.createElement("div");
|
|
3943
|
+
dialog.className = "studio-pdf-focus-dialog";
|
|
3944
|
+
|
|
3945
|
+
const header = document.createElement("div");
|
|
3946
|
+
header.className = "studio-pdf-focus-header";
|
|
3947
|
+
|
|
3948
|
+
const titleGroup = document.createElement("div");
|
|
3949
|
+
titleGroup.className = "studio-pdf-focus-title-group";
|
|
3950
|
+
|
|
3951
|
+
const closeBtn = document.createElement("button");
|
|
3952
|
+
closeBtn.type = "button";
|
|
3953
|
+
closeBtn.className = "studio-pdf-focus-btn studio-pdf-focus-close";
|
|
3954
|
+
closeBtn.title = "Exit PDF focus view.";
|
|
3955
|
+
closeBtn.setAttribute("aria-label", "Exit PDF focus view");
|
|
3956
|
+
closeBtn.appendChild(makeStudioUiRefreshIcon("focus-exit"));
|
|
3957
|
+
closeBtn.addEventListener("click", () => closeStudioPdfFocusViewer());
|
|
3958
|
+
titleGroup.appendChild(closeBtn);
|
|
3959
|
+
|
|
3960
|
+
const titleEl = document.createElement("div");
|
|
3961
|
+
titleEl.id = "studioPdfFocusTitle";
|
|
3962
|
+
titleEl.className = "studio-pdf-focus-title";
|
|
3963
|
+
titleEl.textContent = "PDF preview";
|
|
3964
|
+
titleGroup.appendChild(titleEl);
|
|
3965
|
+
header.appendChild(titleGroup);
|
|
3966
|
+
|
|
3967
|
+
const actions = document.createElement("div");
|
|
3968
|
+
actions.className = "studio-pdf-focus-actions";
|
|
3969
|
+
|
|
3970
|
+
const openLink = document.createElement("a");
|
|
3971
|
+
openLink.className = "studio-pdf-focus-link";
|
|
3972
|
+
openLink.target = "_blank";
|
|
3973
|
+
openLink.rel = "noopener noreferrer";
|
|
3974
|
+
openLink.textContent = "Open PDF";
|
|
3975
|
+
actions.appendChild(openLink);
|
|
3976
|
+
|
|
3977
|
+
const fullscreenBtn = document.createElement("button");
|
|
3978
|
+
fullscreenBtn.type = "button";
|
|
3979
|
+
fullscreenBtn.className = "studio-pdf-focus-btn studio-pdf-focus-fullscreen";
|
|
3980
|
+
fullscreenBtn.addEventListener("click", async () => {
|
|
3981
|
+
const isFullscreen = Boolean(document.fullscreenElement && studioPdfFocusDialogEl && document.fullscreenElement === studioPdfFocusDialogEl);
|
|
3982
|
+
if (isFullscreen) {
|
|
3983
|
+
try {
|
|
3984
|
+
if (typeof document.exitFullscreen === "function") await document.exitFullscreen();
|
|
3985
|
+
} catch (error) {
|
|
3986
|
+
setStatus("Could not exit PDF fullscreen: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
|
|
3987
|
+
} finally {
|
|
3988
|
+
syncStudioPdfFocusFullscreenButton();
|
|
3989
|
+
}
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
3992
|
+
if (!studioPdfFocusDialogEl || typeof studioPdfFocusDialogEl.requestFullscreen !== "function") {
|
|
3993
|
+
setStatus("Browser fullscreen is not available for this PDF viewer.", "warning");
|
|
3994
|
+
return;
|
|
3995
|
+
}
|
|
3996
|
+
try {
|
|
3997
|
+
await studioPdfFocusDialogEl.requestFullscreen();
|
|
3998
|
+
} catch (error) {
|
|
3999
|
+
setStatus("Could not enter PDF fullscreen: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
|
|
4000
|
+
} finally {
|
|
4001
|
+
syncStudioPdfFocusFullscreenButton();
|
|
4002
|
+
}
|
|
4003
|
+
});
|
|
4004
|
+
actions.appendChild(fullscreenBtn);
|
|
4005
|
+
|
|
4006
|
+
header.appendChild(actions);
|
|
4007
|
+
dialog.appendChild(header);
|
|
4008
|
+
|
|
4009
|
+
const frameSlot = document.createElement("div");
|
|
4010
|
+
frameSlot.className = "studio-pdf-focus-frame-slot";
|
|
4011
|
+
const frame = document.createElement("iframe");
|
|
4012
|
+
frame.className = "studio-pdf-focus-frame";
|
|
4013
|
+
frame.title = "PDF focus viewer";
|
|
4014
|
+
frame.loading = "eager";
|
|
4015
|
+
frameSlot.appendChild(frame);
|
|
4016
|
+
dialog.appendChild(frameSlot);
|
|
4017
|
+
|
|
4018
|
+
overlay.appendChild(dialog);
|
|
4019
|
+
overlay.addEventListener("click", (event) => {
|
|
4020
|
+
if (event.target === overlay) closeStudioPdfFocusViewer();
|
|
4021
|
+
});
|
|
4022
|
+
document.addEventListener("fullscreenchange", syncStudioPdfFocusFullscreenButton);
|
|
4023
|
+
|
|
4024
|
+
document.body.appendChild(overlay);
|
|
4025
|
+
studioPdfFocusOverlayEl = overlay;
|
|
4026
|
+
studioPdfFocusDialogEl = dialog;
|
|
4027
|
+
studioPdfFocusFrameSlotEl = frameSlot;
|
|
4028
|
+
studioPdfFocusFrameEl = frame;
|
|
4029
|
+
studioPdfFocusTitleEl = titleEl;
|
|
4030
|
+
studioPdfFocusOpenLinkEl = openLink;
|
|
4031
|
+
studioPdfFocusFullscreenBtn = fullscreenBtn;
|
|
4032
|
+
studioPdfFocusCloseBtn = closeBtn;
|
|
4033
|
+
syncStudioPdfFocusFullscreenButton();
|
|
4034
|
+
return overlay;
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
function openStudioPdfFocusViewer(viewerUrl, title, sourceFrame) {
|
|
4038
|
+
const src = String(viewerUrl || "").trim();
|
|
4039
|
+
if (!src) return;
|
|
4040
|
+
ensureStudioPdfFocusViewer();
|
|
4041
|
+
studioPdfFocusLastFocusedEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
4042
|
+
if (studioPdfFocusTitleEl) studioPdfFocusTitleEl.textContent = String(title || "PDF preview").trim() || "PDF preview";
|
|
4043
|
+
if (studioPdfFocusOpenLinkEl) studioPdfFocusOpenLinkEl.href = src;
|
|
4044
|
+
setStudioPdfFocusFrameSource(src, title, sourceFrame);
|
|
4045
|
+
if (document.body) document.body.classList.add("studio-pdf-focus-open");
|
|
4046
|
+
if (studioPdfFocusOverlayEl) studioPdfFocusOverlayEl.hidden = false;
|
|
4047
|
+
syncStudioPdfFocusFullscreenButton();
|
|
4048
|
+
closeStudioUiRefreshMenus();
|
|
4049
|
+
closeExportPreviewMenu();
|
|
4050
|
+
window.setTimeout(() => {
|
|
4051
|
+
if (studioPdfFocusCloseBtn && typeof studioPdfFocusCloseBtn.focus === "function") {
|
|
4052
|
+
studioPdfFocusCloseBtn.focus();
|
|
4053
|
+
}
|
|
4054
|
+
}, 0);
|
|
4055
|
+
}
|
|
4056
|
+
|
|
4057
|
+
function closeStudioPdfFocusViewer() {
|
|
4058
|
+
if (!isStudioPdfFocusOpen()) return false;
|
|
4059
|
+
if (document.fullscreenElement && studioPdfFocusDialogEl && studioPdfFocusDialogEl.contains(document.fullscreenElement)) {
|
|
4060
|
+
try {
|
|
4061
|
+
const exitResult = document.exitFullscreen && document.exitFullscreen();
|
|
4062
|
+
if (exitResult && typeof exitResult.catch === "function") exitResult.catch(() => {});
|
|
4063
|
+
} catch {}
|
|
4064
|
+
}
|
|
4065
|
+
if (studioPdfFocusOverlayEl) studioPdfFocusOverlayEl.hidden = true;
|
|
4066
|
+
restoreStudioPdfFocusMovedFrame();
|
|
4067
|
+
if (studioPdfFocusFrameEl) studioPdfFocusFrameEl.src = "about:blank";
|
|
4068
|
+
if (document.body) document.body.classList.remove("studio-pdf-focus-open");
|
|
4069
|
+
syncStudioPdfFocusFullscreenButton();
|
|
4070
|
+
const focusTarget = studioPdfFocusLastFocusedEl;
|
|
4071
|
+
studioPdfFocusLastFocusedEl = null;
|
|
4072
|
+
if (focusTarget && typeof focusTarget.focus === "function" && document.contains(focusTarget)) {
|
|
4073
|
+
window.setTimeout(() => focusTarget.focus(), 0);
|
|
4074
|
+
}
|
|
4075
|
+
return true;
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
function buildStudioPdfResourceUrl(options, useEditorResourceContext) {
|
|
3851
4079
|
const token = getToken();
|
|
3852
4080
|
if (!token) return "";
|
|
3853
4081
|
const pdfPath = String(options && options.path ? options.path : "").trim();
|
|
3854
4082
|
if (!pdfPath) return "";
|
|
3855
4083
|
const effectivePath = getEffectiveSavePath();
|
|
3856
|
-
const sourcePath = effectivePath || sourceState.path || "";
|
|
4084
|
+
const sourcePath = useEditorResourceContext ? (effectivePath || sourceState.path || "") : "";
|
|
4085
|
+
const resourceDir = resourceDirInput && resourceDirInput.value.trim() ? resourceDirInput.value.trim() : "";
|
|
3857
4086
|
const params = new URLSearchParams({ token, path: pdfPath });
|
|
3858
4087
|
if (sourcePath) {
|
|
3859
4088
|
params.set("sourcePath", sourcePath);
|
|
3860
|
-
} else if (
|
|
3861
|
-
params.set("resourceDir",
|
|
4089
|
+
} else if (resourceDir) {
|
|
4090
|
+
params.set("resourceDir", resourceDir);
|
|
3862
4091
|
}
|
|
3863
4092
|
return "/pdf-resource?" + params.toString();
|
|
3864
4093
|
}
|
|
3865
4094
|
|
|
3866
|
-
function
|
|
4095
|
+
function syncStudioPdfFocusFullscreenButton() {
|
|
4096
|
+
if (!studioPdfFocusFullscreenBtn) return;
|
|
4097
|
+
const isFullscreen = Boolean(document.fullscreenElement && studioPdfFocusDialogEl && document.fullscreenElement === studioPdfFocusDialogEl);
|
|
4098
|
+
studioPdfFocusFullscreenBtn.replaceChildren(makeStudioUiRefreshIcon(isFullscreen ? "fullscreen-exit" : "fullscreen"));
|
|
4099
|
+
const label = isFullscreen ? "Exit fullscreen" : "Fullscreen";
|
|
4100
|
+
studioPdfFocusFullscreenBtn.title = isFullscreen
|
|
4101
|
+
? "Exit browser fullscreen and keep the PDF focus viewer open."
|
|
4102
|
+
: "Ask the browser to make this PDF viewer fullscreen.";
|
|
4103
|
+
studioPdfFocusFullscreenBtn.setAttribute("aria-label", label);
|
|
4104
|
+
studioPdfFocusFullscreenBtn.setAttribute("aria-pressed", isFullscreen ? "true" : "false");
|
|
4105
|
+
}
|
|
4106
|
+
|
|
4107
|
+
function restoreStudioPdfFocusMovedFrame() {
|
|
4108
|
+
const state = studioPdfFocusMovedFrameState;
|
|
4109
|
+
studioPdfFocusMovedFrameState = null;
|
|
4110
|
+
if (!state || !state.frame) return;
|
|
4111
|
+
const frame = state.frame;
|
|
4112
|
+
frame.className = state.className;
|
|
4113
|
+
frame.style.cssText = state.styleCssText;
|
|
4114
|
+
if (state.title !== null) frame.setAttribute("title", state.title);
|
|
4115
|
+
else frame.removeAttribute("title");
|
|
4116
|
+
if (state.placeholder && state.placeholder.parentNode) {
|
|
4117
|
+
state.placeholder.parentNode.insertBefore(frame, state.placeholder);
|
|
4118
|
+
state.placeholder.remove();
|
|
4119
|
+
} else if (state.parent && state.parent.isConnected) {
|
|
4120
|
+
state.parent.insertBefore(frame, state.nextSibling && state.nextSibling.parentNode === state.parent ? state.nextSibling : null);
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
|
|
4124
|
+
function setStudioPdfFocusFrameSource(src, title, sourceFrame) {
|
|
4125
|
+
if (!studioPdfFocusFrameSlotEl || !studioPdfFocusFrameEl) return;
|
|
4126
|
+
restoreStudioPdfFocusMovedFrame();
|
|
4127
|
+
const sourceIframe = sourceFrame instanceof HTMLIFrameElement ? sourceFrame : null;
|
|
4128
|
+
if (sourceIframe && sourceIframe.isConnected) {
|
|
4129
|
+
const placeholder = document.createElement("span");
|
|
4130
|
+
placeholder.hidden = true;
|
|
4131
|
+
const parent = sourceIframe.parentNode;
|
|
4132
|
+
parent && parent.insertBefore(placeholder, sourceIframe);
|
|
4133
|
+
studioPdfFocusMovedFrameState = {
|
|
4134
|
+
frame: sourceIframe,
|
|
4135
|
+
parent,
|
|
4136
|
+
nextSibling: placeholder.nextSibling,
|
|
4137
|
+
placeholder,
|
|
4138
|
+
className: sourceIframe.className,
|
|
4139
|
+
styleCssText: sourceIframe.style.cssText,
|
|
4140
|
+
title: sourceIframe.getAttribute("title"),
|
|
4141
|
+
};
|
|
4142
|
+
if (studioPdfFocusFrameEl.parentNode) studioPdfFocusFrameEl.parentNode.removeChild(studioPdfFocusFrameEl);
|
|
4143
|
+
sourceIframe.classList.add("studio-pdf-focus-frame");
|
|
4144
|
+
sourceIframe.style.height = "auto";
|
|
4145
|
+
sourceIframe.style.flex = "1 1 auto";
|
|
4146
|
+
sourceIframe.title = String(title || "PDF focus viewer").trim() || "PDF focus viewer";
|
|
4147
|
+
studioPdfFocusFrameSlotEl.appendChild(sourceIframe);
|
|
4148
|
+
return;
|
|
4149
|
+
}
|
|
4150
|
+
if (!studioPdfFocusFrameEl.parentNode) studioPdfFocusFrameSlotEl.appendChild(studioPdfFocusFrameEl);
|
|
4151
|
+
studioPdfFocusFrameEl.src = src;
|
|
4152
|
+
studioPdfFocusFrameEl.title = String(title || "PDF focus viewer").trim() || "PDF focus viewer";
|
|
4153
|
+
}
|
|
4154
|
+
|
|
4155
|
+
function openStudioPdfFocusFromButton(buttonEl) {
|
|
4156
|
+
if (!buttonEl) return false;
|
|
4157
|
+
const card = buttonEl.closest && buttonEl.closest(".studio-pdf-card");
|
|
4158
|
+
const viewerUrl = String(buttonEl.dataset && buttonEl.dataset.studioPdfViewerUrl ? buttonEl.dataset.studioPdfViewerUrl : "").trim()
|
|
4159
|
+
|| String(card && card.dataset ? (card.dataset.studioPdfViewerUrl || "") : "").trim();
|
|
4160
|
+
const title = String(buttonEl.dataset && buttonEl.dataset.studioPdfTitle ? buttonEl.dataset.studioPdfTitle : "").trim()
|
|
4161
|
+
|| String(card && card.dataset ? (card.dataset.studioPdfTitle || "") : "").trim()
|
|
4162
|
+
|| "PDF preview";
|
|
4163
|
+
const sourceFrame = card && typeof card.querySelector === "function" ? card.querySelector("iframe.studio-pdf-frame") : null;
|
|
4164
|
+
if (!viewerUrl) return false;
|
|
4165
|
+
openStudioPdfFocusViewer(viewerUrl, title, sourceFrame);
|
|
4166
|
+
return true;
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4169
|
+
function handleStudioPdfFocusButtonClick(event) {
|
|
4170
|
+
const target = event && event.target;
|
|
4171
|
+
const buttonEl = target instanceof Element ? target.closest(".studio-pdf-card-focus") : null;
|
|
4172
|
+
if (!buttonEl) return;
|
|
4173
|
+
event.preventDefault();
|
|
4174
|
+
event.stopPropagation();
|
|
4175
|
+
if (typeof event.stopImmediatePropagation === "function") {
|
|
4176
|
+
event.stopImmediatePropagation();
|
|
4177
|
+
}
|
|
4178
|
+
if (!openStudioPdfFocusFromButton(buttonEl)) {
|
|
4179
|
+
setStatus("Could not open PDF focus view for this card.", "warning");
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
|
|
4183
|
+
function createStudioPdfCard(block, useEditorResourceContext) {
|
|
3867
4184
|
const options = block && block.options ? block.options : {};
|
|
3868
4185
|
const path = String(options.path || "").trim();
|
|
3869
4186
|
const title = String(options.title || path || "Embedded PDF").trim();
|
|
3870
4187
|
const caption = String(options.caption || "").trim();
|
|
3871
4188
|
const height = normalizeStudioPdfHeight(options.height);
|
|
3872
4189
|
const page = normalizeStudioPdfPage(options.page);
|
|
3873
|
-
const resourceUrl = buildStudioPdfResourceUrl(options);
|
|
4190
|
+
const resourceUrl = buildStudioPdfResourceUrl(options, useEditorResourceContext);
|
|
3874
4191
|
const viewerUrl = resourceUrl && page ? resourceUrl + "#page=" + encodeURIComponent(String(page)) : resourceUrl;
|
|
3875
4192
|
|
|
3876
4193
|
const card = document.createElement("figure");
|
|
3877
4194
|
card.className = "studio-pdf-card";
|
|
4195
|
+
if (card.dataset) {
|
|
4196
|
+
card.dataset.studioPdfViewerUrl = viewerUrl || "";
|
|
4197
|
+
card.dataset.studioPdfTitle = title;
|
|
4198
|
+
}
|
|
3878
4199
|
|
|
3879
4200
|
const header = document.createElement("figcaption");
|
|
3880
4201
|
header.className = "studio-pdf-card-header";
|
|
4202
|
+
|
|
4203
|
+
const titleGroup = document.createElement("div");
|
|
4204
|
+
titleGroup.className = "studio-pdf-card-title-group";
|
|
4205
|
+
if (resourceUrl) {
|
|
4206
|
+
const focusBtn = document.createElement("button");
|
|
4207
|
+
focusBtn.type = "button";
|
|
4208
|
+
focusBtn.className = "studio-pdf-card-action studio-pdf-card-focus";
|
|
4209
|
+
focusBtn.title = "Open this PDF in a larger Studio overlay.";
|
|
4210
|
+
focusBtn.setAttribute("aria-label", "Focus PDF");
|
|
4211
|
+
if (focusBtn.dataset) {
|
|
4212
|
+
focusBtn.dataset.studioPdfViewerUrl = viewerUrl;
|
|
4213
|
+
focusBtn.dataset.studioPdfTitle = title;
|
|
4214
|
+
}
|
|
4215
|
+
focusBtn.appendChild(makeStudioUiRefreshIcon("focus"));
|
|
4216
|
+
focusBtn.addEventListener("click", handleStudioPdfFocusButtonClick);
|
|
4217
|
+
titleGroup.appendChild(focusBtn);
|
|
4218
|
+
}
|
|
3881
4219
|
const label = document.createElement("div");
|
|
3882
4220
|
label.className = "studio-pdf-card-title";
|
|
3883
4221
|
label.textContent = title;
|
|
3884
|
-
|
|
4222
|
+
titleGroup.appendChild(label);
|
|
4223
|
+
header.appendChild(titleGroup);
|
|
3885
4224
|
|
|
3886
4225
|
if (resourceUrl) {
|
|
4226
|
+
const actions = document.createElement("div");
|
|
4227
|
+
actions.className = "studio-pdf-card-actions";
|
|
4228
|
+
|
|
3887
4229
|
const openLink = document.createElement("a");
|
|
3888
|
-
openLink.className = "studio-pdf-card-link";
|
|
4230
|
+
openLink.className = "studio-pdf-card-link studio-pdf-card-action";
|
|
3889
4231
|
openLink.href = viewerUrl;
|
|
3890
4232
|
openLink.target = "_blank";
|
|
3891
4233
|
openLink.rel = "noopener noreferrer";
|
|
3892
4234
|
openLink.textContent = "Open PDF";
|
|
3893
|
-
|
|
4235
|
+
actions.appendChild(openLink);
|
|
4236
|
+
|
|
4237
|
+
header.appendChild(actions);
|
|
3894
4238
|
}
|
|
3895
4239
|
card.appendChild(header);
|
|
3896
4240
|
|
|
@@ -3919,7 +4263,7 @@
|
|
|
3919
4263
|
return card;
|
|
3920
4264
|
}
|
|
3921
4265
|
|
|
3922
|
-
function renderStudioPdfBlocksInElement(targetEl, blocks) {
|
|
4266
|
+
function renderStudioPdfBlocksInElement(targetEl, blocks, useEditorResourceContext) {
|
|
3923
4267
|
if (!targetEl || !Array.isArray(blocks) || blocks.length === 0) return;
|
|
3924
4268
|
const candidates = Array.from(targetEl.querySelectorAll("p, pre, div"));
|
|
3925
4269
|
blocks.forEach((block) => {
|
|
@@ -3927,7 +4271,7 @@
|
|
|
3927
4271
|
if (!placeholder) return;
|
|
3928
4272
|
const match = candidates.find((el) => String(el.textContent || "").trim() === placeholder);
|
|
3929
4273
|
if (match && match.parentNode) {
|
|
3930
|
-
match.replaceWith(createStudioPdfCard(block));
|
|
4274
|
+
match.replaceWith(createStudioPdfCard(block, useEditorResourceContext));
|
|
3931
4275
|
}
|
|
3932
4276
|
});
|
|
3933
4277
|
}
|
|
@@ -4952,6 +5296,22 @@
|
|
|
4952
5296
|
return "";
|
|
4953
5297
|
}
|
|
4954
5298
|
|
|
5299
|
+
async function fetchWithTimeout(url, options, timeoutMs, timeoutLabel) {
|
|
5300
|
+
if (typeof AbortController === "undefined") return fetch(url, options);
|
|
5301
|
+
const controller = new AbortController();
|
|
5302
|
+
const timer = window.setTimeout(() => controller.abort(), Math.max(1000, Number(timeoutMs) || PDF_EXPORT_FETCH_TIMEOUT_MS));
|
|
5303
|
+
try {
|
|
5304
|
+
return await fetch(url, { ...(options || {}), signal: controller.signal });
|
|
5305
|
+
} catch (error) {
|
|
5306
|
+
if (error && error.name === "AbortError") {
|
|
5307
|
+
throw new Error((timeoutLabel || "Request") + " timed out. Try a smaller export or check the PDF toolchain.");
|
|
5308
|
+
}
|
|
5309
|
+
throw error;
|
|
5310
|
+
} finally {
|
|
5311
|
+
window.clearTimeout(timer);
|
|
5312
|
+
}
|
|
5313
|
+
}
|
|
5314
|
+
|
|
4955
5315
|
async function exportRightPanePdf() {
|
|
4956
5316
|
if (uiBusy || previewExportInProgress) {
|
|
4957
5317
|
setStatus("Studio is busy.", "warning");
|
|
@@ -5011,7 +5371,7 @@
|
|
|
5011
5371
|
setStatus("Exporting PDF…", "warning");
|
|
5012
5372
|
|
|
5013
5373
|
try {
|
|
5014
|
-
const response = await
|
|
5374
|
+
const response = await fetchWithTimeout("/export-pdf?token=" + encodeURIComponent(token), {
|
|
5015
5375
|
method: "POST",
|
|
5016
5376
|
headers: {
|
|
5017
5377
|
"Content-Type": "application/json",
|
|
@@ -5024,7 +5384,7 @@
|
|
|
5024
5384
|
editorPdfLanguage: editorPdfLanguage,
|
|
5025
5385
|
filenameHint: filenameHint,
|
|
5026
5386
|
}),
|
|
5027
|
-
});
|
|
5387
|
+
}, PDF_EXPORT_FETCH_TIMEOUT_MS, "PDF export");
|
|
5028
5388
|
|
|
5029
5389
|
const contentType = String(response.headers.get("content-type") || "").toLowerCase();
|
|
5030
5390
|
if (!response.ok) {
|
|
@@ -5178,7 +5538,7 @@
|
|
|
5178
5538
|
setStatus("Exporting HTML…", "warning");
|
|
5179
5539
|
|
|
5180
5540
|
try {
|
|
5181
|
-
const response = await
|
|
5541
|
+
const response = await fetchWithTimeout("/export-html?token=" + encodeURIComponent(token), {
|
|
5182
5542
|
method: "POST",
|
|
5183
5543
|
headers: {
|
|
5184
5544
|
"Content-Type": "application/json",
|
|
@@ -5192,7 +5552,7 @@
|
|
|
5192
5552
|
filenameHint: filenameHint,
|
|
5193
5553
|
title: titleHint,
|
|
5194
5554
|
}),
|
|
5195
|
-
});
|
|
5555
|
+
}, HTML_EXPORT_FETCH_TIMEOUT_MS, "HTML export");
|
|
5196
5556
|
|
|
5197
5557
|
const contentType = String(response.headers.get("content-type") || "").toLowerCase();
|
|
5198
5558
|
if (!response.ok) {
|
|
@@ -5521,7 +5881,7 @@
|
|
|
5521
5881
|
clearPreviewJumpHighlight(targetEl);
|
|
5522
5882
|
finishPreviewRender(targetEl);
|
|
5523
5883
|
targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown, previewFallbackOptions);
|
|
5524
|
-
renderStudioPdfBlocksInElement(targetEl, pdfPrepared.blocks);
|
|
5884
|
+
renderStudioPdfBlocksInElement(targetEl, pdfPrepared.blocks, previewingEditorText);
|
|
5525
5885
|
applyPreviewAnnotationPlaceholdersToElement(targetEl, previewPrepared.placeholders);
|
|
5526
5886
|
await renderAnnotationMathInElement(targetEl);
|
|
5527
5887
|
decoratePdfEmbeds(targetEl);
|
|
@@ -13876,6 +14236,12 @@
|
|
|
13876
14236
|
});
|
|
13877
14237
|
}
|
|
13878
14238
|
|
|
14239
|
+
if (zenModeBtn) {
|
|
14240
|
+
zenModeBtn.addEventListener("click", () => {
|
|
14241
|
+
setStudioZenMode(!studioZenModeEnabled);
|
|
14242
|
+
});
|
|
14243
|
+
}
|
|
14244
|
+
|
|
13879
14245
|
sendRunBtn.addEventListener("click", () => {
|
|
13880
14246
|
if (getAbortablePendingKind() === "direct") {
|
|
13881
14247
|
requestCancelForPendingRequest("direct");
|
|
@@ -14057,6 +14423,13 @@
|
|
|
14057
14423
|
});
|
|
14058
14424
|
}
|
|
14059
14425
|
|
|
14426
|
+
document.addEventListener("click", (event) => {
|
|
14427
|
+
const target = event.target;
|
|
14428
|
+
const focusBtn = target instanceof Element ? target.closest(".studio-pdf-card-focus") : null;
|
|
14429
|
+
if (!focusBtn) return;
|
|
14430
|
+
handleStudioPdfFocusButtonClick(event);
|
|
14431
|
+
}, true);
|
|
14432
|
+
|
|
14060
14433
|
document.addEventListener("click", (event) => {
|
|
14061
14434
|
const target = event.target;
|
|
14062
14435
|
const copyBtn = target instanceof Element ? target.closest(".studio-copy-block-btn") : null;
|