@open-press/core 0.7.0 → 0.8.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.
Files changed (116) hide show
  1. package/engine/commands/dev.mjs +2 -2
  2. package/engine/commands/upgrade.mjs +47 -5
  3. package/engine/output/chrome-pdf.mjs +18 -3
  4. package/engine/output/static-server.mjs +39 -0
  5. package/engine/react/comment-endpoint.mjs +13 -39
  6. package/engine/react/comment-marker.mjs +30 -6
  7. package/engine/react/document-entry.mjs +11 -0
  8. package/engine/react/document-export.mjs +45 -5
  9. package/engine/react/http-json.mjs +24 -0
  10. package/engine/react/mdx-compile.mjs +187 -3
  11. package/engine/react/measurement-css.mjs +93 -1
  12. package/engine/react/object-entities.mjs +119 -0
  13. package/engine/react/pipeline/allocate.mjs +10 -7
  14. package/engine/react/pipeline/frame-measurement.mjs +40 -9
  15. package/engine/react/project-asset-endpoint.mjs +6 -24
  16. package/engine/react/source-edit-endpoint.d.mts +10 -0
  17. package/engine/react/source-edit-endpoint.mjs +75 -0
  18. package/engine/react/sources/mdx-resolver.mjs +12 -14
  19. package/engine/react/style-discovery.mjs +1 -4
  20. package/engine/runtime/file-walk.mjs +22 -0
  21. package/engine/runtime/inspection.mjs +1 -20
  22. package/engine/runtime/path-utils.mjs +20 -0
  23. package/engine/runtime/source-text-tools.d.mts +102 -0
  24. package/engine/runtime/source-text-tools.mjs +551 -16
  25. package/engine/runtime/source-workspace.mjs +4 -31
  26. package/package.json +1 -1
  27. package/src/openpress/{App.tsx → app/OpenPressApp.tsx} +25 -12
  28. package/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +10 -7
  29. package/src/openpress/app/index.ts +2 -0
  30. package/src/openpress/core/Frame.tsx +9 -11
  31. package/src/openpress/core/FrameContext.tsx +8 -3
  32. package/src/openpress/core/MdxArea.tsx +11 -12
  33. package/src/openpress/core/cn.ts +4 -0
  34. package/src/openpress/core/index.tsx +2 -1
  35. package/src/openpress/core/primitives.tsx +29 -8
  36. package/src/openpress/core/types.ts +8 -0
  37. package/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
  38. package/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
  39. package/src/openpress/{types.ts → document-model/documentTypes.ts} +42 -0
  40. package/src/openpress/document-model/index.ts +6 -0
  41. package/src/openpress/document-model/objectEntityModel.ts +51 -0
  42. package/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
  43. package/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
  44. package/src/openpress/manuscript/index.tsx +49 -7
  45. package/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
  46. package/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
  47. package/src/openpress/reader/index.ts +10 -0
  48. package/src/openpress/reader/pageViewportScaleModel.ts +73 -0
  49. package/src/openpress/reader/readerTypes.ts +4 -0
  50. package/src/openpress/reader/usePageViewportScale.ts +119 -0
  51. package/src/openpress/reader/usePanelState.ts +56 -0
  52. package/src/openpress/reader/useReaderHashSync.ts +61 -0
  53. package/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
  54. package/src/openpress/reader/useReaderRuntime.ts +146 -0
  55. package/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
  56. package/src/openpress/shared/Panel.tsx +77 -0
  57. package/src/openpress/shared/index.ts +4 -0
  58. package/src/openpress/shared/numberUtils.ts +3 -0
  59. package/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
  60. package/src/openpress/workbench/Workbench.tsx +407 -0
  61. package/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
  62. package/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
  63. package/src/openpress/workbench/actions/SearchControl.tsx +345 -0
  64. package/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
  65. package/src/openpress/workbench/actions/index.ts +5 -0
  66. package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
  67. package/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
  68. package/src/openpress/workbench/dialog/index.ts +1 -0
  69. package/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
  70. package/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
  71. package/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
  72. package/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
  73. package/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
  74. package/src/openpress/workbench/document/index.ts +10 -0
  75. package/src/openpress/workbench/index.ts +2 -0
  76. package/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
  77. package/src/openpress/workbench/inspector/index.ts +5 -0
  78. package/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
  79. package/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
  80. package/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
  81. package/src/openpress/workbench/inspector/useInspectorComments.ts +248 -0
  82. package/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
  83. package/src/openpress/workbench/mentions/index.ts +2 -0
  84. package/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
  85. package/src/openpress/workbench/panels/Panel.tsx +1 -0
  86. package/src/openpress/workbench/panels/PendingCommentsPanel.tsx +76 -0
  87. package/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
  88. package/src/openpress/workbench/panels/index.ts +3 -0
  89. package/src/openpress/workbench/project/ProjectEntryPanel.tsx +523 -0
  90. package/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
  91. package/src/openpress/workbench/project/index.ts +2 -0
  92. package/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
  93. package/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
  94. package/src/openpress/workbench/shell/index.ts +1 -0
  95. package/src/openpress/workbench/workbenchFormatters.ts +120 -0
  96. package/src/openpress/workbench/workbenchTypes.ts +35 -0
  97. package/src/styles/openpress/print-route.css +0 -2
  98. package/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
  99. package/src/styles/openpress/public-viewer.css +25 -320
  100. package/src/styles/openpress/reader-runtime.css +243 -55
  101. package/src/styles/openpress/responsive.css +145 -270
  102. package/src/styles/openpress/workbench-panels.css +214 -178
  103. package/src/styles/openpress/workbench.css +986 -451
  104. package/src/styles/openpress.css +1 -1
  105. package/vite.config.ts +50 -0
  106. package/src/openpress/inspector.ts +0 -282
  107. package/src/openpress/projectWorkspace.tsx +0 -919
  108. package/src/openpress/readerRuntime.ts +0 -230
  109. package/src/openpress/workbench.tsx +0 -1265
  110. package/src/openpress/workbenchTypes.ts +0 -4
  111. /package/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
  112. /package/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
  113. /package/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
  114. /package/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
  115. /package/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
  116. /package/src/openpress/{projectSources.ts → workbench/project/projectSourceModel.ts} +0 -0
@@ -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
+ });
@@ -0,0 +1,4 @@
1
+ export * from "./frameScheduler";
2
+ export * from "./numberUtils";
3
+ export * from "./Panel";
4
+ export * from "./runtimeMode";
@@ -0,0 +1,3 @@
1
+ export function clampNumber(value: number, min: number, max: number) {
2
+ return Math.min(Math.max(value, min), Math.max(min, max));
3
+ }
@@ -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
- }