@open-press/core 0.7.1 → 1.0.0
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/README.md +6 -3
- package/engine/cli.mjs +8 -8
- package/engine/commands/_shared.mjs +37 -15
- package/engine/commands/dev.mjs +2 -2
- package/engine/commands/image.mjs +29 -0
- package/engine/commands/skills-sync.mjs +71 -0
- package/engine/commands/typecheck.mjs +63 -1
- package/engine/commands/upgrade.mjs +3 -3
- package/engine/document-export.mjs +1 -1
- package/engine/output/chrome-pdf.mjs +110 -3
- package/engine/output/static-server.mjs +87 -9
- package/engine/react/comment-endpoint.mjs +13 -39
- package/engine/react/comment-marker.mjs +43 -19
- package/engine/react/document-entry.mjs +46 -28
- package/engine/react/document-export.mjs +328 -164
- package/engine/react/http-json.mjs +24 -0
- package/engine/react/mdx-compile.mjs +126 -3
- package/engine/react/measurement-css.mjs +114 -1
- package/engine/react/object-entities.mjs +204 -0
- package/engine/react/pagination/allocator.mjs +48 -3
- package/engine/react/pagination.mjs +1 -1
- package/engine/react/pipeline/allocate.mjs +41 -72
- package/engine/react/pipeline/frame-measurement.mjs +6 -0
- package/engine/react/press-tree-inspection.mjs +172 -0
- package/engine/react/project-asset-endpoint.mjs +6 -24
- package/engine/react/source-edit-endpoint.d.mts +10 -0
- package/engine/react/source-edit-endpoint.mjs +75 -0
- package/engine/react/sources/mdx-resolver.mjs +13 -15
- package/engine/react/style-discovery.mjs +23 -8
- package/engine/runtime/config.d.mts +8 -0
- package/engine/runtime/config.mjs +57 -60
- package/engine/runtime/file-utils.mjs +9 -1
- package/engine/runtime/file-walk.mjs +22 -0
- package/engine/runtime/inspection.mjs +1 -20
- package/engine/runtime/page-geometry.mjs +131 -0
- package/engine/runtime/path-utils.mjs +20 -0
- package/engine/runtime/source-text-tools.d.mts +102 -0
- package/engine/runtime/source-text-tools.mjs +551 -16
- package/engine/runtime/source-workspace.mjs +16 -34
- package/engine/runtime/validation.mjs +19 -10
- package/package.json +3 -5
- package/src/openpress/app/OpenPressApp.tsx +296 -0
- package/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +20 -9
- package/src/openpress/app/WorkspaceGalleryPage.tsx +219 -0
- package/src/openpress/app/index.ts +2 -0
- package/src/openpress/core/Frame.tsx +26 -15
- package/src/openpress/core/FrameContext.tsx +10 -3
- package/src/openpress/core/MdxArea.tsx +11 -12
- package/src/openpress/core/Press.tsx +25 -4
- package/src/openpress/core/Workspace.tsx +36 -0
- package/src/openpress/core/cn.ts +4 -0
- package/src/openpress/core/index.tsx +11 -3
- package/src/openpress/core/primitives.tsx +74 -6
- package/src/openpress/core/types.ts +94 -41
- package/src/openpress/core/useSource.ts +1 -1
- package/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
- package/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
- package/src/openpress/{types.ts → document-model/documentTypes.ts} +51 -0
- package/src/openpress/document-model/index.ts +7 -0
- package/src/openpress/document-model/objectEntityModel.ts +55 -0
- package/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
- package/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
- package/src/openpress/document-model/workspaceManifestModel.ts +57 -0
- package/src/openpress/manuscript/index.tsx +49 -7
- package/src/openpress/mdx/index.ts +15 -7
- package/src/openpress/reader/PageThumbnailsPanel.tsx +168 -0
- package/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
- package/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
- package/src/openpress/reader/index.ts +11 -0
- package/src/openpress/reader/pageViewportScaleModel.ts +73 -0
- package/src/openpress/reader/readerTypes.ts +4 -0
- package/src/openpress/reader/usePageViewportScale.ts +119 -0
- package/src/openpress/reader/usePanelState.ts +56 -0
- package/src/openpress/reader/useReaderHashSync.ts +61 -0
- package/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
- package/src/openpress/reader/useReaderRuntime.ts +146 -0
- package/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
- package/src/openpress/shared/Panel.tsx +77 -0
- package/src/openpress/shared/index.ts +4 -0
- package/src/openpress/shared/numberUtils.ts +3 -0
- package/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
- package/src/openpress/workbench/Workbench.tsx +506 -0
- package/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
- package/src/openpress/workbench/actions/ExportImageControl.tsx +96 -0
- package/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
- package/src/openpress/workbench/actions/SearchControl.tsx +345 -0
- package/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
- package/src/openpress/workbench/actions/index.ts +6 -0
- package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
- package/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
- package/src/openpress/workbench/dialog/index.ts +1 -0
- package/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
- package/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
- package/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
- package/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
- package/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
- package/src/openpress/workbench/document/index.ts +10 -0
- package/src/openpress/workbench/index.ts +2 -0
- package/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
- package/src/openpress/workbench/inspector/index.ts +5 -0
- package/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
- package/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
- package/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
- package/src/openpress/workbench/inspector/useInspectorComments.ts +254 -0
- package/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
- package/src/openpress/workbench/mentions/index.ts +2 -0
- package/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
- package/src/openpress/workbench/panels/Panel.tsx +1 -0
- package/src/openpress/workbench/panels/PendingCommentsPanel.tsx +80 -0
- package/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
- package/src/openpress/workbench/panels/index.ts +3 -0
- package/src/openpress/workbench/project/ProjectEntryPanel.tsx +525 -0
- package/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
- package/src/openpress/workbench/project/index.ts +2 -0
- package/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
- package/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
- package/src/openpress/workbench/shell/index.ts +1 -0
- package/src/openpress/workbench/workbenchFormatters.ts +120 -0
- package/src/openpress/workbench/workbenchTypes.ts +35 -0
- package/src/styles/openpress/print-route.css +0 -2
- package/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
- package/src/styles/openpress/public-viewer.css +25 -320
- package/src/styles/openpress/reader-runtime.css +252 -55
- package/src/styles/openpress/responsive.css +145 -270
- package/src/styles/openpress/workbench-panels.css +327 -178
- package/src/styles/openpress/workbench.css +986 -451
- package/src/styles/openpress/workspace-gallery.css +300 -0
- package/src/styles/openpress.css +2 -1
- package/tsconfig.json +1 -1
- package/vite.config.ts +50 -0
- package/engine/commands/init.mjs +0 -24
- package/engine/init.mjs +0 -90
- package/src/openpress/App.tsx +0 -127
- package/src/openpress/inspector.ts +0 -282
- package/src/openpress/projectWorkspace.tsx +0 -919
- package/src/openpress/readerRuntime.ts +0 -230
- package/src/openpress/workbench.tsx +0 -1265
- package/src/openpress/workbenchTypes.ts +0 -4
- /package/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
- /package/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
- /package/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
- /package/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
- /package/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
- /package/src/openpress/{projectSources.ts → workbench/project/projectSourceModel.ts} +0 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export type PageLayoutMode = "single" | "spread";
|
|
2
|
+
|
|
3
|
+
export type PageViewportScaleMode =
|
|
4
|
+
| "fit-width"
|
|
5
|
+
| "fit-page"
|
|
6
|
+
| "scale-25"
|
|
7
|
+
| "scale-50"
|
|
8
|
+
| "scale-75"
|
|
9
|
+
| "scale-100"
|
|
10
|
+
| "scale-125"
|
|
11
|
+
| "scale-150"
|
|
12
|
+
| "scale-200";
|
|
13
|
+
|
|
14
|
+
export const PAGE_VIEWPORT_SCALE_OPTIONS: Array<{
|
|
15
|
+
value: PageViewportScaleMode;
|
|
16
|
+
label: string;
|
|
17
|
+
}> = [
|
|
18
|
+
{ value: "scale-25", label: "25%" },
|
|
19
|
+
{ value: "scale-50", label: "50%" },
|
|
20
|
+
{ value: "scale-75", label: "75%" },
|
|
21
|
+
{ value: "scale-100", label: "100%" },
|
|
22
|
+
{ value: "scale-125", label: "125%" },
|
|
23
|
+
{ value: "scale-150", label: "150%" },
|
|
24
|
+
{ value: "scale-200", label: "200%" },
|
|
25
|
+
{ value: "fit-width", label: "符合頁面寬度" },
|
|
26
|
+
{ value: "fit-page", label: "符合全開頁面" },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const MIN_PAGE_VIEWPORT_SCALE = 0.12;
|
|
30
|
+
const MAX_FIT_PAGE_VIEWPORT_SCALE = 1;
|
|
31
|
+
const MAX_FIXED_PAGE_VIEWPORT_SCALE = 2;
|
|
32
|
+
|
|
33
|
+
export function resolvePageViewportScale({
|
|
34
|
+
mode,
|
|
35
|
+
fitWidthScale,
|
|
36
|
+
fitPageScale,
|
|
37
|
+
}: {
|
|
38
|
+
mode: PageViewportScaleMode;
|
|
39
|
+
fitWidthScale: number;
|
|
40
|
+
fitPageScale: number;
|
|
41
|
+
}) {
|
|
42
|
+
if (mode === "fit-width") return clampPageViewportScale(fitWidthScale, MAX_FIT_PAGE_VIEWPORT_SCALE);
|
|
43
|
+
if (mode === "fit-page") return clampPageViewportScale(fitPageScale, MAX_FIT_PAGE_VIEWPORT_SCALE);
|
|
44
|
+
return scaleModeToFixedValue(mode);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatPageViewportScaleLabel(mode: PageViewportScaleMode, scale: number) {
|
|
48
|
+
void mode;
|
|
49
|
+
return formatPageViewportScalePercent(scale);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function formatPageViewportScalePercent(scale: number) {
|
|
53
|
+
return `${Math.round(clampPageViewportScale(scale, MAX_FIXED_PAGE_VIEWPORT_SCALE) * 100)}%`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function formatPageViewportScaleValue(scale: number) {
|
|
57
|
+
return clampPageViewportScale(scale, MAX_FIXED_PAGE_VIEWPORT_SCALE)
|
|
58
|
+
.toFixed(4)
|
|
59
|
+
.replace(/0+$/, "")
|
|
60
|
+
.replace(/\.$/, "");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function scaleModeToFixedValue(mode: PageViewportScaleMode) {
|
|
64
|
+
const match = /^scale-(\d+)$/.exec(mode);
|
|
65
|
+
if (!match) return 1;
|
|
66
|
+
return clampPageViewportScale(Number.parseInt(match[1] ?? "100", 10) / 100, MAX_FIXED_PAGE_VIEWPORT_SCALE);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function clampPageViewportScale(value: number, maxScale: number) {
|
|
70
|
+
if (!Number.isFinite(value)) return 1;
|
|
71
|
+
const safeMaxScale = Number.isFinite(maxScale) && maxScale > 0 ? maxScale : MAX_FIXED_PAGE_VIEWPORT_SCALE;
|
|
72
|
+
return Math.min(Math.max(value, MIN_PAGE_VIEWPORT_SCALE), safeMaxScale);
|
|
73
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { useLayoutEffect, useMemo, useState, type RefObject } from "react";
|
|
2
|
+
import { scheduleBrowserFrame } from "../shared";
|
|
3
|
+
import {
|
|
4
|
+
formatPageViewportScaleLabel,
|
|
5
|
+
formatPageViewportScaleValue,
|
|
6
|
+
resolvePageViewportScale,
|
|
7
|
+
type PageLayoutMode,
|
|
8
|
+
type PageViewportScaleMode,
|
|
9
|
+
} from "./pageViewportScaleModel";
|
|
10
|
+
|
|
11
|
+
export function usePageViewportScale({
|
|
12
|
+
stageRef,
|
|
13
|
+
pageContainerRef,
|
|
14
|
+
pageCount,
|
|
15
|
+
layoutMode = "single",
|
|
16
|
+
}: {
|
|
17
|
+
stageRef: RefObject<HTMLElement | null>;
|
|
18
|
+
pageContainerRef: RefObject<HTMLElement | null>;
|
|
19
|
+
pageCount: number;
|
|
20
|
+
layoutMode?: PageLayoutMode;
|
|
21
|
+
}) {
|
|
22
|
+
const [scaleMode, setScaleMode] = useState<PageViewportScaleMode>("fit-width");
|
|
23
|
+
const [scale, setScale] = useState(1);
|
|
24
|
+
|
|
25
|
+
useLayoutEffect(() => {
|
|
26
|
+
if (typeof window === "undefined") return undefined;
|
|
27
|
+
|
|
28
|
+
let cancelFrame: (() => void) | null = null;
|
|
29
|
+
|
|
30
|
+
const syncScale = () => {
|
|
31
|
+
cancelFrame?.();
|
|
32
|
+
cancelFrame = scheduleBrowserFrame(() => {
|
|
33
|
+
cancelFrame = null;
|
|
34
|
+
const container = pageContainerRef.current;
|
|
35
|
+
if (!container) return;
|
|
36
|
+
|
|
37
|
+
const pageSurface = container.querySelector<HTMLElement>(".openpress-html-page__html");
|
|
38
|
+
if (!pageSurface) {
|
|
39
|
+
container.style.setProperty("--openpress-page-viewport-scale", "1");
|
|
40
|
+
container.dataset.openpressPageScaleMode = scaleMode;
|
|
41
|
+
container.dataset.openpressPageScale = "1";
|
|
42
|
+
setScale(1);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const stage = stageRef.current ?? container.parentElement;
|
|
47
|
+
const containerStyle = window.getComputedStyle(container);
|
|
48
|
+
const paddingLeft = parseCssPixelValue(containerStyle.paddingLeft);
|
|
49
|
+
const paddingRight = parseCssPixelValue(containerStyle.paddingRight);
|
|
50
|
+
const paddingTop = parseCssPixelValue(containerStyle.paddingTop);
|
|
51
|
+
const paddingBottom = parseCssPixelValue(containerStyle.paddingBottom);
|
|
52
|
+
const columnGap = parseCssPixelValue(containerStyle.columnGap || containerStyle.gap);
|
|
53
|
+
const availableWidth = Math.max(
|
|
54
|
+
1,
|
|
55
|
+
(stage?.clientWidth || container.clientWidth || window.innerWidth) - paddingLeft - paddingRight,
|
|
56
|
+
);
|
|
57
|
+
const availableHeight = Math.max(
|
|
58
|
+
1,
|
|
59
|
+
(stage?.clientHeight || container.clientHeight || window.innerHeight) - paddingTop - paddingBottom,
|
|
60
|
+
);
|
|
61
|
+
const pageWidth = pageSurface.offsetWidth;
|
|
62
|
+
const pageHeight = pageSurface.offsetHeight;
|
|
63
|
+
const canonicalWidth = layoutMode === "spread" ? (pageWidth * 2) + columnGap : pageWidth;
|
|
64
|
+
const canonicalHeight = pageHeight;
|
|
65
|
+
const fitWidthScale = canonicalWidth > 0 ? availableWidth / canonicalWidth : 1;
|
|
66
|
+
const fitPageScale = canonicalWidth > 0 && canonicalHeight > 0
|
|
67
|
+
? Math.min(availableWidth / canonicalWidth, availableHeight / canonicalHeight)
|
|
68
|
+
: 1;
|
|
69
|
+
const nextScale = resolvePageViewportScale({ mode: scaleMode, fitWidthScale, fitPageScale });
|
|
70
|
+
const nextScaleValue = formatPageViewportScaleValue(nextScale);
|
|
71
|
+
|
|
72
|
+
container.style.setProperty("--openpress-page-viewport-scale", nextScaleValue);
|
|
73
|
+
container.dataset.openpressPageScaleMode = scaleMode;
|
|
74
|
+
container.dataset.openpressPageScale = nextScaleValue;
|
|
75
|
+
setScale((current) => (current === nextScale ? current : nextScale));
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
syncScale();
|
|
80
|
+
|
|
81
|
+
const ResizeObserverCtor = window.ResizeObserver;
|
|
82
|
+
const observer = ResizeObserverCtor ? new ResizeObserverCtor(syncScale) : null;
|
|
83
|
+
const stage = stageRef.current;
|
|
84
|
+
const container = pageContainerRef.current;
|
|
85
|
+
if (stage) observer?.observe(stage);
|
|
86
|
+
if (container) observer?.observe(container);
|
|
87
|
+
|
|
88
|
+
window.addEventListener("resize", syncScale);
|
|
89
|
+
window.visualViewport?.addEventListener("resize", syncScale);
|
|
90
|
+
return () => {
|
|
91
|
+
cancelFrame?.();
|
|
92
|
+
observer?.disconnect();
|
|
93
|
+
window.removeEventListener("resize", syncScale);
|
|
94
|
+
window.visualViewport?.removeEventListener("resize", syncScale);
|
|
95
|
+
};
|
|
96
|
+
}, [layoutMode, pageContainerRef, pageCount, scaleMode, stageRef]);
|
|
97
|
+
|
|
98
|
+
const scaleLabel = useMemo(
|
|
99
|
+
() => {
|
|
100
|
+
const labelScale = scaleMode.startsWith("scale-")
|
|
101
|
+
? resolvePageViewportScale({ mode: scaleMode, fitWidthScale: scale, fitPageScale: scale })
|
|
102
|
+
: scale;
|
|
103
|
+
return formatPageViewportScaleLabel(scaleMode, labelScale);
|
|
104
|
+
},
|
|
105
|
+
[scale, scaleMode],
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
scale,
|
|
110
|
+
scaleMode,
|
|
111
|
+
scaleLabel,
|
|
112
|
+
setScaleMode,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseCssPixelValue(value: string) {
|
|
117
|
+
const parsed = Number.parseFloat(value);
|
|
118
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
119
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export interface UsePanelStateOptions {
|
|
4
|
+
leftPanelBreakpoint?: number;
|
|
5
|
+
rightPanelBreakpoint?: number;
|
|
6
|
+
onAfterResize?: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface PanelState {
|
|
10
|
+
leftPanelOpen: boolean;
|
|
11
|
+
rightPanelOpen: boolean;
|
|
12
|
+
toggleLeftPanel: () => void;
|
|
13
|
+
toggleRightPanel: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function usePanelState({
|
|
17
|
+
leftPanelBreakpoint,
|
|
18
|
+
rightPanelBreakpoint = 1000,
|
|
19
|
+
onAfterResize,
|
|
20
|
+
}: UsePanelStateOptions = {}): PanelState {
|
|
21
|
+
const shouldOpenLeftPanel = useCallback(
|
|
22
|
+
() =>
|
|
23
|
+
leftPanelBreakpoint === undefined || typeof window === "undefined" || window.innerWidth >= leftPanelBreakpoint,
|
|
24
|
+
[leftPanelBreakpoint],
|
|
25
|
+
);
|
|
26
|
+
const shouldOpenRightPanel = useCallback(
|
|
27
|
+
() => typeof window === "undefined" || window.innerWidth >= rightPanelBreakpoint,
|
|
28
|
+
[rightPanelBreakpoint],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const [rightPanelOpen, setRightPanelOpen] = useState(false);
|
|
32
|
+
const [leftPanelOpen, setLeftPanelOpen] = useState(false);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (typeof window === "undefined") return undefined;
|
|
36
|
+
|
|
37
|
+
const handleResize = () => {
|
|
38
|
+
setLeftPanelOpen((open) => (open && !shouldOpenLeftPanel() ? false : open));
|
|
39
|
+
setRightPanelOpen((open) => (open && !shouldOpenRightPanel() ? false : open));
|
|
40
|
+
onAfterResize?.();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
handleResize();
|
|
44
|
+
window.addEventListener("resize", handleResize);
|
|
45
|
+
window.visualViewport?.addEventListener("resize", handleResize);
|
|
46
|
+
return () => {
|
|
47
|
+
window.removeEventListener("resize", handleResize);
|
|
48
|
+
window.visualViewport?.removeEventListener("resize", handleResize);
|
|
49
|
+
};
|
|
50
|
+
}, [shouldOpenLeftPanel, shouldOpenRightPanel, onAfterResize]);
|
|
51
|
+
|
|
52
|
+
const toggleLeftPanel = useCallback(() => setLeftPanelOpen((open) => !open), []);
|
|
53
|
+
const toggleRightPanel = useCallback(() => setRightPanelOpen((open) => !open), []);
|
|
54
|
+
|
|
55
|
+
return { leftPanelOpen, rightPanelOpen, toggleLeftPanel, toggleRightPanel };
|
|
56
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useEffect, type MutableRefObject } from "react";
|
|
2
|
+
import { pageIndexFromHash, replacePageRoute } from "./readerPageRoute";
|
|
3
|
+
import { scrollToPage } from "./readerScroll";
|
|
4
|
+
|
|
5
|
+
export interface UseReaderHashSyncOptions {
|
|
6
|
+
stageRef: MutableRefObject<HTMLElement | null>;
|
|
7
|
+
pageRefs: MutableRefObject<Array<HTMLElement | null>>;
|
|
8
|
+
currentPageIndex: number;
|
|
9
|
+
currentPageIndexRef: MutableRefObject<number>;
|
|
10
|
+
normalizedPageCount: number;
|
|
11
|
+
setCurrentPageIndex: (index: number) => void;
|
|
12
|
+
armPendingScrollTarget: (target: number) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useReaderHashSync({
|
|
16
|
+
stageRef,
|
|
17
|
+
pageRefs,
|
|
18
|
+
currentPageIndex,
|
|
19
|
+
currentPageIndexRef,
|
|
20
|
+
normalizedPageCount,
|
|
21
|
+
setCurrentPageIndex,
|
|
22
|
+
armPendingScrollTarget,
|
|
23
|
+
}: UseReaderHashSyncOptions) {
|
|
24
|
+
// Mirror currentPageIndex into the URL hash so deep links + history work.
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (typeof window === "undefined") return;
|
|
27
|
+
replacePageRoute(currentPageIndex);
|
|
28
|
+
}, [currentPageIndex]);
|
|
29
|
+
|
|
30
|
+
// Listen for hash/back/forward navigation and drive scroll to match.
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (typeof window === "undefined") return undefined;
|
|
33
|
+
|
|
34
|
+
const syncFromHash = (behavior: ScrollBehavior) => {
|
|
35
|
+
const refs = pageRefs.current;
|
|
36
|
+
const hashPage = pageIndexFromHash(window.location.hash, normalizedPageCount);
|
|
37
|
+
if (hashPage === null) return;
|
|
38
|
+
// replacePageRoute writes the hash to mirror state; skip if it already
|
|
39
|
+
// matches so we don't fight ourselves.
|
|
40
|
+
if (hashPage === currentPageIndexRef.current) return;
|
|
41
|
+
armPendingScrollTarget(hashPage);
|
|
42
|
+
setCurrentPageIndex(hashPage);
|
|
43
|
+
scrollToPage(refs, hashPage, behavior, stageRef.current);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const onHashChange = () => syncFromHash("smooth");
|
|
47
|
+
window.addEventListener("hashchange", onHashChange);
|
|
48
|
+
window.addEventListener("popstate", onHashChange);
|
|
49
|
+
return () => {
|
|
50
|
+
window.removeEventListener("hashchange", onHashChange);
|
|
51
|
+
window.removeEventListener("popstate", onHashChange);
|
|
52
|
+
};
|
|
53
|
+
}, [
|
|
54
|
+
armPendingScrollTarget,
|
|
55
|
+
currentPageIndexRef,
|
|
56
|
+
normalizedPageCount,
|
|
57
|
+
pageRefs,
|
|
58
|
+
setCurrentPageIndex,
|
|
59
|
+
stageRef,
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
export interface UseReaderKeyboardNavOptions {
|
|
4
|
+
nextPage: () => void;
|
|
5
|
+
prevPage: () => void;
|
|
6
|
+
setPage: (pageIndex: number) => void;
|
|
7
|
+
normalizedPageCount: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useReaderKeyboardNav({
|
|
11
|
+
nextPage,
|
|
12
|
+
prevPage,
|
|
13
|
+
setPage,
|
|
14
|
+
normalizedPageCount,
|
|
15
|
+
}: UseReaderKeyboardNavOptions) {
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
18
|
+
if (isEditableTarget(event.target)) return;
|
|
19
|
+
if (hasActiveTextSelection()) return;
|
|
20
|
+
if (event.key === "ArrowRight" || event.key === "PageDown") {
|
|
21
|
+
event.preventDefault();
|
|
22
|
+
nextPage();
|
|
23
|
+
} else if (event.key === "ArrowLeft" || event.key === "PageUp") {
|
|
24
|
+
event.preventDefault();
|
|
25
|
+
prevPage();
|
|
26
|
+
} else if (event.key === "Home") {
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
setPage(0);
|
|
29
|
+
} else if (event.key === "End") {
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
setPage(Math.max(0, normalizedPageCount - 1));
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
35
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
36
|
+
}, [nextPage, prevPage, setPage, normalizedPageCount]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isEditableTarget(target: EventTarget | null) {
|
|
40
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
41
|
+
return Boolean(target.closest("input, textarea, select, button, [contenteditable]"));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hasActiveTextSelection() {
|
|
45
|
+
const selection = window.getSelection?.();
|
|
46
|
+
if (!selection || selection.isCollapsed) return false;
|
|
47
|
+
return Boolean(selection.toString().trim());
|
|
48
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState, type RefCallback } from "react";
|
|
2
|
+
import { pageIndexFromHash } from "./readerPageRoute";
|
|
3
|
+
import { createReaderPageRegistry } from "./readerPageRegistry";
|
|
4
|
+
import { clampReaderPageIndex, formatReaderPageNumber, normalizeReaderPageCount } from "./readerStateModel";
|
|
5
|
+
import { createPageVisibilityObserver, scrollToPage } from "./readerScroll";
|
|
6
|
+
import { usePanelState } from "./usePanelState";
|
|
7
|
+
import { useReaderScrollAnchor } from "./useReaderScrollAnchor";
|
|
8
|
+
import { useReaderHashSync } from "./useReaderHashSync";
|
|
9
|
+
import { useReaderKeyboardNav } from "./useReaderKeyboardNav";
|
|
10
|
+
|
|
11
|
+
export interface SetPageOptions {
|
|
12
|
+
behavior?: ScrollBehavior;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface UseReaderRuntimeOptions {
|
|
16
|
+
pageCount: number;
|
|
17
|
+
leftPanelBreakpoint?: number;
|
|
18
|
+
rightPanelBreakpoint?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useReaderRuntime({
|
|
22
|
+
pageCount,
|
|
23
|
+
leftPanelBreakpoint,
|
|
24
|
+
rightPanelBreakpoint = 1000,
|
|
25
|
+
}: UseReaderRuntimeOptions) {
|
|
26
|
+
const normalizedPageCount = normalizeReaderPageCount(pageCount);
|
|
27
|
+
const stageRef = useRef<HTMLElement | null>(null);
|
|
28
|
+
const [pageRegistrationVersion, setPageRegistrationVersion] = useState(0);
|
|
29
|
+
const pageRegistry = useRef<ReturnType<typeof createReaderPageRegistry<HTMLElement>> | null>(null);
|
|
30
|
+
if (!pageRegistry.current) {
|
|
31
|
+
pageRegistry.current = createReaderPageRegistry<HTMLElement>(setPageRegistrationVersion);
|
|
32
|
+
}
|
|
33
|
+
const pageRefs = useMemo(() => ({
|
|
34
|
+
get current() {
|
|
35
|
+
return pageRegistry.current?.refs ?? [];
|
|
36
|
+
},
|
|
37
|
+
}), []) as { current: Array<HTMLElement | null> };
|
|
38
|
+
|
|
39
|
+
const [currentPageIndex, setCurrentPageIndex] = useState(() => {
|
|
40
|
+
if (typeof window === "undefined") return 0;
|
|
41
|
+
const fromHash = pageIndexFromHash(window.location.hash, normalizedPageCount);
|
|
42
|
+
return fromHash ?? 0;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const currentPageIndexRef = useRef(currentPageIndex);
|
|
46
|
+
currentPageIndexRef.current = currentPageIndex;
|
|
47
|
+
|
|
48
|
+
const { pendingScrollTargetRef, armPendingScrollTarget, clearPendingScrollTarget, reAnchorAfterPaint } =
|
|
49
|
+
useReaderScrollAnchor({ stageRef, pageRefs, currentPageIndexRef });
|
|
50
|
+
|
|
51
|
+
const { leftPanelOpen, rightPanelOpen, toggleLeftPanel, toggleRightPanel } = usePanelState({
|
|
52
|
+
leftPanelBreakpoint,
|
|
53
|
+
rightPanelBreakpoint,
|
|
54
|
+
// scroll-snap-type: y mandatory re-aligns to the closest snap point on
|
|
55
|
+
// viewport change, which can land one page off from where the reader was.
|
|
56
|
+
onAfterResize: reAnchorAfterPaint,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Trim the registry + clamp current page when the page count shrinks.
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
pageRegistry.current?.trim(normalizedPageCount);
|
|
62
|
+
setCurrentPageIndex((idx) => clampReaderPageIndex(idx, normalizedPageCount));
|
|
63
|
+
}, [normalizedPageCount]);
|
|
64
|
+
|
|
65
|
+
// Drive currentPageIndex from visible pages. Suppress intermediates while a
|
|
66
|
+
// programmatic scroll is in flight.
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const stage = stageRef.current;
|
|
69
|
+
if (!stage) return undefined;
|
|
70
|
+
const observer = createPageVisibilityObserver(stage, (pageIndex) => {
|
|
71
|
+
if (pendingScrollTargetRef.current !== null) {
|
|
72
|
+
if (pageIndex !== pendingScrollTargetRef.current) return;
|
|
73
|
+
clearPendingScrollTarget();
|
|
74
|
+
}
|
|
75
|
+
setCurrentPageIndex((prev) => (prev === pageIndex ? prev : pageIndex));
|
|
76
|
+
});
|
|
77
|
+
if (!observer) return undefined;
|
|
78
|
+
pageRegistry.current?.refs.forEach((el) => el && observer.observe(el));
|
|
79
|
+
return () => observer.disconnect();
|
|
80
|
+
}, [clearPendingScrollTarget, normalizedPageCount, pageRegistrationVersion, pendingScrollTargetRef]);
|
|
81
|
+
|
|
82
|
+
// When refs change (initial mount, pagination kicks in), re-anchor the stage
|
|
83
|
+
// to the page we already believe we're on so scroll-snap mandatory doesn't
|
|
84
|
+
// pull us to whichever page is closest.
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const refs = pageRegistry.current?.refs ?? [];
|
|
87
|
+
const idx = currentPageIndexRef.current;
|
|
88
|
+
if (idx === 0) return;
|
|
89
|
+
if (!refs[idx]) return;
|
|
90
|
+
armPendingScrollTarget(idx);
|
|
91
|
+
scrollToPage(refs, idx, "instant", stageRef.current);
|
|
92
|
+
}, [armPendingScrollTarget, pageRegistrationVersion]);
|
|
93
|
+
|
|
94
|
+
const setPage = useCallback(
|
|
95
|
+
(pageIndex: number, options: SetPageOptions = {}) => {
|
|
96
|
+
const refs = pageRegistry.current?.refs ?? [];
|
|
97
|
+
const target = clampReaderPageIndex(pageIndex, normalizedPageCount);
|
|
98
|
+
armPendingScrollTarget(target);
|
|
99
|
+
setCurrentPageIndex(target);
|
|
100
|
+
scrollToPage(refs, target, options.behavior ?? "smooth", stageRef.current);
|
|
101
|
+
},
|
|
102
|
+
[armPendingScrollTarget, normalizedPageCount],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const nextPage = useCallback(() => {
|
|
106
|
+
setPage(currentPageIndexRef.current + 1);
|
|
107
|
+
}, [setPage]);
|
|
108
|
+
|
|
109
|
+
const prevPage = useCallback(() => {
|
|
110
|
+
setPage(currentPageIndexRef.current - 1);
|
|
111
|
+
}, [setPage]);
|
|
112
|
+
|
|
113
|
+
useReaderHashSync({
|
|
114
|
+
stageRef,
|
|
115
|
+
pageRefs,
|
|
116
|
+
currentPageIndex,
|
|
117
|
+
currentPageIndexRef,
|
|
118
|
+
normalizedPageCount,
|
|
119
|
+
setCurrentPageIndex,
|
|
120
|
+
armPendingScrollTarget,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
useReaderKeyboardNav({ nextPage, prevPage, setPage, normalizedPageCount });
|
|
124
|
+
|
|
125
|
+
const registerPage = useCallback<(pageIndex: number) => RefCallback<HTMLElement>>(
|
|
126
|
+
(pageIndex) => pageRegistry.current?.registerPage(pageIndex) ?? (() => undefined),
|
|
127
|
+
[],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const progressPercent =
|
|
131
|
+
normalizedPageCount <= 1 ? 100 : ((currentPageIndex + 1) / normalizedPageCount) * 100;
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
stageRef,
|
|
135
|
+
currentPageIndex,
|
|
136
|
+
currentPageLabel: formatReaderPageNumber(currentPageIndex + 1),
|
|
137
|
+
totalPageLabel: formatReaderPageNumber(normalizedPageCount),
|
|
138
|
+
progressPercent,
|
|
139
|
+
leftPanelOpen,
|
|
140
|
+
rightPanelOpen,
|
|
141
|
+
registerPage,
|
|
142
|
+
setPage,
|
|
143
|
+
toggleLeftPanel,
|
|
144
|
+
toggleRightPanel,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, type MutableRefObject } from "react";
|
|
2
|
+
import { scrollToPage } from "./readerScroll";
|
|
3
|
+
|
|
4
|
+
// Generous upper bound on a smooth scrollIntoView. If the target ref is gone or
|
|
5
|
+
// the browser never settles on it, clear the guard so the IO observer regains
|
|
6
|
+
// authority over currentPageIndex.
|
|
7
|
+
const PROGRAMMATIC_SCROLL_FALLBACK_MS = 2500;
|
|
8
|
+
|
|
9
|
+
export interface UseReaderScrollAnchorOptions {
|
|
10
|
+
stageRef: MutableRefObject<HTMLElement | null>;
|
|
11
|
+
pageRefs: MutableRefObject<Array<HTMLElement | null>>;
|
|
12
|
+
currentPageIndexRef: MutableRefObject<number>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ReaderScrollAnchor {
|
|
16
|
+
pendingScrollTargetRef: MutableRefObject<number | null>;
|
|
17
|
+
armPendingScrollTarget: (target: number) => void;
|
|
18
|
+
clearPendingScrollTarget: () => void;
|
|
19
|
+
reAnchorAfterPaint: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useReaderScrollAnchor({
|
|
23
|
+
stageRef,
|
|
24
|
+
pageRefs,
|
|
25
|
+
currentPageIndexRef,
|
|
26
|
+
}: UseReaderScrollAnchorOptions): ReaderScrollAnchor {
|
|
27
|
+
// While a programmatic scroll is in flight, the IntersectionObserver should
|
|
28
|
+
// only accept the destination page (not the intermediates we sweep past).
|
|
29
|
+
const pendingScrollTargetRef = useRef<number | null>(null);
|
|
30
|
+
const pendingScrollClearTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
31
|
+
|
|
32
|
+
const armPendingScrollTarget = useCallback((target: number) => {
|
|
33
|
+
pendingScrollTargetRef.current = target;
|
|
34
|
+
if (pendingScrollClearTimerRef.current !== null) clearTimeout(pendingScrollClearTimerRef.current);
|
|
35
|
+
pendingScrollClearTimerRef.current = setTimeout(() => {
|
|
36
|
+
pendingScrollTargetRef.current = null;
|
|
37
|
+
pendingScrollClearTimerRef.current = null;
|
|
38
|
+
}, PROGRAMMATIC_SCROLL_FALLBACK_MS);
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
const clearPendingScrollTarget = useCallback(() => {
|
|
42
|
+
pendingScrollTargetRef.current = null;
|
|
43
|
+
if (pendingScrollClearTimerRef.current !== null) {
|
|
44
|
+
clearTimeout(pendingScrollClearTimerRef.current);
|
|
45
|
+
pendingScrollClearTimerRef.current = null;
|
|
46
|
+
}
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
useEffect(() => () => clearPendingScrollTarget(), [clearPendingScrollTarget]);
|
|
50
|
+
|
|
51
|
+
// Re-anchor the stage to the page we already believe we're on. scroll-snap
|
|
52
|
+
// mandatory would otherwise snap to whichever page is closest after a layout
|
|
53
|
+
// change. Pin to the active programmatic target if there is one.
|
|
54
|
+
const reAnchorAfterPaint = useCallback(() => {
|
|
55
|
+
if (typeof window === "undefined") return;
|
|
56
|
+
window.requestAnimationFrame(() => {
|
|
57
|
+
const refs = pageRefs.current;
|
|
58
|
+
const target = pendingScrollTargetRef.current ?? currentPageIndexRef.current;
|
|
59
|
+
scrollToPage(refs, target, "instant", stageRef.current);
|
|
60
|
+
});
|
|
61
|
+
}, [currentPageIndexRef, pageRefs, stageRef]);
|
|
62
|
+
|
|
63
|
+
return { pendingScrollTargetRef, armPendingScrollTarget, clearPendingScrollTarget, reAnchorAfterPaint };
|
|
64
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef } from "react";
|
|
2
|
+
import { cn } from "../core/cn";
|
|
3
|
+
|
|
4
|
+
type PanelProps = ComponentPropsWithoutRef<"section">;
|
|
5
|
+
type PanelHeaderProps = ComponentPropsWithoutRef<"header">;
|
|
6
|
+
type PanelDivProps = ComponentPropsWithoutRef<"div">;
|
|
7
|
+
type PanelTextProps = ComponentPropsWithoutRef<"p">;
|
|
8
|
+
type PanelTitleProps = ComponentPropsWithoutRef<"h2">;
|
|
9
|
+
type PanelSectionTitleProps = ComponentPropsWithoutRef<"h3">;
|
|
10
|
+
type PanelButtonProps = ComponentPropsWithoutRef<"button">;
|
|
11
|
+
|
|
12
|
+
function PanelRoot({ className, ...props }: PanelProps) {
|
|
13
|
+
return <section {...props} className={cn("openpress-panel", className)} />;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function PanelHeader({ className, ...props }: PanelHeaderProps) {
|
|
17
|
+
return <header {...props} className={cn("openpress-panel-header", className)} />;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function PanelKicker({ className, ...props }: ComponentPropsWithoutRef<"span">) {
|
|
21
|
+
return <span {...props} className={cn("openpress-panel-kicker", className)} />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function PanelTitle({ className, ...props }: PanelTitleProps) {
|
|
25
|
+
return <h2 {...props} className={cn("openpress-panel-title", className)} />;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function PanelDescription({ className, ...props }: PanelTextProps) {
|
|
29
|
+
return <p {...props} className={cn("openpress-panel-description", className)} />;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function PanelActions({ className, ...props }: PanelDivProps) {
|
|
33
|
+
return <div {...props} className={cn("openpress-panel-actions", className)} />;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function PanelActionButton({ className, ...props }: PanelButtonProps) {
|
|
37
|
+
return <button {...props} className={cn("openpress-panel-action-button", className)} />;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function PanelBody({ className, ...props }: PanelDivProps) {
|
|
41
|
+
return <div {...props} className={cn("openpress-panel-body", className)} />;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function PanelSection({ className, ...props }: PanelProps) {
|
|
45
|
+
return <section {...props} className={cn("openpress-panel-section", className)} />;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function PanelSectionTitle({ className, ...props }: PanelSectionTitleProps) {
|
|
49
|
+
return <h3 {...props} className={cn("openpress-panel-section-title", className)} />;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function PanelSectionDescription({ className, ...props }: PanelTextProps) {
|
|
53
|
+
return <p {...props} className={cn("openpress-panel-section-description", className)} />;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function PanelEmpty({ className, ...props }: PanelDivProps) {
|
|
57
|
+
return <div {...props} className={cn("openpress-panel-empty", className)} />;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function PanelError({ className, role = "alert", ...props }: PanelTextProps) {
|
|
61
|
+
return <p {...props} role={role} className={cn("openpress-panel-error", className)} />;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const Panel = Object.assign(PanelRoot, {
|
|
65
|
+
Header: PanelHeader,
|
|
66
|
+
Kicker: PanelKicker,
|
|
67
|
+
Title: PanelTitle,
|
|
68
|
+
Description: PanelDescription,
|
|
69
|
+
Actions: PanelActions,
|
|
70
|
+
ActionButton: PanelActionButton,
|
|
71
|
+
Body: PanelBody,
|
|
72
|
+
Section: PanelSection,
|
|
73
|
+
SectionTitle: PanelSectionTitle,
|
|
74
|
+
SectionDescription: PanelSectionDescription,
|
|
75
|
+
Empty: PanelEmpty,
|
|
76
|
+
Error: PanelError,
|
|
77
|
+
});
|
|
@@ -9,14 +9,3 @@ export function isWorkspaceModeLocation(location: Pick<Location, "hostname" | "s
|
|
|
9
9
|
export function isPrintModeLocation(location: Pick<Location, "search">) {
|
|
10
10
|
return new URLSearchParams(location.search).has("print");
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
export function buildPublicPreviewHref(currentHref: string, pageIndex?: number) {
|
|
14
|
-
const url = new URL(currentHref);
|
|
15
|
-
url.searchParams.delete("dev");
|
|
16
|
-
url.searchParams.delete("workspace");
|
|
17
|
-
url.searchParams.delete("fontPreview");
|
|
18
|
-
if (typeof pageIndex === "number") {
|
|
19
|
-
url.hash = `page-${String(pageIndex + 1).padStart(2, "0")}`;
|
|
20
|
-
}
|
|
21
|
-
return url.toString();
|
|
22
|
-
}
|