studiograph 1.3.48-next.261 → 1.3.48-next.263
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/dist/agent/prompts/system.md +2 -2
- package/dist/agent/skills/artifact-canvas/skill.md +25 -37
- package/dist/agent/skills/paged-documents/skill.md +74 -104
- package/dist/agent/tools/artifact-tools.js +1 -1
- package/dist/agent/tools/artifact-tools.js.map +1 -1
- package/dist/core/artifact-columns-css.d.ts +38 -0
- package/dist/core/artifact-columns-css.js +157 -0
- package/dist/core/artifact-columns-css.js.map +1 -0
- package/dist/core/artifact-frame-document.d.ts +57 -15
- package/dist/core/artifact-frame-document.js +396 -356
- package/dist/core/artifact-frame-document.js.map +1 -1
- package/dist/core/artifact-frame-model.d.ts +42 -1
- package/dist/core/artifact-frame-model.js +102 -1
- package/dist/core/artifact-frame-model.js.map +1 -1
- package/dist/mcp/tools.js +8 -8
- package/dist/mcp/tools.js.map +1 -1
- package/dist/server/routes/artifact-export-api.js +2 -3
- package/dist/server/routes/artifact-export-api.js.map +1 -1
- package/dist/server/routes/chat-turn-outcome.d.ts +36 -0
- package/dist/server/routes/chat-turn-outcome.js +17 -0
- package/dist/server/routes/chat-turn-outcome.js.map +1 -0
- package/dist/services/artifact-library-sources.d.ts +1 -6
- package/dist/services/artifact-library-sources.js +1 -9
- package/dist/services/artifact-library-sources.js.map +1 -1
- package/dist/services/artifact-render-export.d.ts +3 -3
- package/dist/services/artifact-render-export.js +57 -50
- package/dist/services/artifact-render-export.js.map +1 -1
- package/dist/web/_app/immutable/chunks/{Cg7m1gqN.js → 1Av0cjjM.js} +1 -1
- package/dist/web/_app/immutable/chunks/7C_XQ_V22.js +949 -0
- package/dist/web/_app/immutable/chunks/{Bpmx7Umc.js → 7dxFJmtT.js} +1 -1
- package/dist/web/_app/immutable/chunks/{CR1JY_cb.js → B3TQ2v3O.js} +1 -1
- package/dist/web/_app/immutable/chunks/{CZBW5wi82.js → BKJo_vcH2.js} +1 -1
- package/dist/web/_app/immutable/chunks/{Cx5IiiDO2.js → BLjaRsry2.js} +1 -1
- package/dist/web/_app/immutable/chunks/{snCOoD78.js → BWM7Fyds.js} +1 -1
- package/dist/web/_app/immutable/chunks/{CQl_RUVN2.js → BZrDj1Yc2.js} +1 -1
- package/dist/web/_app/immutable/chunks/{D0_Iu3Uj.js → B_EIjYsb.js} +1 -1
- package/dist/web/_app/immutable/chunks/{DaB0hU8_.js → BaVciYXp.js} +1 -1
- package/dist/web/_app/immutable/chunks/{CQW66fYJ.js → BkWTUZUm.js} +1 -1
- package/dist/web/_app/immutable/chunks/{BU5TCj7m.js → BzEpkiVx.js} +1 -1
- package/dist/web/_app/immutable/chunks/{CkivrILi.js → CFQ_EH_P.js} +1 -1
- package/dist/web/_app/immutable/chunks/CL_APTyv.js +1 -0
- package/dist/web/_app/immutable/chunks/{2z2ekE6D2.js → COK5qG3L2.js} +2 -2
- package/dist/web/_app/immutable/chunks/{BIeOkmiV.js → CRwWneZ-.js} +1 -1
- package/dist/web/_app/immutable/chunks/{1QRy7-wo.js → CW2H4JII.js} +1 -1
- package/dist/web/_app/immutable/chunks/{pPsy4008.js → CWeviCpk.js} +1 -1
- package/dist/web/_app/immutable/chunks/{DMFH9Cnt2.js → CXOqnNsj2.js} +1 -1
- package/dist/web/_app/immutable/chunks/{oIZupUnz.js → CX_WuP2f.js} +1 -1
- package/dist/web/_app/immutable/chunks/{BIwe25Lk.js → CY5xLE5F.js} +1 -1
- package/dist/web/_app/immutable/chunks/{D-vvXTU3.js → CeUT6lMf.js} +2 -2
- package/dist/web/_app/immutable/chunks/{B5UH1FNe.js → CgmH6rPv.js} +3 -3
- package/dist/web/_app/immutable/chunks/{-Tbr-96c2.js → CkOWeGhc2.js} +1 -1
- package/dist/web/_app/immutable/chunks/{K5YHt1WL2.js → CkPu5VTd2.js} +1 -1
- package/dist/web/_app/immutable/chunks/ClSRVrsW.js +1 -0
- package/dist/web/_app/immutable/chunks/{CcgYvk_3.js → Cmq7bKUh.js} +1 -1
- package/dist/web/_app/immutable/chunks/{F5xZdLlB.js → CnR_G-ce.js} +1 -1
- package/dist/web/_app/immutable/chunks/{CuYL65Bn.js → CzSx91mJ.js} +1 -1
- package/dist/web/_app/immutable/chunks/D-a94Hdg.js +1 -0
- package/dist/web/_app/immutable/chunks/DAcMjPms.js +2 -0
- package/dist/web/_app/immutable/chunks/{B06uE1qJ.js → DC2fTKy0.js} +1 -1
- package/dist/web/_app/immutable/chunks/{atFSAfcd2.js → DDfqLzbz2.js} +2 -2
- package/dist/web/_app/immutable/chunks/{DhD1HF7R.js → DFLuHkej.js} +1 -1
- package/dist/web/_app/immutable/chunks/{PwUOTfSv.js → DOZR4-gy.js} +1 -1
- package/dist/web/_app/immutable/chunks/DO_V9Cgc.js +1 -0
- package/dist/web/_app/immutable/chunks/{Cxdio_Al.js → DPI7dnke.js} +3 -3
- package/dist/web/_app/immutable/chunks/{CG8YKaqc.js → DVcmIzo2.js} +1 -1
- package/dist/web/_app/immutable/chunks/DZ2Zr5zb.js +1 -0
- package/dist/web/_app/immutable/chunks/{ChS3kWO-.js → DiGsqyRs.js} +1 -1
- package/dist/web/_app/immutable/chunks/{CKwV6HKc.js → DmzyKw-k.js} +1 -1
- package/dist/web/_app/immutable/chunks/Dvj33Rn2.js +1 -0
- package/dist/web/_app/immutable/chunks/{B9VQbssY.js → MDb5bFUr.js} +1 -1
- package/dist/web/_app/immutable/chunks/mYfi0ijf2.js +66 -0
- package/dist/web/_app/immutable/chunks/{CuatNdzp.js → nkOOikT2.js} +1 -1
- package/dist/web/_app/immutable/chunks/{CLninaDG2.js → qU6V8dVT2.js} +2 -2
- package/dist/web/_app/immutable/chunks/{DtXJCaQc2.js → qtwjrlYo2.js} +1 -1
- package/dist/web/_app/immutable/chunks/{B3u4QMoU.js → vR5HiNz9.js} +1 -1
- package/dist/web/_app/immutable/chunks/{7l950wkn.js → xhwwouQZ.js} +1 -1
- package/dist/web/_app/immutable/chunks/{BBrP9GiZ.js → yKBXs2Vl.js} +1 -1
- package/dist/web/_app/immutable/chunks/{sUAtda6y2.js → zMLQbXeR2.js} +1 -1
- package/dist/web/_app/immutable/entry/{app.1tcNTKI-.js → app.CRvuxoVJ.js} +2 -2
- package/dist/web/_app/immutable/entry/start.CRSlQSAo.js +1 -0
- package/dist/web/_app/immutable/nodes/{0.v0W5DL7W.js → 0.DCtO9hJz.js} +2 -2
- package/dist/web/_app/immutable/nodes/{1.81Rb3Ith.js → 1.CU0Xk0hX.js} +1 -1
- package/dist/web/_app/immutable/nodes/{10.CAlZNFOq.js → 10.BQ_I0U3h.js} +1 -1
- package/dist/web/_app/immutable/nodes/{11.CCfUginI.js → 11.D8W7J9oj.js} +1 -1
- package/dist/web/_app/immutable/nodes/{12.BJLboAyk.js → 12.shu8hSiJ.js} +1 -1
- package/dist/web/_app/immutable/nodes/{13.Cz4UTl2G.js → 13.CqSie9pj.js} +1 -1
- package/dist/web/_app/immutable/nodes/15.CRTNtmaK.js +1 -0
- package/dist/web/_app/immutable/nodes/{16.BjohAUt9.js → 16.CLnfWtVJ.js} +1 -1
- package/dist/web/_app/immutable/nodes/{18.DsOT0tDo.js → 18.L6u3xca6.js} +1 -1
- package/dist/web/_app/immutable/nodes/{19.BiJWLOUT.js → 19.BfT4fpP2.js} +1 -1
- package/dist/web/_app/immutable/nodes/{2.Cd72nG2V.js → 2.Cywkr3cW.js} +4 -4
- package/dist/web/_app/immutable/nodes/{3.BIT9mm9r.js → 3.C5-yIMEM.js} +1 -1
- package/dist/web/_app/immutable/nodes/{4.Cn7r0Poe.js → 4.LWwxRkzM.js} +3 -3
- package/dist/web/_app/immutable/nodes/{5.CkJNdCZG.js → 5.CwRllvWS.js} +1 -1
- package/dist/web/_app/immutable/nodes/{6.1rbIRQgQ.js → 6.BcR9vQIF.js} +1 -1
- package/dist/web/_app/immutable/nodes/{7.w17Zl36i.js → 7.Bm0Ie3c3.js} +1 -1
- package/dist/web/_app/immutable/nodes/{8.DqTsYuU-.js → 8.Dj0JJcTw.js} +1 -1
- package/dist/web/_app/immutable/nodes/{9.BJhYZPfc.js → 9.9MiRRKIu.js} +1 -1
- package/dist/web/_app/version.json +1 -1
- package/dist/web/index.html +14 -14
- package/package.json +1 -2
- package/dist/web/_app/immutable/chunks/9Sn2PwSn2.js +0 -1025
- package/dist/web/_app/immutable/chunks/AFZSy-Vk.js +0 -2
- package/dist/web/_app/immutable/chunks/B4PrlkmE.js +0 -1
- package/dist/web/_app/immutable/chunks/C85NG5lS.js +0 -1
- package/dist/web/_app/immutable/chunks/CJbr-v9X.js +0 -1
- package/dist/web/_app/immutable/chunks/D4vfNy_f.js +0 -1
- package/dist/web/_app/immutable/chunks/DG7uGoZb2.js +0 -58
- package/dist/web/_app/immutable/chunks/DTOO9nUj.js +0 -1
- package/dist/web/_app/immutable/chunks/Dav8Lgyy2.js +0 -5
- package/dist/web/_app/immutable/chunks/DjXtgWTP.js +0 -1
- package/dist/web/_app/immutable/entry/start.BJMsySI1.js +0 -1
- package/dist/web/_app/immutable/nodes/15.D2mNE6C-.js +0 -1
- /package/dist/web/_app/immutable/chunks/{Oglnp7sm.js → Oglnp7sm2.js} +0 -0
|
@@ -20,11 +20,11 @@ import { computeFrameLayout, FRAME_LAYOUT_DEFAULT_GAP } from './artifact-frame-l
|
|
|
20
20
|
import { escapeAttribute, escapeHtml, escapeScript, escapeStyle } from './artifact-escape.js';
|
|
21
21
|
import { applyAssetResolution, artifactCsp } from './artifact-asset-resolution.js';
|
|
22
22
|
import { brokenImageScriptTag, libraryTags } from './artifact-libraries.js';
|
|
23
|
-
import {
|
|
24
|
-
/** Cap (ms) on the in-sandbox `document.fonts.ready` wait before
|
|
25
|
-
*
|
|
26
|
-
* normally resolves immediately; the cap only bites when a font falls back
|
|
27
|
-
* slow network endpoint, trading a worst-case late-font reflow for not stalling
|
|
23
|
+
import { rewriteAuthorCssForColumns } from './artifact-columns-css.js';
|
|
24
|
+
/** Cap (ms) on the in-sandbox `document.fonts.ready` wait before the columns
|
|
25
|
+
* compositor measures. The host inlines workspace-asset fonts as data: URLs so
|
|
26
|
+
* this normally resolves immediately; the cap only bites when a font falls back
|
|
27
|
+
* to a slow network endpoint, trading a worst-case late-font reflow for not stalling
|
|
28
28
|
* the canvas for many seconds. Interpolated into the pagination bootstrap. */
|
|
29
29
|
const FONT_READY_TIMEOUT_MS = 2000;
|
|
30
30
|
/** Artboard fill for a frame that opts into `neutral`. The DEFAULT (unset) is
|
|
@@ -138,21 +138,6 @@ function sharedStyleTags(bundle) {
|
|
|
138
138
|
.map(([path, css]) => `<style data-artifact-file="${escapeAttribute(path)}">\n${escapeStyle(css)}\n</style>`)
|
|
139
139
|
.join('\n');
|
|
140
140
|
}
|
|
141
|
-
/** Concatenated shared stylesheet TEXT (raw CSS). Paged.js only honors break
|
|
142
|
-
* rules (`break-before/after/inside`, named pages) from stylesheets handed to
|
|
143
|
-
* its polisher — CSS that merely sits in the document head styles the content
|
|
144
|
-
* visually but is invisible to the fragmenter. So the authored shared CSS is
|
|
145
|
-
* passed to `preview()` alongside the `@page` rule (it is ALSO in the head for
|
|
146
|
-
* global styling; double-application is idempotent). */
|
|
147
|
-
function sharedCssText(bundle) {
|
|
148
|
-
const files = bundle.shared?.files;
|
|
149
|
-
if (!files)
|
|
150
|
-
return '';
|
|
151
|
-
return Object.entries(files)
|
|
152
|
-
.filter(([path]) => path.toLowerCase().endsWith('.css'))
|
|
153
|
-
.map(([, css]) => css)
|
|
154
|
-
.join('\n');
|
|
155
|
-
}
|
|
156
141
|
/** The platform layout CSS: a transparent canvas sized to the total extent, with
|
|
157
142
|
* each frame container absolutely positioned at its computed rect. Fixed frames
|
|
158
143
|
* get an exact height (content clips); auto-height frames grow to content
|
|
@@ -390,9 +375,27 @@ function frameRuntimeScript(artifactId, surface, fullBleed = false, whiteboard =
|
|
|
390
375
|
selected.set(t.targetId, { el: target, target: t });
|
|
391
376
|
publishSelection();
|
|
392
377
|
};
|
|
378
|
+
// Clip an element rect to a box (both in iframe viewport coords), returning the
|
|
379
|
+
// visible intersection as a plain {x,y,width,height}. A paginated document renders
|
|
380
|
+
// each page as a [data-sg-page-window] (overflow:hidden) onto ONE column of a single
|
|
381
|
+
// native multi-column flow; a block that fragments across a column (= page) boundary
|
|
382
|
+
// has a getBoundingClientRect spanning BOTH pages, and getBoundingClientRect ignores
|
|
383
|
+
// the window's clip — so a selection of it would draw ACROSS pages. Clipping to the
|
|
384
|
+
// page window keeps the overlay on the current page (its only visible part there).
|
|
385
|
+
const clipRectToBox = (rect, box) => {
|
|
386
|
+
const left = Math.max(rect.left, box.left);
|
|
387
|
+
const top = Math.max(rect.top, box.top);
|
|
388
|
+
const right = Math.min(rect.right, box.right);
|
|
389
|
+
const bottom = Math.min(rect.bottom, box.bottom);
|
|
390
|
+
return { x: left, y: top, width: Math.max(0, right - left), height: Math.max(0, bottom - top) };
|
|
391
|
+
};
|
|
393
392
|
const descriptorFor = (el) => {
|
|
394
393
|
const frame = el.closest('[data-sg-frame]');
|
|
395
|
-
|
|
394
|
+
// Only content inside a paginated page window is clipped; furniture and a bare
|
|
395
|
+
// frame/tile selection (not inside a window) keep their full rect.
|
|
396
|
+
const pageWindow = el.closest('[data-sg-page-window]');
|
|
397
|
+
const elRect = el.getBoundingClientRect();
|
|
398
|
+
const rect = pageWindow ? clipRectToBox(elRect, pageWindow.getBoundingClientRect()) : elRect;
|
|
396
399
|
const frameId = frame ? frame.getAttribute('data-sg-frame-id') : '';
|
|
397
400
|
// Never emit an empty selector (the host drops empty-selector targets): the
|
|
398
401
|
// frame element addresses by its frame id, anything else by its DOM path,
|
|
@@ -870,51 +873,45 @@ function deckStandaloneRuntime() {
|
|
|
870
873
|
})();`;
|
|
871
874
|
}
|
|
872
875
|
/** Present a `document` artifact as a continuous vertical scroll through its
|
|
873
|
-
* pages — read like a PDF, not stepped like a deck. The
|
|
874
|
-
*
|
|
875
|
-
*
|
|
876
|
-
*
|
|
876
|
+
* pages — read like a PDF, not stepped like a deck. The columns compositor
|
|
877
|
+
* replaces the source frame with discrete `[data-sg-frame][data-sg-page]` page
|
|
878
|
+
* tiles (it runs in EVERY mode; `promote` only selects whether their layout is
|
|
879
|
+
* host-driven (editor grid) or CSS-driven (this scroll column)); a non-paginated
|
|
880
|
+
* (auto/fill-height) document frame flows as a single page tile-free frame. The
|
|
877
881
|
* host sizes the iframe to the present stage; the page column scrolls internally.
|
|
878
|
-
*
|
|
879
|
-
*
|
|
880
|
-
|
|
881
|
-
function presentDocumentLayoutCss(bundle, surfaceAttr = 'present') {
|
|
882
|
-
const surface = `html[data-artifact-surface="${surfaceAttr}"]`;
|
|
882
|
+
* Each tile carries its own inline PW×PH geometry + background from the
|
|
883
|
+
* compositor — here we only center it (`margin:0 auto`) and gap it. */
|
|
884
|
+
function presentDocumentLayoutCss(bundle) {
|
|
883
885
|
// A non-paginated (auto/fill-height) document frame IS the page — give the frame
|
|
884
|
-
// itself page chrome. A paginated frame
|
|
885
|
-
// get chrome
|
|
886
|
+
// itself page chrome. A paginated frame is removed and replaced by page tiles,
|
|
887
|
+
// which get their chrome from the [data-sg-page] rule below.
|
|
886
888
|
const rules = bundle.frames
|
|
887
889
|
.map((frame) => {
|
|
888
890
|
const width = typeof frame.geometry.width === 'number' ? `${frame.geometry.width}px` : '100%';
|
|
889
891
|
const minHeight = typeof frame.geometry.height === 'number' ? `min-height:${frame.geometry.height}px;` : '';
|
|
890
892
|
const background = frameBackground(frame.layout, frame.background, bundle.display?.background);
|
|
891
|
-
return `[data-sg-frame-id="${frame.id}"]:not([data-sg-
|
|
893
|
+
return `[data-sg-frame-id="${frame.id}"]:not([data-sg-page]){width:${width};max-width:100%;margin:0 auto 32px;${minHeight}background:${background};box-shadow:0 6px 24px rgba(0,0,0,0.18);}`;
|
|
892
894
|
})
|
|
893
895
|
.join('\n');
|
|
894
|
-
// The page column
|
|
895
|
-
//
|
|
896
|
-
//
|
|
897
|
-
//
|
|
898
|
-
// `margin:0 auto` (fixed-width box in a full-width column) and gap with
|
|
899
|
-
// `margin-bottom`; the body block-stacks frames + scrolls.
|
|
896
|
+
// The page column uses block flow: each PW×PH tile centers with `margin:0 auto`
|
|
897
|
+
// (fixed-width box in a full-width column), gaps with `margin-bottom`, and the
|
|
898
|
+
// body block-stacks the tiles + scrolls. Page numbers are baked literal text in
|
|
899
|
+
// the per-tile furniture overlay, so there is no cross-tile `counter()` to keep.
|
|
900
900
|
return `html,body{margin:0;padding:0;background:transparent;height:100%;}
|
|
901
901
|
body{box-sizing:border-box;min-height:100%;overflow:auto;padding:48px 0;}
|
|
902
902
|
[data-sg-frame]{position:relative;box-sizing:border-box;background:transparent;overflow:visible;}
|
|
903
|
-
[data-sg-frame][data-sg-
|
|
904
|
-
${surface} .pagedjs_pages{display:block !important;width:auto !important;margin:0 !important;padding:0 !important;background:transparent !important;}
|
|
905
|
-
${surface} .pagedjs_page{position:relative !important;left:auto !important;top:auto !important;margin:0 auto 32px !important;transform:none !important;background:#fff;box-shadow:0 6px 24px rgba(0,0,0,0.18);}
|
|
906
|
-
${surface} .pagedjs_sheet{box-shadow:none !important;}
|
|
903
|
+
[data-sg-frame][data-sg-page]{margin:0 auto 32px;overflow:hidden;box-shadow:0 6px 24px rgba(0,0,0,0.18);}
|
|
907
904
|
${furniturePositionCss()}
|
|
908
905
|
${rules}`;
|
|
909
906
|
}
|
|
910
907
|
// ---------------------------------------------------------------------------
|
|
911
|
-
// Document pagination
|
|
912
|
-
//
|
|
913
|
-
//
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
//
|
|
908
|
+
// Document pagination. A `document` frame holds ONE flowing content stream; the
|
|
909
|
+
// native-columns compositor (columnsCompositorBootstrap) lays it out as a multi-
|
|
910
|
+
// column flow INSIDE the sandbox (one column == one page) and materializes a
|
|
911
|
+
// discrete page TILE per page. The page count is a render OUTPUT, never authored
|
|
912
|
+
// or persisted (the authored `document` frame is the single source of truth —
|
|
913
|
+
// frame model `ResolvedFrame.origin: 'derived'`). PDF export prints the same
|
|
914
|
+
// tiles, so it matches the on-screen pagination exactly.
|
|
918
915
|
//
|
|
919
916
|
// A `document` frame paginates only when its height is a fixed page box (e.g.
|
|
920
917
|
// US Letter 816×1056); a `document` with `height:'auto'` stays a single flowing
|
|
@@ -922,39 +919,85 @@ ${rules}`;
|
|
|
922
919
|
// Defaults live in the model (DEFAULT_PAGE_MARGINS) so the validation cross-check
|
|
923
920
|
// and this render agree on what an absent `pageMargins` side resolves to.
|
|
924
921
|
// ---------------------------------------------------------------------------
|
|
925
|
-
|
|
926
|
-
// for `</script` on every build. Memoize by identity so a session pays once.
|
|
927
|
-
let escapedPagedCache = null;
|
|
928
|
-
function escapedPagedSource(source) {
|
|
929
|
-
if (escapedPagedCache && escapedPagedCache.source === source)
|
|
930
|
-
return escapedPagedCache.escaped;
|
|
931
|
-
const escaped = escapeScript(source);
|
|
932
|
-
escapedPagedCache = { source, escaped };
|
|
933
|
-
return escaped;
|
|
934
|
-
}
|
|
935
|
-
/** The frames the platform paginates: `pages` layout with a fixed page box
|
|
922
|
+
/** A frame the columns compositor paginates: `pages` layout with a fixed page box
|
|
936
923
|
* (numeric width AND height). A `'fill'`/`'auto'` pages frame is a single flowing
|
|
937
924
|
* page, not paginated. */
|
|
938
|
-
function
|
|
939
|
-
return
|
|
925
|
+
function isColumnPaginatedFrame(frame) {
|
|
926
|
+
return frame.layout === 'pages'
|
|
940
927
|
&& typeof frame.geometry.width === 'number'
|
|
941
|
-
&& typeof frame.geometry.height === 'number'
|
|
928
|
+
&& typeof frame.geometry.height === 'number';
|
|
929
|
+
}
|
|
930
|
+
/** The frames the platform paginates (see {@link isColumnPaginatedFrame}). */
|
|
931
|
+
function paginatedFrames(bundle) {
|
|
932
|
+
return bundle.frames.filter(isColumnPaginatedFrame);
|
|
933
|
+
}
|
|
934
|
+
/** Rewrite a paginated frame's CSS for the native-columns compositor (forced page
|
|
935
|
+
* breaks → column breaks; `position:running()` removed), covering BOTH `<style>`
|
|
936
|
+
* blocks AND inline `style="…"` / `style='…'` attributes. An element with an inline
|
|
937
|
+
* `style="break-before:page"` (the offer-letter signature-block idiom) would
|
|
938
|
+
* otherwise survive un-mapped and only paginate by Chromium's lenient page→column
|
|
939
|
+
* coercion inside multicol — not guaranteed cross-browser. Each value runs through
|
|
940
|
+
* the same token-safe {@link rewriteAuthorCssForColumns}.
|
|
941
|
+
*
|
|
942
|
+
* ONE left-to-right scan over an alternation whose `<style>`-block branch is FIRST,
|
|
943
|
+
* so the scanner consumes a whole style element before it can reach a `style=`
|
|
944
|
+
* literal sitting in that element's CSS text (an `[style]` attribute selector, a
|
|
945
|
+
* `content:"…style=…"` string): only real markup attributes match the inline branch
|
|
946
|
+
* — no double-processing. A style element's text can't contain `</style>`, so the
|
|
947
|
+
* non-greedy block match is safe. (Limitation, mirroring the url() segmenter's: body
|
|
948
|
+
* TEXT shaped exactly like `style="break-before:page"` would also be rewritten; that
|
|
949
|
+
* pathological case is not worth a full HTML parser, and the value itself is still
|
|
950
|
+
* token-safe — strings/url()/comments inside it are preserved. Only QUOTED
|
|
951
|
+
* attribute values are matched; a rare unquoted single-declaration
|
|
952
|
+
* `style=break-before:page` is left to the browser's lenient page→column coercion,
|
|
953
|
+
* as it was before this mapping existed.) */
|
|
954
|
+
export function rewritePaginatedContentStyles(html) {
|
|
955
|
+
return html.replace(/(<style\b[^>]*>)([\s\S]*?)(<\/style>)|(\sstyle\s*=\s*)("([^"]*)"|'([^']*)')/gi, (_m, blockOpen, blockCss, blockClose, attrPrefix, attrQuoted, dq, sq) => {
|
|
956
|
+
if (blockOpen !== undefined) {
|
|
957
|
+
return blockOpen + rewriteAuthorCssForColumns(blockCss) + blockClose;
|
|
958
|
+
}
|
|
959
|
+
const quote = attrQuoted[0];
|
|
960
|
+
const value = dq !== undefined ? dq : sq;
|
|
961
|
+
return attrPrefix + quote + rewriteAuthorCssForColumns(value) + quote;
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
/** A bundle whose author CSS is mapped for the native-columns compositor: every
|
|
965
|
+
* shared `.css` file + each paginated frame's inline `<style>` blocks run through
|
|
966
|
+
* {@link rewriteAuthorCssForColumns}. Build-time (no sandbox runtime twin).
|
|
967
|
+
* Geometry, ids, furniture, and non-paginated frames are untouched, so the layout
|
|
968
|
+
* + predicates computed from it are identical to the source bundle. */
|
|
969
|
+
function rewriteBundleForColumns(bundle) {
|
|
970
|
+
const shared = bundle.shared;
|
|
971
|
+
const files = shared?.files;
|
|
972
|
+
const rewrittenFiles = files
|
|
973
|
+
? Object.fromEntries(Object.entries(files).map(([path, content]) => [path, path.toLowerCase().endsWith('.css') ? rewriteAuthorCssForColumns(content) : content]))
|
|
974
|
+
: files;
|
|
975
|
+
return {
|
|
976
|
+
...bundle,
|
|
977
|
+
// Rewrite shared `.css` files AND any `<style>` blocks in `shared.head` (an
|
|
978
|
+
// author can put CSS there too — its break/running rules must map for columns).
|
|
979
|
+
...(shared
|
|
980
|
+
? { shared: { ...shared, files: rewrittenFiles, ...(shared.head ? { head: rewritePaginatedContentStyles(shared.head) } : {}) } }
|
|
981
|
+
: {}),
|
|
982
|
+
frames: bundle.frames.map((frame) => isColumnPaginatedFrame(frame) ? { ...frame, content: rewritePaginatedContentStyles(frame.content) } : frame),
|
|
983
|
+
};
|
|
942
984
|
}
|
|
943
|
-
/** Whether a HOST-LESS render (PNG/PDF/inspect/standalone download)
|
|
944
|
-
*
|
|
985
|
+
/** Whether a HOST-LESS render (PNG/PDF/inspect/standalone download) lays this
|
|
986
|
+
* bundle out as a scrolling page COLUMN of compositor-built page tiles. THE single
|
|
945
987
|
* source of truth for that decision across every host-less caller (the headless
|
|
946
988
|
* render, the export route, the MCP export tool), so none drifts from the
|
|
947
989
|
* builder's actual column layout.
|
|
948
990
|
*
|
|
949
991
|
* Requires a FULLY-paginated document — every frame is a `pages` frame — with at
|
|
950
992
|
* least one fixed page box. A MIXED artifact (pages + other layouts) is NOT
|
|
951
|
-
* paginated host-less: the column layout ({@link isPagesArtifact}) wouldn't
|
|
952
|
-
*
|
|
953
|
-
*
|
|
954
|
-
*
|
|
955
|
-
*
|
|
956
|
-
*
|
|
957
|
-
* Paged.js
|
|
993
|
+
* paginated host-less: the column layout ({@link isPagesArtifact}) wouldn't apply,
|
|
994
|
+
* so it would fall to the clipped grid AND the render's page-box selector would
|
|
995
|
+
* switch to the page TILES and drop the non-page frames entirely. Instead a mixed
|
|
996
|
+
* artifact renders as the grid with every frame present (the live editor, which
|
|
997
|
+
* composites pages into the grid, is the surface that paginates mixed). The
|
|
998
|
+
* compositor itself is always inlined (no library to load), so this no longer
|
|
999
|
+
* gates a multi-hundred-KB Paged.js fetch — only the host-less COLUMN vs GRID
|
|
1000
|
+
* layout choice + the page-box selector. */
|
|
958
1001
|
export function bundleNeedsPagination(bundle) {
|
|
959
1002
|
return isPagesArtifact(bundle) && paginatedFrames(bundle).length > 0;
|
|
960
1003
|
}
|
|
@@ -970,34 +1013,17 @@ function resolvePageMargins(frame) {
|
|
|
970
1013
|
left: m?.left ?? DEFAULT_PAGE_MARGINS.left,
|
|
971
1014
|
};
|
|
972
1015
|
}
|
|
973
|
-
/**
|
|
974
|
-
*
|
|
975
|
-
* it
|
|
976
|
-
* the
|
|
977
|
-
*
|
|
978
|
-
*
|
|
979
|
-
*
|
|
980
|
-
*
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
return `@page{margin:${m.top}px ${m.right}px ${m.bottom}px ${m.left}px;`
|
|
985
|
-
+ `@bottom-right{content:"Page " counter(page) " of " counter(pages);`
|
|
986
|
-
+ `font:12px var(--sg-furniture-font, system-ui, -apple-system, sans-serif);color:#6b7280;}}`;
|
|
987
|
-
}
|
|
988
|
-
/** The AUTHORITATIVE `@page { size }` rule, locked to the frame geometry. Applied
|
|
989
|
-
* as the LAST sheet (after author CSS) so the paginated page box always matches
|
|
990
|
-
* the canvas frame's geometry — the page SIZE is the frame's `geometry`, not
|
|
991
|
-
* something author CSS overrides (which would clip/gap the geometry-forced frame
|
|
992
|
-
* box and desync the reported page count). Margins/margin-boxes stay overridable.
|
|
993
|
-
* This locks the GENERIC `@page` size (the common case); a named/pseudo page with
|
|
994
|
-
* its own size (e.g. `@page cover { size: A4 }`) is a deliberate advanced
|
|
995
|
-
* divergence and is left to the author — it will simply not match the canvas
|
|
996
|
-
* frame box, which is their choice. */
|
|
997
|
-
function pageSizeRuleCss(frame) {
|
|
998
|
-
const width = frame.geometry.width;
|
|
999
|
-
const height = frame.geometry.height;
|
|
1000
|
-
return `@page{size:${width}px ${height}px;}`;
|
|
1016
|
+
/** A content reset for the multi-column flow: cap replaced elements + tables to
|
|
1017
|
+
* the content-column width. Native multicol does NOT shrink an over-wide element
|
|
1018
|
+
* — it overflows the column HORIZONTALLY, which inflates the two-pass page-count
|
|
1019
|
+
* measure (the Range right edge extends past the last column → phantom pages).
|
|
1020
|
+
* Capping the common wide-content culprits (images, media, tables, code blocks) to
|
|
1021
|
+
* 100% keeps the measure honest AND matches the page-document contract (content
|
|
1022
|
+
* fits the page). Low specificity so an author can opt out. (An author who FORCES
|
|
1023
|
+
* a plain block wider than the column can still inflate the count — documented.) */
|
|
1024
|
+
function columnFlowResetCss() {
|
|
1025
|
+
return `[data-sg-doc-flow] img{max-width:100%;height:auto;}
|
|
1026
|
+
[data-sg-doc-flow] svg,[data-sg-doc-flow] video,[data-sg-doc-flow] canvas,[data-sg-doc-flow] table,[data-sg-doc-flow] pre{max-width:100%;}`;
|
|
1001
1027
|
}
|
|
1002
1028
|
/** Base style for the injected per-page furniture (running header/footer). Only
|
|
1003
1029
|
* font/color/positioning-context here — the actual top/bottom/left/right offset
|
|
@@ -1008,71 +1034,69 @@ function pageSizeRuleCss(frame) {
|
|
|
1008
1034
|
function furniturePositionCss() {
|
|
1009
1035
|
return `[data-sg-furniture]{position:absolute;font:12px var(--sg-furniture-font, system-ui, -apple-system, sans-serif);color:#374151;pointer-events:none;}`;
|
|
1010
1036
|
}
|
|
1011
|
-
/** Host CSS
|
|
1012
|
-
*
|
|
1013
|
-
*
|
|
1014
|
-
*
|
|
1037
|
+
/** Host CSS for the editor/export GRID pagination path: just the base style for
|
|
1038
|
+
* the injected per-page furniture overlay (running header/footer + page number).
|
|
1039
|
+
* The page tiles themselves are positioned by frameLayoutCss / the host's
|
|
1040
|
+
* apply-layout, and carry their content window + multi-column flow + background
|
|
1041
|
+
* inline from the columns compositor. */
|
|
1015
1042
|
function paginationHostCss() {
|
|
1016
|
-
return
|
|
1017
|
-
.pagedjs_page{margin:0;background:#fff;position:relative;box-shadow:none;}
|
|
1018
|
-
.pagedjs_sheet{box-shadow:none;}
|
|
1019
|
-
[data-sg-derived] .pagedjs_page{margin:0;}
|
|
1020
|
-
${furniturePositionCss()}`;
|
|
1043
|
+
return furniturePositionCss();
|
|
1021
1044
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1045
|
+
export function columnPageGeometry(frame) {
|
|
1046
|
+
const margins = resolvePageMargins(frame);
|
|
1047
|
+
const width = frame.geometry.width;
|
|
1048
|
+
const height = frame.geometry.height;
|
|
1049
|
+
const contentWidth = width - margins.left - margins.right;
|
|
1050
|
+
const contentHeight = height - margins.top - margins.bottom;
|
|
1051
|
+
const gap = margins.right + margins.left + 40;
|
|
1052
|
+
return { width, height, contentWidth, contentHeight, gap, pitch: contentWidth + gap, margins };
|
|
1053
|
+
}
|
|
1054
|
+
/** The in-sandbox NATIVE-COLUMNS pagination compositor (replaces Paged.js). After
|
|
1055
|
+
* fonts settle it lays each paginated `document` frame's flowing content out as a
|
|
1056
|
+
* multi-column flow (one column == one page), then materializes discrete page
|
|
1057
|
+
* TILES — one `[data-sg-frame][data-sg-page]` per page, each a `width`×`height`
|
|
1058
|
+
* box with a margin-inset `overflow:hidden` window holding a clone of the flow
|
|
1059
|
+
* translated `−i*pitch` so only page `i` shows — plus the furniture overlay
|
|
1060
|
+
* (running header/footer + a default "Page N of M"). The break/running author CSS
|
|
1061
|
+
* was already mapped to columns at build time ({@link rewriteAuthorCssForColumns}).
|
|
1025
1062
|
*
|
|
1026
|
-
* `promote` selects the
|
|
1027
|
-
* - true (live host: editor/present):
|
|
1028
|
-
*
|
|
1029
|
-
*
|
|
1030
|
-
*
|
|
1031
|
-
*
|
|
1032
|
-
* - false (export/print, host:'none'): pages stay in Paged.js's native
|
|
1033
|
-
* `.pagedjs_page` structure in place, so Chromium's print engine maps each
|
|
1034
|
-
* page box to one PDF page (`preferCSSPageSize`) — no guillotine.
|
|
1063
|
+
* `promote` selects only the tile LAYOUT:
|
|
1064
|
+
* - true (live host: editor/present grid): tiles are absolutely placed (the host's
|
|
1065
|
+
* apply-layout refines them) and a `frame-pages` message reports the derived
|
|
1066
|
+
* `<sourceId>__p<n>` ids so the host lays them out + drives selection/zoom/pan.
|
|
1067
|
+
* - false (export / scroll present / standalone): tiles are laid out by CSS
|
|
1068
|
+
* (presentDocumentLayoutCss) as a centered scroll column; no host message.
|
|
1035
1069
|
*
|
|
1036
|
-
*
|
|
1037
|
-
|
|
1070
|
+
* Either way it sets `<html data-sg-paginated-done>` once finished — the settle
|
|
1071
|
+
* signal a host-less render (PNG/PDF/inspect) waits on. NOTE: no backticks in this
|
|
1072
|
+
* string-built bootstrap — they would close the template literal. */
|
|
1073
|
+
function columnsCompositorBootstrap(frames, artifactId, promote, renderId, hostedCanvas, displayBackground) {
|
|
1038
1074
|
const configById = {};
|
|
1039
1075
|
for (const frame of frames) {
|
|
1040
1076
|
configById[frame.id] = {
|
|
1041
|
-
|
|
1042
|
-
sizeCss: pageSizeRuleCss(frame),
|
|
1043
|
-
width: frame.geometry.width,
|
|
1044
|
-
height: frame.geometry.height,
|
|
1077
|
+
...columnPageGeometry(frame),
|
|
1045
1078
|
title: frame.title || 'Document',
|
|
1046
|
-
|
|
1079
|
+
background: frameBackground(frame.layout, frame.background, displayBackground),
|
|
1047
1080
|
...(frame.furniture ? { furniture: frame.furniture } : {}),
|
|
1081
|
+
...(frame.pageNumbers ? { pageNumbers: frame.pageNumbers } : {}),
|
|
1048
1082
|
};
|
|
1049
1083
|
}
|
|
1050
1084
|
return `
|
|
1051
1085
|
(() => {
|
|
1052
1086
|
const artifactId = ${JSON.stringify(artifactId)};
|
|
1053
1087
|
const renderId = ${JSON.stringify(renderId)};
|
|
1054
|
-
const
|
|
1088
|
+
const FRAME_GAP = ${FRAME_LAYOUT_DEFAULT_GAP};
|
|
1055
1089
|
const PROMOTE = ${JSON.stringify(promote)};
|
|
1056
1090
|
const HOSTED_CANVAS = ${JSON.stringify(hostedCanvas)};
|
|
1057
|
-
|
|
1058
|
-
// verbatim. Neutralizes every non-absolute url() to about:blank in the CSS handed
|
|
1059
|
-
// to Paged.js, because Paged.js calls new URL(value) (no base) on each url() token
|
|
1060
|
-
// and a scheme-less ref throws synchronously, aborting the whole pagination run.
|
|
1061
|
-
// Only the Paged-fed copy is sanitized — the head <style> keeps real url()s and
|
|
1062
|
-
// paints fonts/backgrounds. Runs AFTER asset resolution (the embedded SHARED_CSS
|
|
1063
|
-
// is already token-rewritten), so it can't fight the ?at= rewrite.
|
|
1064
|
-
${SANITIZE_CSS_URLS_TWIN}
|
|
1065
|
-
const SHARED_CSS = sanitizeCssUrlsForPagination(${JSON.stringify(sharedCss)});
|
|
1091
|
+
const DONE_ATTR = 'data-sg-paginated-done';
|
|
1066
1092
|
const PAGINATE = ${JSON.stringify(configById)};
|
|
1067
1093
|
const cssEscape = (value) => (window.CSS && CSS.escape) ? CSS.escape(value) : String(value);
|
|
1068
|
-
//
|
|
1069
|
-
|
|
1070
|
-
// Pick the page-position variant: page 1 -> first (or middle), last page -> last
|
|
1071
|
-
// (or middle), every other page -> middle.
|
|
1094
|
+
// Pick the page-position furniture variant: page 1 -> first (or middle), last
|
|
1095
|
+
// page -> last (or middle), every other page -> middle.
|
|
1072
1096
|
const pickSlot = (slots, index, count) => index === 0
|
|
1073
1097
|
? (slots.first || slots.middle)
|
|
1074
1098
|
: (index === count - 1 ? (slots.last || slots.middle) : slots.middle);
|
|
1075
|
-
const addRegion = (
|
|
1099
|
+
const addRegion = (tile, html, region, margins, anchor, offset) => {
|
|
1076
1100
|
if (!html) return;
|
|
1077
1101
|
const el = document.createElement('div');
|
|
1078
1102
|
el.setAttribute('data-sg-furniture', '');
|
|
@@ -1081,213 +1105,214 @@ function paginationBootstrap(frames, artifactId, promote, sharedCss, renderId, h
|
|
|
1081
1105
|
el.style.left = margins.left + 'px';
|
|
1082
1106
|
el.style.right = margins.right + 'px';
|
|
1083
1107
|
el.innerHTML = html;
|
|
1084
|
-
|
|
1085
|
-
//
|
|
1086
|
-
//
|
|
1087
|
-
//
|
|
1088
|
-
//
|
|
1089
|
-
//
|
|
1090
|
-
//
|
|
1091
|
-
//
|
|
1108
|
+
tile.appendChild(el);
|
|
1109
|
+
// Two placements, by anchor:
|
|
1110
|
+
// - 'page' (letterhead/masthead): pin to the page's OUTER edge — header at the
|
|
1111
|
+
// top edge, footer at the bottom edge, offset px in — independent of margin
|
|
1112
|
+
// depth, so a masthead rides high with a deep body margin (no content-padding
|
|
1113
|
+
// hack). Height-independent.
|
|
1114
|
+
// - 'content' (default, running header/footer): bottom-align the header to just
|
|
1115
|
+
// above the content (within the top margin) / top-align the footer to just
|
|
1116
|
+
// below it, from the MEASURED height so a tall band never overlaps the body.
|
|
1117
|
+
// The default "Page N of M" counter is bottom-RIGHT, so a centered/left footer
|
|
1118
|
+
// band clears it. If a margin is smaller than its furniture the author must grow
|
|
1119
|
+
// pageMargins — out-of-flow furniture can't reserve space.
|
|
1092
1120
|
const GAP = 10, MIN_INSET = 8;
|
|
1093
1121
|
const place = () => {
|
|
1122
|
+
if (anchor === 'page') {
|
|
1123
|
+
// Pin to the page's outer edge, offset px in (default MIN_INSET).
|
|
1124
|
+
const inset = (typeof offset === 'number') ? offset : MIN_INSET;
|
|
1125
|
+
if (region === 'header') el.style.top = inset + 'px';
|
|
1126
|
+
else el.style.bottom = inset + 'px';
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
// Content anchor: offset is the GAP between the band and the content edge
|
|
1130
|
+
// (default GAP) — so it has effect on both anchors, per the model.
|
|
1131
|
+
const gap = (typeof offset === 'number') ? offset : GAP;
|
|
1094
1132
|
const h = el.offsetHeight;
|
|
1095
|
-
if (region === 'header') el.style.top = Math.max(MIN_INSET, margins.top -
|
|
1096
|
-
else el.style.bottom = Math.max(MIN_INSET, margins.bottom -
|
|
1133
|
+
if (region === 'header') el.style.top = Math.max(MIN_INSET, margins.top - gap - h) + 'px';
|
|
1134
|
+
else el.style.bottom = Math.max(MIN_INSET, margins.bottom - gap - h) + 'px';
|
|
1097
1135
|
};
|
|
1098
1136
|
place();
|
|
1099
1137
|
// A logo <img> without a fixed height measures 0 until it loads, which would
|
|
1100
|
-
// mis-place
|
|
1101
|
-
// network-idle, so the reposition lands before capture).
|
|
1138
|
+
// mis-place a content-anchored band; re-place once each image settles (the render
|
|
1139
|
+
// waits for network-idle, so the reposition lands before capture).
|
|
1102
1140
|
el.querySelectorAll('img').forEach((img) => { if (!img.complete) img.addEventListener('load', place); });
|
|
1103
1141
|
};
|
|
1104
|
-
const addFurniture = (
|
|
1105
|
-
if (furniture.header) addRegion(
|
|
1106
|
-
if (furniture.footer) addRegion(
|
|
1142
|
+
const addFurniture = (tile, furniture, index, count, margins) => {
|
|
1143
|
+
if (furniture.header) addRegion(tile, pickSlot(furniture.header, index, count), 'header', margins, furniture.header.anchor, furniture.header.offset);
|
|
1144
|
+
if (furniture.footer) addRegion(tile, pickSlot(furniture.footer, index, count), 'footer', margins, furniture.footer.anchor, furniture.footer.offset);
|
|
1107
1145
|
};
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1146
|
+
// The DEFAULT page number, bottom-right within the bottom margin (replaces the
|
|
1147
|
+
// removed Paged.js @page{@bottom-right{counter(page)}} default). Baked as literal
|
|
1148
|
+
// text per tile — no cross-tile counter() to keep — so every document keeps page
|
|
1149
|
+
// numbers even when it declares no furniture.
|
|
1150
|
+
const addPageNumber = (tile, index, count, margins, cfg) => {
|
|
1151
|
+
cfg = cfg || {};
|
|
1152
|
+
if (cfg.show === false) return;
|
|
1153
|
+
if (cfg.showOnFirst === false && index === 0) return;
|
|
1154
|
+
const startAt = (typeof cfg.startAt === 'number') ? cfg.startAt : 1;
|
|
1155
|
+
const n = startAt + index;
|
|
1156
|
+
const m = startAt + count - 1;
|
|
1157
|
+
const format = (typeof cfg.format === 'string') ? cfg.format : 'Page {n} of {m}';
|
|
1158
|
+
const text = format.replace(/\\{n\\}/g, String(n)).replace(/\\{m\\}/g, String(m));
|
|
1159
|
+
if (!text) return;
|
|
1160
|
+
const position = cfg.position || 'bottom-right';
|
|
1161
|
+
const isTop = position.indexOf('top-') === 0;
|
|
1162
|
+
const el = document.createElement('div');
|
|
1163
|
+
el.setAttribute('data-sg-furniture', '');
|
|
1164
|
+
el.setAttribute('data-sg-furniture-region', 'pagenum');
|
|
1165
|
+
el.style.color = '#6b7280';
|
|
1166
|
+
// Vertical: offset px from the nearer page edge, else centered in the band
|
|
1167
|
+
// (the legacy default: bottom-right at about bottom-margin/2).
|
|
1168
|
+
const band = isTop ? margins.top : margins.bottom;
|
|
1169
|
+
const v = (typeof cfg.offset === 'number') ? cfg.offset : Math.max(8, Math.round(band / 2) - 8);
|
|
1170
|
+
el.style[isTop ? 'top' : 'bottom'] = v + 'px';
|
|
1171
|
+
// Horizontal: aligned to the content column's left/right edge, or centered
|
|
1172
|
+
// WITHIN the content column (spanning left..right with text-align, like a
|
|
1173
|
+
// furniture band) so it stays aligned under asymmetric margins.
|
|
1174
|
+
if (position.indexOf('-left') >= 0) el.style.left = margins.left + 'px';
|
|
1175
|
+
else if (position.indexOf('-right') >= 0) el.style.right = margins.right + 'px';
|
|
1176
|
+
else { el.style.left = margins.left + 'px'; el.style.right = margins.right + 'px'; el.style.textAlign = 'center'; }
|
|
1177
|
+
el.textContent = text;
|
|
1178
|
+
tile.appendChild(el);
|
|
1179
|
+
};
|
|
1180
|
+
const compose = async () => {
|
|
1121
1181
|
const expansions = [];
|
|
1122
|
-
// Frames whose
|
|
1123
|
-
//
|
|
1124
|
-
// hint instead of silently showing a clipped, margin-less single page.
|
|
1182
|
+
// Frames whose composition THREW. Reported to the host so it can surface a
|
|
1183
|
+
// non-blocking "couldn't paginate" hint instead of a clipped single page.
|
|
1125
1184
|
const failed = [];
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
//
|
|
1137
|
-
|
|
1138
|
-
//
|
|
1139
|
-
// node sitting IN the paginated flow trips Paged.js's fragmenter (it counts
|
|
1140
|
-
// as a child to walk but has no box) — in the editor surface it yields a
|
|
1141
|
-
// blank first page + over-pagination (e.g. an inline @font-face block paged a
|
|
1142
|
-
// 2-page letter to 4). Styles still apply globally from <head>, where they
|
|
1143
|
-
// belong. Only document frames reach this bootstrap, so decks/apps are
|
|
1144
|
-
// untouched. We ALSO collect inline <style> text into inlineCss and feed it
|
|
1145
|
-
// to the paginator below: Paged.js only honors @page / break / running rules
|
|
1146
|
-
// from the sheets passed to preview(), so an author who put those in an
|
|
1147
|
-
// inline <style> (rather than shared.files) would otherwise see them painted
|
|
1148
|
-
// but ignored for pagination.
|
|
1149
|
-
let inlineCss = '';
|
|
1150
|
-
content.querySelectorAll('style, link[rel="stylesheet"]').forEach((node) => {
|
|
1151
|
-
if (node.tagName === 'STYLE') inlineCss += (node.textContent || '') + ' ';
|
|
1152
|
-
document.head.appendChild(node);
|
|
1153
|
-
});
|
|
1154
|
-
// Render straight into the source frame in BOTH modes (in place). Promote
|
|
1155
|
-
// then moves each page box out to a top-level frame (the canvas owns layout);
|
|
1156
|
-
// export leaves them in place so Paged.js's print structure maps to PDF pages.
|
|
1157
|
-
// Rendering promote into an OFFSCREEN position:absolute container made Paged.js
|
|
1158
|
-
// spuriously split the flow wrapper and emit a blank leading page (plus a
|
|
1159
|
-
// tightened first page) whenever an author used a position:running() letterhead
|
|
1160
|
-
// band — an editor-canvas-only defect (export, which always rendered in place,
|
|
1161
|
-
// was clean). Rendering in place gives promote the exact in-flow geometry
|
|
1162
|
-
// export uses, so the running-element pagination matches. data-sg-derived
|
|
1163
|
-
// still tags the source so paginationHostCss neutralizes Paged.js preview
|
|
1164
|
-
// chrome while the pages live inside it (before promotion).
|
|
1165
|
-
// Capture the authored markup before clearing so a pagination FAILURE restores
|
|
1166
|
-
// the frame to its EXACT original DOM (an un-paginated single flow), not the
|
|
1167
|
-
// doc-flow-wrapped, style-hoisted content copy — which would drop direct-child
|
|
1168
|
-
// selectors like [data-sg-frame-id] > .cover. (source still holds its original
|
|
1169
|
-
// inline styles here: the hoist above moved style/link out of the content clone.)
|
|
1170
|
-
const originalHtml = source.innerHTML;
|
|
1171
|
-
if (PROMOTE) source.setAttribute('data-sg-derived', '');
|
|
1172
|
-
source.innerHTML = '';
|
|
1173
|
-
let pageEls;
|
|
1185
|
+
// ALWAYS run on every exit path (success, per-frame throw, or top-level throw):
|
|
1186
|
+
// mark the settle signal + post frame-pages, so a host-less render's wait
|
|
1187
|
+
// resolves and the live host never holds the reveal forever.
|
|
1188
|
+
const finish = () => {
|
|
1189
|
+
try { document.documentElement.setAttribute(DONE_ATTR, ''); } catch {}
|
|
1190
|
+
if (PROMOTE) {
|
|
1191
|
+
try { parent.postMessage({ channel: 'studiograph-artifact', artifactId: artifactId, type: 'frame-pages', renderId: renderId, expansions: expansions, failed: failed }, '*'); } catch {}
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
try {
|
|
1195
|
+
// Measure after fonts settle so column breaks match the final type. Fonts are
|
|
1196
|
+
// inlined as data: URLs by the host, so this normally resolves immediately;
|
|
1197
|
+
// TIME-BOX it so a slow font endpoint can't stall composition for seconds.
|
|
1174
1198
|
try {
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1199
|
+
if (document.fonts && document.fonts.ready) {
|
|
1200
|
+
await Promise.race([document.fonts.ready, new Promise(function (r) { setTimeout(r, ${FONT_READY_TIMEOUT_MS}); })]);
|
|
1201
|
+
}
|
|
1202
|
+
} catch {}
|
|
1203
|
+
for (const id of Object.keys(PAGINATE)) {
|
|
1204
|
+
const source = document.querySelector('[data-sg-frame-id="' + cssEscape(id) + '"]');
|
|
1205
|
+
if (!source || !source.parentNode) continue;
|
|
1206
|
+
const cfg = PAGINATE[id];
|
|
1207
|
+
try {
|
|
1208
|
+
const PW = cfg.width, PH = cfg.height, CW = cfg.contentWidth, CH = cfg.contentHeight;
|
|
1209
|
+
const GAP = cfg.gap, PITCH = cfg.pitch, m = cfg.margins;
|
|
1210
|
+
// Hoist inline <style>/<link> out of the flow into <head> so they apply
|
|
1211
|
+
// ONCE (not duplicated per page clone) and don't sit inside the multicol
|
|
1212
|
+
// flow. They were rewritten for columns at build time with the shared CSS.
|
|
1213
|
+
source.querySelectorAll('style, link[rel="stylesheet"]').forEach((node) => document.head.appendChild(node));
|
|
1214
|
+
const flowHtml = source.innerHTML;
|
|
1215
|
+
// Two-pass deterministic geometry: multicol STRETCHES columns to fill the
|
|
1216
|
+
// container, drifting positions, so pass 1 measures the page count at a
|
|
1217
|
+
// wide width and pass 2 pins the flow to exactly n*CW+(n-1)*GAP (+ an
|
|
1218
|
+
// explicit column-count) so columns are exactly CW. Measure the real
|
|
1219
|
+
// content extent via a Range (stretched columns inflate scrollWidth and
|
|
1220
|
+
// would invent phantom empty pages).
|
|
1221
|
+
const master = document.createElement('div');
|
|
1222
|
+
master.setAttribute('data-sg-measure', '');
|
|
1223
|
+
// data-sg-doc-flow so the columnFlowResetCss media caps apply during the
|
|
1224
|
+
// measure too (else a wide img/table overflows here and over-counts pages).
|
|
1225
|
+
master.setAttribute('data-sg-doc-flow', '');
|
|
1226
|
+
master.style.cssText = 'position:absolute;left:-99999px;top:0;visibility:hidden;height:' + CH + 'px;width:8000px;column-width:' + CW + 'px;column-gap:' + GAP + 'px;column-fill:auto;';
|
|
1227
|
+
master.innerHTML = flowHtml;
|
|
1228
|
+
document.body.appendChild(master);
|
|
1229
|
+
const measure = () => {
|
|
1230
|
+
const range = document.createRange();
|
|
1231
|
+
range.selectNodeContents(master);
|
|
1232
|
+
const right = range.getBoundingClientRect().right - master.getBoundingClientRect().left;
|
|
1233
|
+
return Math.max(1, Math.ceil((right - 1) / PITCH));
|
|
1234
|
+
};
|
|
1235
|
+
let n = measure();
|
|
1236
|
+
let exact = n * CW + (n - 1) * GAP;
|
|
1237
|
+
master.style.width = exact + 'px';
|
|
1238
|
+
master.style.columnCount = String(n);
|
|
1239
|
+
n = measure();
|
|
1240
|
+
exact = n * CW + (n - 1) * GAP;
|
|
1241
|
+
document.body.removeChild(master);
|
|
1242
|
+
const reported = [];
|
|
1243
|
+
const startLeft = source.offsetLeft;
|
|
1244
|
+
let cursorTop = source.offsetTop;
|
|
1245
|
+
for (let i = 0; i < n; i++) {
|
|
1246
|
+
const pid = id + '__p' + (i + 1);
|
|
1247
|
+
const tile = document.createElement('section');
|
|
1248
|
+
tile.setAttribute('data-sg-frame', '');
|
|
1249
|
+
tile.setAttribute('data-sg-frame-id', pid);
|
|
1250
|
+
tile.setAttribute('data-frame-layout', 'pages');
|
|
1251
|
+
tile.setAttribute('data-sg-derived-from', id);
|
|
1252
|
+
tile.setAttribute('data-sg-page', String(i + 1));
|
|
1253
|
+
tile.setAttribute('role', 'group');
|
|
1254
|
+
tile.setAttribute('aria-label', cfg.title + ' — page ' + (i + 1));
|
|
1255
|
+
tile.style.width = PW + 'px';
|
|
1256
|
+
tile.style.height = PH + 'px';
|
|
1257
|
+
tile.style.background = cfg.background;
|
|
1258
|
+
// PROMOTE (editor grid): place absolutely; the host's apply-layout
|
|
1259
|
+
// refines with the same column math. Scroll modes leave the position to
|
|
1260
|
+
// presentDocumentLayoutCss (margin:0 auto centering).
|
|
1261
|
+
if (PROMOTE) { tile.style.left = startLeft + 'px'; tile.style.top = cursorTop + 'px'; cursorTop += PH + FRAME_GAP; }
|
|
1262
|
+
const win = document.createElement('div');
|
|
1263
|
+
win.setAttribute('data-sg-page-window', '');
|
|
1264
|
+
win.style.cssText = 'position:absolute;overflow:hidden;top:' + m.top + 'px;left:' + m.left + 'px;width:' + CW + 'px;height:' + CH + 'px;';
|
|
1265
|
+
const flow = document.createElement('div');
|
|
1266
|
+
flow.setAttribute('data-sg-doc-flow', '');
|
|
1267
|
+
flow.style.cssText = 'height:' + CH + 'px;width:' + exact + 'px;column-width:' + CW + 'px;column-gap:' + GAP + 'px;column-fill:auto;column-count:' + n + ';transform:translateX(' + (-i * PITCH) + 'px);';
|
|
1268
|
+
flow.innerHTML = flowHtml;
|
|
1269
|
+
// Clone hygiene: each tile holds a full clone of the flow, so author ids
|
|
1270
|
+
// repeat across tiles. We do NOT strip them — the clones are IDENTICAL,
|
|
1271
|
+
// so an SVG url(#grad)/href="#x" resolves to the first matching id (same
|
|
1272
|
+
// gradient/anchor on every page); stripping would instead break a
|
|
1273
|
+
// gradient's own def→fill reference WITHIN the clone. The host draws a11y
|
|
1274
|
+
// from its own per-tile proxies, so duplicate ids don't reach the a11y tree.
|
|
1275
|
+
win.appendChild(flow);
|
|
1276
|
+
tile.appendChild(win);
|
|
1277
|
+
addFurniture(tile, cfg.furniture || {}, i, n, m);
|
|
1278
|
+
addPageNumber(tile, i, n, m, cfg.pageNumbers);
|
|
1279
|
+
source.parentNode.insertBefore(tile, source);
|
|
1280
|
+
reported.push({ id: pid, width: PW, height: PH });
|
|
1281
|
+
}
|
|
1282
|
+
source.parentNode.removeChild(source);
|
|
1283
|
+
expansions.push({ sourceId: id, title: cfg.title, pages: reported });
|
|
1284
|
+
} catch (error) {
|
|
1285
|
+
console.error('Artifact column pagination failed for frame ' + id, error);
|
|
1286
|
+
failed.push({ id: id, reason: String((error && error.message) || error) });
|
|
1287
|
+
// Leave the source frame in place as an un-paginated single flow.
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1207
1290
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1291
|
+
// A document's author CSS often sets a body background (the page color). The
|
|
1292
|
+
// hoisted inline styles + the head shared CSS can paint the WHOLE hosted
|
|
1293
|
+
// surface behind the tiles (the editor grid reads as a solid slab, present
|
|
1294
|
+
// loses page separation). Re-assert the transparent canvas LAST so the tiles
|
|
1295
|
+
// keep their own background and the gaps show the canvas. HOSTED_CANVAS only
|
|
1296
|
+
// (host iframe: editor grid + live present); standalone export / PDF / PNG —
|
|
1297
|
+
// where body IS the document backdrop — are untouched.
|
|
1298
|
+
if (HOSTED_CANVAS) {
|
|
1299
|
+
const canvasReset = document.createElement('style');
|
|
1300
|
+
canvasReset.setAttribute('data-sg-canvas-reset', '');
|
|
1301
|
+
canvasReset.textContent = 'html,body{background:transparent !important;}';
|
|
1302
|
+
document.head.appendChild(canvasReset);
|
|
1215
1303
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
page.style.width = cfg.width + 'px';
|
|
1225
|
-
page.style.height = cfg.height + 'px';
|
|
1226
|
-
const pid = id + '__p' + (index + 1);
|
|
1227
|
-
const frame = document.createElement('section');
|
|
1228
|
-
frame.setAttribute('data-sg-frame', '');
|
|
1229
|
-
frame.setAttribute('data-sg-frame-id', pid);
|
|
1230
|
-
frame.setAttribute('data-frame-layout', 'pages');
|
|
1231
|
-
frame.setAttribute('data-sg-derived-from', id);
|
|
1232
|
-
frame.setAttribute('data-sg-page', String(index + 1));
|
|
1233
|
-
frame.setAttribute('role', 'group');
|
|
1234
|
-
frame.setAttribute('aria-label', cfg.title + ' — page ' + (index + 1));
|
|
1235
|
-
frame.style.width = cfg.width + 'px';
|
|
1236
|
-
frame.style.height = cfg.height + 'px';
|
|
1237
|
-
// Re-establish BOTH page counters that Paged.js kept on the .pagedjs_pages
|
|
1238
|
-
// container we removed by promoting each page to its own top-level frame:
|
|
1239
|
-
// the pages-total on EVERY page so "of M" resolves M (not 0), and a
|
|
1240
|
-
// page-zero reset on the FIRST page so the running number accumulates across
|
|
1241
|
-
// the promoted sibling frames (1..M). Without an explicit reset of the page
|
|
1242
|
-
// counter on a common scope, Chromium implicitly resets it per element and
|
|
1243
|
-
// every footer reads "Page 1". counter-reset scopes to following siblings,
|
|
1244
|
-
// so resetting on the first page also restarts numbering per document when an
|
|
1245
|
-
// artifact has more than one paginated frame.
|
|
1246
|
-
frame.style.counterReset = (index === 0 ? 'page 0 ' : '') + 'pages ' + pageEls.length;
|
|
1247
|
-
// Initial column placement (host apply-layout refines with the same math).
|
|
1248
|
-
frame.style.left = startLeft + 'px';
|
|
1249
|
-
frame.style.top = cursorTop + 'px';
|
|
1250
|
-
cursorTop += cfg.height + GAP;
|
|
1251
|
-
frame.appendChild(page);
|
|
1252
|
-
source.parentNode.insertBefore(frame, source);
|
|
1253
|
-
reported.push({ id: pid, width: cfg.width, height: cfg.height });
|
|
1254
|
-
});
|
|
1255
|
-
// Promotion moved every page box out of the source frame; drop the now-empty
|
|
1256
|
-
// source (its Paged.js .pagedjs_pages shell). Both modes render in place now,
|
|
1257
|
-
// so there is no separate offscreen container to remove.
|
|
1258
|
-
source.parentNode.removeChild(source);
|
|
1259
|
-
expansions.push({ sourceId: id, title: cfg.title, pages: reported });
|
|
1260
|
-
}
|
|
1261
|
-
// A document's author CSS usually sets a body background (the page color). The
|
|
1262
|
-
// bootstrap hoists the frame's inline style to head at runtime, so that rule wins
|
|
1263
|
-
// over the platform's transparent canvas and paints the WHOLE hosted surface
|
|
1264
|
-
// behind the page boxes -- the editor grid reads as a solid slab, the present
|
|
1265
|
-
// view loses its page separation -- instead of pages floating on the canvas.
|
|
1266
|
-
// Re-assert the transparent canvas LAST (after the hoisted author styles): the
|
|
1267
|
-
// page boxes keep their own background, so pages stay their page color and the
|
|
1268
|
-
// gaps show the canvas. HOSTED_CANVAS only (host iframe: editor grid + live
|
|
1269
|
-
// present), so standalone export / PDF / PNG -- where body IS the document
|
|
1270
|
-
// backdrop -- are untouched. Runs for both promote and present (present returns
|
|
1271
|
-
// just below). (Only document frames reach this bootstrap. NOTE: no backticks in
|
|
1272
|
-
// this string-built bootstrap -- they close the template literal.)
|
|
1273
|
-
if (HOSTED_CANVAS) {
|
|
1274
|
-
const canvasReset = document.createElement('style');
|
|
1275
|
-
canvasReset.setAttribute('data-sg-canvas-reset', '');
|
|
1276
|
-
canvasReset.textContent = 'html,body{background:transparent !important;}';
|
|
1277
|
-
document.head.appendChild(canvasReset);
|
|
1304
|
+
if (PROMOTE) {
|
|
1305
|
+
// Renumber data-frame-index across all top-level frames in DOM order so
|
|
1306
|
+
// selection's frameIndex stays consistent after the tiles replace the source.
|
|
1307
|
+
Array.prototype.slice.call(document.querySelectorAll('[data-sg-frame]'))
|
|
1308
|
+
.forEach((el, i) => el.setAttribute('data-frame-index', String(i)));
|
|
1309
|
+
}
|
|
1310
|
+
} finally {
|
|
1311
|
+
finish();
|
|
1278
1312
|
}
|
|
1279
|
-
if (!PROMOTE) return;
|
|
1280
|
-
// Renumber data-frame-index across all top-level frames in DOM order so
|
|
1281
|
-
// selection's frameIndex stays consistent after promotion.
|
|
1282
|
-
Array.prototype.slice.call(document.querySelectorAll('[data-sg-frame]'))
|
|
1283
|
-
.forEach((el, i) => el.setAttribute('data-frame-index', String(i)));
|
|
1284
|
-
// Always post — even with zero successful expansions — so the host knows
|
|
1285
|
-
// pagination has SETTLED and stops holding the reveal (a frame that failed
|
|
1286
|
-
// to paginate just renders as its un-paginated single flow).
|
|
1287
|
-
try { parent.postMessage({ channel: 'studiograph-artifact', artifactId, type: 'frame-pages', renderId, expansions, failed }, '*'); } catch {}
|
|
1288
1313
|
};
|
|
1289
|
-
if (document.readyState === 'loading') window.addEventListener('DOMContentLoaded', () => {
|
|
1290
|
-
else
|
|
1314
|
+
if (document.readyState === 'loading') window.addEventListener('DOMContentLoaded', () => { compose(); });
|
|
1315
|
+
else compose();
|
|
1291
1316
|
})();`;
|
|
1292
1317
|
}
|
|
1293
1318
|
/**
|
|
@@ -1319,8 +1344,8 @@ export function buildFrameDocument(bundle, ctx) {
|
|
|
1319
1344
|
const deckStandalone = ctx.host === 'none' && !!ctx.interactive
|
|
1320
1345
|
&& !whiteboard && !fullBleed && !isPagesArtifact(bundle);
|
|
1321
1346
|
// A present document scrolls through its pages (PDF-style) instead of stepping
|
|
1322
|
-
// frame-by-frame:
|
|
1323
|
-
//
|
|
1347
|
+
// frame-by-frame: the compositor builds page tiles (no host promotion) laid out
|
|
1348
|
+
// by the scroll-column layout, with a non-stepper runtime.
|
|
1324
1349
|
const presentDocument = isPresent && !fullBleed && isPagesArtifact(bundle);
|
|
1325
1350
|
// A standalone (downloaded HTML) document reads the same way: a PDF-style page
|
|
1326
1351
|
// column that scrolls natively. surface is 'export' (not 'present') and there is
|
|
@@ -1330,10 +1355,9 @@ export function buildFrameDocument(bundle, ctx) {
|
|
|
1330
1355
|
const documentStandalone = ctx.host === 'none' && !!ctx.interactive && !fullBleed && isPagesArtifact(bundle);
|
|
1331
1356
|
// A STATIC document export (PNG/PDF — host:'none', non-interactive) lays pages
|
|
1332
1357
|
// out as a column too. The grid would give the single `pages` source frame a
|
|
1333
|
-
// one-page fixed height + overflow:hidden, clipping every page after the first
|
|
1334
|
-
//
|
|
1335
|
-
//
|
|
1336
|
-
// every page.
|
|
1358
|
+
// one-page fixed height + overflow:hidden, clipping every page after the first;
|
|
1359
|
+
// the column stacks each compositor-built page tile as a visible, measurable box
|
|
1360
|
+
// so per-page PNG/PDF capture sees every page.
|
|
1337
1361
|
const documentExport = ctx.host === 'none' && !ctx.interactive && !fullBleed && isPagesArtifact(bundle);
|
|
1338
1362
|
// Every path that lays pages out as a column rather than the grid.
|
|
1339
1363
|
const scrollDocument = presentDocument || documentStandalone || documentExport;
|
|
@@ -1349,7 +1373,7 @@ export function buildFrameDocument(bundle, ctx) {
|
|
|
1349
1373
|
: fullBleed
|
|
1350
1374
|
? fullBleedLayoutCss(bundle)
|
|
1351
1375
|
: scrollDocument
|
|
1352
|
-
? presentDocumentLayoutCss(bundle
|
|
1376
|
+
? presentDocumentLayoutCss(bundle)
|
|
1353
1377
|
: isPresent && !whiteboard
|
|
1354
1378
|
? presentLayoutCss(bundle)
|
|
1355
1379
|
: frameLayoutCss(bundle, layout, ctx.host === 'iframe' ? ctx.colorScheme : undefined);
|
|
@@ -1367,12 +1391,26 @@ export function buildFrameDocument(bundle, ctx) {
|
|
|
1367
1391
|
: isPresent && !presentDocument && !whiteboard
|
|
1368
1392
|
? presentRuntimeScript(bundle.artifactId)
|
|
1369
1393
|
: frameRuntimeScript(bundle.artifactId, surface, fullBleed, whiteboard);
|
|
1370
|
-
// Document frames paginate
|
|
1371
|
-
// single non-document frame, never paginated
|
|
1372
|
-
|
|
1373
|
-
//
|
|
1374
|
-
//
|
|
1375
|
-
//
|
|
1394
|
+
// Document frames paginate via the inlined native-columns compositor (no library
|
|
1395
|
+
// to load). Full-bleed is a single non-document frame, never paginated. The gate
|
|
1396
|
+
// is host-aware (matches the pre-columns pagedSource gate, so behavior is
|
|
1397
|
+
// unchanged): a LIVE host (editor/present iframe) composites ANY paginated frame
|
|
1398
|
+
// into the grid — including a MIXED artifact's lone `pages` frame, which the host
|
|
1399
|
+
// promotes alongside the other frames. A HOST-LESS render only composites a PURE
|
|
1400
|
+
// pages document (every frame is `pages` → the scroll-column layout applies); a
|
|
1401
|
+
// mixed host-less render leaves the `pages` frame as a single grid box (no scroll
|
|
1402
|
+
// column to lay tiles in → unpositioned tiles would overlap), exactly as
|
|
1403
|
+
// `bundleNeedsPagination` (isPagesArtifact) gated before.
|
|
1404
|
+
const paginatable = !fullBleed ? paginatedFrames(bundle) : [];
|
|
1405
|
+
const paginate = paginatable.length > 0 && (ctx.host === 'iframe' || isPagesArtifact(bundle)) ? paginatable : [];
|
|
1406
|
+
// When the doc paginates, map its author CSS (shared + paginated frames' inline
|
|
1407
|
+
// <style>) for columns — forced page breaks → column breaks, position:running()
|
|
1408
|
+
// neutralized — so the head + body emit the column-ready CSS. Geometry/ids/layout
|
|
1409
|
+
// are unchanged, so everything else still reads from the source `bundle`.
|
|
1410
|
+
const docBundle = paginate.length ? rewriteBundleForColumns(bundle) : bundle;
|
|
1411
|
+
// Compose pages into the live host's grid as absolutely-placed tiles (editor) —
|
|
1412
|
+
// but NOT for a present document, which lays its page tiles out as a CSS scroll
|
|
1413
|
+
// column (export/host:none never promotes either).
|
|
1376
1414
|
const promote = ctx.host === 'iframe' && !presentDocument;
|
|
1377
1415
|
const head = [
|
|
1378
1416
|
'<meta charset="utf-8">',
|
|
@@ -1380,21 +1418,24 @@ export function buildFrameDocument(bundle, ctx) {
|
|
|
1380
1418
|
ctx.baseHref ? `<base href="${escapeAttribute(ctx.baseHref)}">` : '',
|
|
1381
1419
|
'<meta name="generator" content="Studiograph Artifact Canvas">',
|
|
1382
1420
|
`<title>${escapeHtml(bundle.title)}</title>`,
|
|
1383
|
-
|
|
1421
|
+
docBundle.shared?.head ?? '',
|
|
1384
1422
|
tokensStyleTag(bundle.tokens),
|
|
1385
1423
|
// The whiteboard ink-token contract (theme-aware --sg-ink/-muted/-surface).
|
|
1386
1424
|
// Before the author's shared CSS so authored rules can reference the vars;
|
|
1387
1425
|
// only a whiteboard surface is platform-themed, so other layouts emit nothing.
|
|
1388
1426
|
whiteboard ? `<style data-artifact-whiteboard-tokens>${whiteboardThemeTokensCss(ctx.colorScheme, ctx.host, whiteboardStandalone)}</style>` : '',
|
|
1389
|
-
sharedStyleTags(
|
|
1427
|
+
sharedStyleTags(docBundle),
|
|
1390
1428
|
`<style data-artifact-frame-layout>\n${layoutCss}\n</style>`,
|
|
1391
1429
|
// Bake the grab cursor for an always-hand surface (present whiteboard) so it
|
|
1392
1430
|
// shows from first paint instead of waiting for the runtime's load handler.
|
|
1393
1431
|
handCursorByDefault ? '<style data-artifact-cursor>html,body{cursor:grab;}</style>' : '',
|
|
1394
|
-
// The editor/export
|
|
1395
|
-
//
|
|
1396
|
-
// pages scroll, they aren't promoted/printed), so it skips this.
|
|
1432
|
+
// The editor/export GRID furniture base CSS. A scroll document folds the same
|
|
1433
|
+
// furniture base into presentDocumentLayoutCss, so it skips this.
|
|
1397
1434
|
paginate.length && !scrollDocument ? `<style data-artifact-pagination>\n${paginationHostCss()}\n</style>` : '',
|
|
1435
|
+
// Content reset for the multi-column flow (caps over-wide media/tables so they
|
|
1436
|
+
// don't overflow the column and inflate the page-count measure). Applies to the
|
|
1437
|
+
// measure master + every tile clone (both carry data-sg-doc-flow).
|
|
1438
|
+
paginate.length ? `<style data-artifact-columns-reset>\n${columnFlowResetCss()}\n</style>` : '',
|
|
1398
1439
|
libraryTags(bundle.libraries ?? [], ctx.librarySources),
|
|
1399
1440
|
// A live host (iframe) drives chrome/transform; a standalone interactive
|
|
1400
1441
|
// whiteboard / deck self-hosts its own pan-zoom / stepper — all need the runtime.
|
|
@@ -1406,11 +1447,10 @@ export function buildFrameDocument(bundle, ctx) {
|
|
|
1406
1447
|
// (a present document scrolls; a whiteboard/full-bleed present fills).
|
|
1407
1448
|
const staticPresentActive = ctx.host === 'none' && isPresent && !scrollDocument && !whiteboard && !fullBleed;
|
|
1408
1449
|
const body = [
|
|
1409
|
-
frameContainersHtml(
|
|
1450
|
+
frameContainersHtml(docBundle, staticPresentActive),
|
|
1410
1451
|
whiteboardStandalone ? whiteboardStandaloneToolbarHtml() : '',
|
|
1411
1452
|
deckStandalone ? deckStandaloneControlsHtml() : '',
|
|
1412
|
-
paginate.length ? `<script data-artifact-
|
|
1413
|
-
paginate.length ? `<script data-artifact-pagination>${escapeScript(paginationBootstrap(paginate, bundle.artifactId, promote, sharedCssText(bundle), ctx.renderId ?? '', ctx.host === 'iframe'))}</script>` : '',
|
|
1453
|
+
paginate.length ? `<script data-artifact-pagination>${escapeScript(columnsCompositorBootstrap(paginate, bundle.artifactId, promote, ctx.renderId ?? '', ctx.host === 'iframe', bundle.display?.background))}</script>` : '',
|
|
1414
1454
|
brokenImageScriptTag(),
|
|
1415
1455
|
sharedScriptTags(bundle),
|
|
1416
1456
|
].filter(Boolean).join('\n');
|