,
): DecorationSet {
@@ -602,11 +668,25 @@ function buildDecorations(
if (node) decorations.push(Decoration.node(pos, pos + node.nodeSize, { class: cls }));
};
+ // Mount the lock badge for a locked, whole-node page as its own standalone
+ // widget at the page-start boundary. It must live OUTSIDE the page-break /
+ // first-page widget — those carry content-visibility, whose paint containment
+ // hides anything inside them. A sibling widget in the flat list is a direct
+ // child of `.pagination` (not contained), so it paints into the gutter. Side
+ // +1 keeps it after the page-break widget at the same position, i.e. on the
+ // new page's side of the boundary. Mid-node split pages can't use this (their
+ // page has no fresh boundary node), but their break widget is
+ // content-visibility-exempt, so they keep the header-area badge instead.
+ const pushPageLockBadge = (pos: number) => {
+ pushWidget(pos, "page-lock-marker", 1, makePageLockMarker);
+ };
+
// First page top margin / header
pushWidget(0, `page-1-header-${firstPageLabel}-${fp}`, -1, () =>
createFirstPageWidget(firstPageLabel, options),
);
markPageStart(0, "pagination-doc-start");
+ if (firstPageLocked) pushPageLockBadge(0);
// Page breaks
// The key MUST include every value that affects the widget DOM (freespace,
@@ -614,13 +694,18 @@ function buildDecorations(
// pagenum. A matching key keeps the previously drawn DOM, so a key that
// omits e.g. freespace causes stale spacer heights after content edits.
for (const b of breaks) {
- const key = `pb-${b.pagenum}-${b.freespace}-${b.contdName}-${b.splitNodeType}-${b.label ?? ""}-${b.prevLabel ?? ""}-${b.isEmpty ? "E" : ""}-${fp}`;
+ const key = `pb-${b.pagenum}-${b.freespace}-${b.contdName}-${b.splitNodeType}-${b.label ?? ""}-${b.prevLabel ?? ""}-${b.isEmpty ? "E" : ""}-${b.locked ? "L" : ""}-${fp}`;
pushWidget(b.pos, key, -1, () => createPageBreakWidget(b, options));
// Only whole-node breaks start a fresh node; mid-node sentence splits
// (splitNodeType !== null) keep the straddling node, which never had a
// margin to reset — the old `> .pagination-page-break + p` rule didn't
// match inside- widgets either.
- if (b.splitNodeType === null) markPageStart(b.pos, "pagination-break-start");
+ if (b.splitNodeType === null) {
+ markPageStart(b.pos, "pagination-break-start");
+ // Whole-node locked page → mount its badge on the new page's first
+ // paragraph (split locked pages render it in the header area above).
+ if (b.locked) pushPageLockBadge(b.pos);
+ }
}
// Last page bottom margin / footer.
@@ -695,8 +780,7 @@ const ZERO_TOP_MARGIN_TYPES = new Set([
* a constant keeps this off the layout path entirely, the same way LINE_HEIGHT
* mirrors --line-height. Keep in sync with the margin-top rules in scriptio.css.
*/
-const nodeTopMargin = (nodeType: ScreenplayElement): number =>
- ZERO_TOP_MARGIN_TYPES.has(nodeType) ? 0 : LINE_HEIGHT;
+const nodeTopMargin = (nodeType: ScreenplayElement): number => (ZERO_TOP_MARGIN_TYPES.has(nodeType) ? 0 : LINE_HEIGHT);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const setupTestDiv = (editorDom: HTMLElement, _: PaginationOptions): HTMLElement => {
@@ -862,6 +946,8 @@ interface PaginationState {
breaks: PageBreakInfo[];
lastPageFreespace: number;
firstPageLabel: string;
+ /** Whether page 1 carries a frozen page-lock token (drives its lock badge). */
+ firstPageLocked: boolean;
}
/**
@@ -903,6 +989,7 @@ const createPaginationPlugin = (extension: {
breaks: [],
lastPageFreespace: 0,
firstPageLabel: "1",
+ firstPageLocked: false,
}),
apply: timeApply("pagination", (tr, value: PaginationState, oldState, newState): PaginationState => {
// Wait for the screenplay fonts to finish loading before doing
@@ -1395,6 +1482,7 @@ const createPaginationPlugin = (extension: {
// pages keep their frozen labels, provisional inserts get suffix labels
// (e.g. "4A"), and pages past the last lock continue the integer sequence.
let firstPageLabel = "1";
+ let firstPageLocked = false;
if (pageLocks) {
// Omitted scenes collapse whole locked pages out of the
// document. Each such removed page contributes an "absorbed"
@@ -1472,8 +1560,7 @@ const createPaginationPlugin = (extension: {
// Reclaimed pages join the lock map for label computation so
// they get their absorbed number (and following provisional
// pages suffix off it: "18", "18A", …).
- const labelLocks =
- Object.keys(synthetic).length > 0 ? { ...pageLocks, ...synthetic } : pageLocks;
+ const labelLocks = Object.keys(synthetic).length > 0 ? { ...pageLocks, ...synthetic } : pageLocks;
const pageLabels = computePageLabels(breaks, labelLocks, skippedLetters);
// Fold the unreclaimed tokens into the FOLLOWING surviving
@@ -1514,9 +1601,11 @@ const createPaginationPlugin = (extension: {
}
firstPageLabel = pageLabels[0].label;
+ firstPageLocked = pageLabels[0].status === "locked";
for (let i = 0; i < breaks.length; i++) {
breaks[i].label = pageLabels[i + 1].label;
breaks[i].prevLabel = pageLabels[i].label;
+ breaks[i].locked = pageLabels[i + 1].status === "locked";
}
}
@@ -1525,6 +1614,7 @@ const createPaginationPlugin = (extension: {
fullRemeasure ||
lastPageFreespace !== value.lastPageFreespace ||
firstPageLabel !== value.firstPageLabel ||
+ firstPageLocked !== value.firstPageLocked ||
breaks.length !== mappedOldBreaks.length ||
breaks.some(
(b, i) =>
@@ -1533,7 +1623,8 @@ const createPaginationPlugin = (extension: {
b.contdName !== mappedOldBreaks[i].contdName ||
b.label !== mappedOldBreaks[i].label ||
b.prevLabel !== mappedOldBreaks[i].prevLabel ||
- !!b.isEmpty !== !!mappedOldBreaks[i].isEmpty,
+ !!b.isEmpty !== !!mappedOldBreaks[i].isEmpty ||
+ !!b.locked !== !!mappedOldBreaks[i].locked,
);
// Always map the previous set first: it re-anchors every existing
@@ -1554,6 +1645,7 @@ const createPaginationPlugin = (extension: {
breaks,
lastPageFreespace,
firstPageLabel,
+ firstPageLocked,
options,
buildReuseMap(mapped),
);
@@ -1577,12 +1669,13 @@ const createPaginationPlugin = (extension: {
breaks,
lastPageFreespace,
firstPageLabel,
+ firstPageLocked,
options,
buildReuseMap(mapped),
);
}
- return { decset, breaks, lastPageFreespace, firstPageLabel };
+ return { decset, breaks, lastPageFreespace, firstPageLabel, firstPageLocked };
}),
},
appendTransaction() {
@@ -1701,7 +1794,14 @@ export const ScriptioPagination = Extension.create({
if (!style) {
style = document.createElement("style");
style.id = "pagination-style";
- style.textContent = `
+ document.head.appendChild(style);
+ }
+ // Always (re)write the rules. The