diff --git a/src/features/qword/QWord.test.tsx b/src/features/qword/QWord.test.tsx
index 21390da..fd4b535 100644
--- a/src/features/qword/QWord.test.tsx
+++ b/src/features/qword/QWord.test.tsx
@@ -60,6 +60,47 @@ describe("QWord", () => {
}
});
+ it("показывает отдельные листы печатной разметки по расчетному числу страниц", async () => {
+ const originalResizeObserver = globalThis.ResizeObserver;
+ const originalRequestAnimationFrame = window.requestAnimationFrame;
+
+ globalThis.ResizeObserver = class {
+ observe() {}
+ disconnect() {}
+ } as unknown as typeof ResizeObserver;
+ window.requestAnimationFrame = ((callback: FrameRequestCallback) => {
+ callback(0);
+ return 1;
+ }) as typeof window.requestAnimationFrame;
+
+ const document: WordDocument = {
+ id: "word-print-pages-test",
+ title: "Страницы.docx",
+ updatedAt: "2026-06-02T00:00:00.000Z",
+ html: "
Первая страница
Вторая страница
"
+ };
+
+ try {
+ const view = render();
+ const stage = view.getByLabelText("Редактор QWord");
+ const paper = view.getByLabelText("Содержимое документа");
+ const frame = view.container.querySelector(".paper-frame") as HTMLElement;
+
+ paper.style.setProperty("--qword-page-height", "1123px");
+ Object.defineProperty(paper, "scrollHeight", { configurable: true, value: 2400 });
+ fireEvent.scroll(stage);
+
+ await waitFor(() => expect(view.container.querySelectorAll(".page-sheet")).toHaveLength(3));
+ expect(frame.style.getPropertyValue("--qword-page-count")).toBe("3");
+ expect(frame.style.minHeight).toBe("3425px");
+ expect(frame.style.paddingBottom).toBe("56px");
+ expect(view.container.querySelectorAll(".page-break-guides span")).toHaveLength(2);
+ } finally {
+ globalThis.ResizeObserver = originalResizeObserver;
+ window.requestAnimationFrame = originalRequestAnimationFrame;
+ }
+ });
+
it("переключает вкладки ленты и показывает команды выбранной вкладки", () => {
const originalResizeObserver = globalThis.ResizeObserver;
const originalRequestAnimationFrame = window.requestAnimationFrame;
diff --git a/src/features/qword/QWord.tsx b/src/features/qword/QWord.tsx
index 016aa06..f66956a 100644
--- a/src/features/qword/QWord.tsx
+++ b/src/features/qword/QWord.tsx
@@ -180,6 +180,7 @@ function findInPage(query: string) {
}
const defaultPageViewHeight = 1180;
+const defaultPageGap = 28;
function pageHeightFromStyles(element: HTMLElement) {
const pageHeight = Number.parseFloat(window.getComputedStyle(element).getPropertyValue("--qword-page-height"));
@@ -270,6 +271,12 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP
const [pageView, setPageView] = useState({ count: 1, current: 1, height: defaultPageViewHeight });
const wordCount = useMemo(() => countWords(document.html), [document.html]);
const stageStyle = { "--qword-zoom": String(zoom / 100) } as CSSProperties;
+ const pageFrameStyle = {
+ "--qword-page-count": String(pageView.count),
+ "--qword-page-gap": `${defaultPageGap}px`,
+ minHeight: `${pageView.count * pageView.height + Math.max(0, pageView.count - 1) * defaultPageGap}px`,
+ paddingBottom: pageView.count > 1 ? `${(pageView.count - 1) * defaultPageGap}px` : undefined
+ } as CSSProperties;
const activeRibbonTabLabel = wordRibbonTabs.find((tab) => tab.id === activeRibbonTab)?.label ?? "Главная";
const stageClassName = ["document-stage", `qword-view-${viewMode}`].join(" ");
@@ -998,7 +1005,12 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP
))}
-
+
+
+ {Array.from({ length: pageView.count }, (_, index) => (
+
+ ))}
+
1 ? (
{Array.from({ length: pageView.count - 1 }, (_, index) => (
-
- Страница {index + 2}
-
+
))}
) : null}
diff --git a/src/styles.css b/src/styles.css
index 522ea83..c80fa1f 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -1053,28 +1053,54 @@ button {
}
.paper-frame {
+ --qword-page-gap: 28px;
+ --qword-page-height: 1123px;
position: relative;
width: var(--qword-page-width, 794px);
zoom: var(--qword-zoom, 1);
margin: 0 auto 38px;
}
-.paper {
- --qword-page-height: 1123px;
- min-height: var(--qword-page-height);
+.page-sheets {
+ position: absolute;
+ inset: 0 0 auto;
+ z-index: 0;
+ display: flex;
+ flex-direction: column;
+ gap: var(--qword-page-gap);
+ pointer-events: none;
+}
+
+.page-sheet {
+ display: block;
width: 100%;
- margin: 0;
- padding: 72px 86px;
+ height: var(--qword-page-height);
border: 1px solid #c7d1dd;
background: #ffffff;
box-shadow: var(--shadow);
+}
+
+.paper {
+ min-height: var(--qword-page-height);
+ position: relative;
+ z-index: 1;
+ width: 100%;
+ margin: 0;
+ padding: 72px 86px;
+ border: 0;
+ background: transparent;
+ box-shadow: none;
outline: 0;
}
.qword-view-read .page-ruler,
.qword-view-read .vertical-page-ruler,
+.qword-view-read .page-sheets,
+.qword-view-read .page-break-guides,
.qword-view-web .page-ruler,
-.qword-view-web .vertical-page-ruler {
+.qword-view-web .vertical-page-ruler,
+.qword-view-web .page-sheets,
+.qword-view-web .page-break-guides {
display: none;
}
@@ -1086,6 +1112,9 @@ button {
.qword-view-read .paper {
min-height: auto;
padding: 56px 72px;
+ border: 1px solid #c7d1dd;
+ background: #ffffff;
+ box-shadow: var(--shadow);
}
.qword-view-web .paper-frame {
@@ -1096,6 +1125,7 @@ button {
.qword-view-web .paper {
min-height: auto;
padding: 36px 48px;
+ background: #ffffff;
border-right: 0;
border-left: 0;
box-shadow: none;
@@ -1104,6 +1134,7 @@ button {
.page-break-guides {
position: absolute;
inset: 0;
+ z-index: 2;
pointer-events: none;
}
@@ -1111,30 +1142,8 @@ button {
position: absolute;
left: 0;
right: 0;
- display: grid;
- place-items: center;
- height: 24px;
- border-top: 1px dashed #94a3b8;
- color: #64748b;
- font-size: 11px;
- transform: translateY(-12px);
-}
-
-.page-break-guides span::before,
-.page-break-guides span::after {
- content: "";
- position: absolute;
- top: 11px;
- width: calc(50% - 58px);
- border-top: 1px dashed #94a3b8;
-}
-
-.page-break-guides span::before {
- left: 0;
-}
-
-.page-break-guides span::after {
- right: 0;
+ height: var(--qword-page-gap);
+ background: #dedede;
}
.document-content h1 {