@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
@@ -1,7 +1,7 @@
1
- import { useEffect, useState } from "react";
2
- import { Renderer } from "./renderer";
3
- import { isLocalWorkspaceHost } from "./runtimeMode";
4
- import type { DeploymentInfo, ReaderDocument } from "./types";
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { OpenPressRuntime } from "./OpenPressRuntime";
3
+ import { isLocalWorkspaceHost } from "../shared";
4
+ import type { DeploymentInfo, ReaderDocument } from "../document-model";
5
5
 
6
6
  type LoadState =
7
7
  | { status: "loading" }
@@ -39,22 +39,26 @@ function LoadingScreen() {
39
39
  );
40
40
  }
41
41
 
42
- export function App() {
42
+ export function OpenPressApp() {
43
43
  const [state, setState] = useState<LoadState>({ status: "loading" });
44
44
 
45
+ const refreshDocument = useCallback(async () => {
46
+ const document = await loadReaderDocument();
47
+ setState((current) => {
48
+ if (current.status !== "ready") return current;
49
+ return { ...current, document };
50
+ });
51
+ }, []);
52
+
45
53
  useEffect(() => {
46
54
  let cancelled = false;
47
55
 
48
56
  async function loadDocument() {
49
57
  try {
50
- const [response, deploymentInfo] = await Promise.all([
51
- fetch("/openpress/document.json", { cache: "no-store" }),
58
+ const [document, deploymentInfo] = await Promise.all([
59
+ loadReaderDocument(),
52
60
  loadDeploymentInfo(),
53
61
  ]);
54
- if (!response.ok) {
55
- throw new Error(`Unable to load /openpress/document.json (${response.status})`);
56
- }
57
- const document = (await response.json()) as ReaderDocument;
58
62
  if (!cancelled) {
59
63
  setState({ status: "ready", document, deploymentInfo });
60
64
  }
@@ -81,13 +85,22 @@ export function App() {
81
85
  }
82
86
 
83
87
  return (
84
- <Renderer
88
+ <OpenPressRuntime
85
89
  document={state.document}
86
90
  deploymentInfo={state.deploymentInfo}
91
+ onDocumentRefresh={refreshDocument}
87
92
  />
88
93
  );
89
94
  }
90
95
 
96
+ async function loadReaderDocument(): Promise<ReaderDocument> {
97
+ const response = await fetch("/openpress/document.json", { cache: "no-store" });
98
+ if (!response.ok) {
99
+ throw new Error(`Unable to load /openpress/document.json (${response.status})`);
100
+ }
101
+ return (await response.json()) as ReaderDocument;
102
+ }
103
+
91
104
  async function loadDeploymentInfo(): Promise<DeploymentInfo> {
92
105
  if (typeof window !== "undefined" && isLocalWorkspaceHost(window.location.hostname)) {
93
106
  const localInfo = await loadDeploymentInfoFrom("/__openpress/status");
@@ -1,23 +1,25 @@
1
1
  import { useMemo, type CSSProperties } from "react";
2
- import { PrintDocument, PublicViewer } from "./publicPage";
3
- import { isPrintModeLocation, isWorkspaceModeLocation } from "./runtimeMode";
4
- import { HtmlWorkbench } from "./workbench";
2
+ import { PrintDocument, PublicViewer } from "../reader";
3
+ import { isPrintModeLocation, isWorkspaceModeLocation } from "../shared";
4
+ import { HtmlWorkbench } from "../workbench";
5
5
  import type {
6
6
  DeploymentInfo,
7
7
  ReaderDocument,
8
8
  HtmlPageBlock,
9
9
  Theme,
10
- } from "./types";
10
+ } from "../document-model";
11
11
 
12
- interface RendererProps {
12
+ interface OpenPressRuntimeProps {
13
13
  document: ReaderDocument;
14
14
  deploymentInfo?: DeploymentInfo;
15
+ onDocumentRefresh?: () => void | Promise<void>;
15
16
  }
16
17
 
17
- export function Renderer({
18
+ export function OpenPressRuntime({
18
19
  document,
19
20
  deploymentInfo = { online: false },
20
- }: RendererProps) {
21
+ onDocumentRefresh,
22
+ }: OpenPressRuntimeProps) {
21
23
  const style = themeToCssVariables(document.theme);
22
24
  const htmlPages = document.blocks.filter((block): block is HtmlPageBlock => block.kind === "htmlPage");
23
25
  const workspaceMode = useMemo(() => {
@@ -45,6 +47,7 @@ export function Renderer({
45
47
  style={style}
46
48
  devMode={workspaceMode}
47
49
  deploymentInfo={deploymentInfo}
50
+ onDocumentRefresh={onDocumentRefresh}
48
51
  />
49
52
  );
50
53
  }
@@ -0,0 +1,2 @@
1
+ export { OpenPressApp } from "./OpenPressApp";
2
+ export { OpenPressRuntime } from "./OpenPressRuntime";
@@ -1,18 +1,15 @@
1
- import { useContext, type ReactNode } from "react";
1
+ import { useContext } from "react";
2
+ import { cn } from "./cn";
2
3
  import { FrameContext, type FrameContextValue } from "./FrameContext";
3
4
  import { PressContext } from "./Press";
4
5
  import type { FrameProps } from "./types";
6
+ import { createFrameObjectEntityId } from "../document-model/objectEntityModel";
5
7
 
6
8
  // Substring reserved for the overflow extension pipeline.
7
9
  const RESERVED_EXTENDED = ":extended:";
8
10
 
9
11
  export const FRAME_MARKER: unique symbol = Symbol.for("@open-press/core:Frame");
10
12
 
11
- function classNames(...values: Array<string | undefined>) {
12
- const joined = values.filter(Boolean).join(" ");
13
- return joined.length > 0 ? joined : undefined;
14
- }
15
-
16
13
  export function Frame({
17
14
  frameKey,
18
15
  role,
@@ -40,13 +37,13 @@ export function Frame({
40
37
  const areaCounts: Record<string, number> = {};
41
38
  const frameContextValue: FrameContextValue = {
42
39
  frameKey: frameKey ?? "",
43
- consumeArea(chainId: string): ReactNode | null {
40
+ consumeArea(chainId: string) {
44
41
  const index = areaCounts[chainId] ?? 0;
45
42
  areaCounts[chainId] = index + 1;
46
- if (!frameAllocation) return null;
43
+ if (!frameAllocation) return { indexInFrame: index, blocks: null };
47
44
  const chainSlots = frameAllocation[chainId];
48
- if (!chainSlots) return null;
49
- return chainSlots[index] ?? null;
45
+ if (!chainSlots) return { indexInFrame: index, blocks: null };
46
+ return { indexInFrame: index, blocks: chainSlots[index] ?? null };
50
47
  },
51
48
  };
52
49
 
@@ -56,8 +53,9 @@ export function Frame({
56
53
  <FrameContext.Provider value={frameContextValue}>
57
54
  <section
58
55
  {...(rest as Record<string, unknown>)}
59
- className={classNames("reader-page", className)}
56
+ className={cn("reader-page", className)}
60
57
  data-openpress-frame-key={frameKey}
58
+ data-openpress-object-id={createFrameObjectEntityId(frameKey)}
61
59
  data-frame-role={role}
62
60
  data-page-kind={pageKind}
63
61
  data-frame-chrome={chrome ? "true" : "false"}
@@ -9,11 +9,16 @@ import { createContext, type ReactNode } from "react";
9
9
  // and so on. Empty Frames (no allocation) return null, which renders the
10
10
  // MdxArea as a measurement placeholder.
11
11
 
12
+ export interface ConsumedMdxArea {
13
+ indexInFrame: number;
14
+ // Null when the frame has no allocation (measurement pass) or no blocks
15
+ // for this chain at the claimed index.
16
+ blocks: ReactNode | null;
17
+ }
18
+
12
19
  export interface FrameContextValue {
13
20
  frameKey: string;
14
- // Consume the next allocation slot for this chainId. Returns null if the
15
- // frame has no allocation (measurement pass) or no blocks for this chain.
16
- consumeArea(chainId: string): ReactNode | null;
21
+ consumeArea(chainId: string): ConsumedMdxArea;
17
22
  }
18
23
 
19
24
  export const FrameContext = createContext<FrameContextValue | null>(null);
@@ -1,11 +1,8 @@
1
1
  import { useContext, type ReactNode } from "react";
2
+ import { cn } from "./cn";
2
3
  import { FrameContext } from "./FrameContext";
3
4
  import type { MdxAreaProps } from "./types";
4
-
5
- function classNames(...values: Array<string | undefined>) {
6
- const joined = values.filter(Boolean).join(" ");
7
- return joined.length > 0 ? joined : undefined;
8
- }
5
+ import { createMdxAreaObjectEntityId } from "../document-model/objectEntityModel";
9
6
 
10
7
  export function MdxArea({
11
8
  chainId,
@@ -14,20 +11,22 @@ export function MdxArea({
14
11
  ...rest
15
12
  }: MdxAreaProps) {
16
13
  const frame = useContext(FrameContext);
17
-
18
- let blocks: ReactNode | null = null;
19
- if (frame) {
20
- blocks = frame.consumeArea(chainId);
21
- }
14
+ const consumed = frame?.consumeArea(chainId) ?? null;
15
+ const blocks: ReactNode | null = consumed?.blocks ?? null;
16
+ const objectId = frame && consumed
17
+ ? createMdxAreaObjectEntityId(frame.frameKey, chainId, consumed.indexInFrame)
18
+ : undefined;
22
19
 
23
20
  return (
24
21
  <div
25
22
  {...(rest as Record<string, unknown>)}
26
- className={classNames("openpress-mdx-area", className)}
23
+ className={cn("openpress-mdx-area", className)}
27
24
  data-openpress-mdx-area="true"
28
25
  data-openpress-mdx-area-chain={chainId}
26
+ data-openpress-mdx-area-index={consumed?.indexInFrame}
27
+ data-openpress-object-id={objectId}
29
28
  data-openpress-mdx-area-overflow={overflow}
30
- data-openpress-mdx-area-empty={blocks == null ? "true" : undefined}
29
+ data-openpress-mdx-area-empty={blocks == null ? "true" : "false"}
31
30
  >
32
31
  {blocks}
33
32
  </div>
@@ -0,0 +1,4 @@
1
+ export function cn(...values: Array<string | false | null | undefined>) {
2
+ const joined = values.filter(Boolean).join(" ");
3
+ return joined.length > 0 ? joined : undefined;
4
+ }
@@ -10,7 +10,7 @@ export { Frame, FRAME_MARKER } from "./Frame";
10
10
  export { FrameContext } from "./FrameContext";
11
11
  export { MdxArea } from "./MdxArea";
12
12
  export { useSource } from "./useSource";
13
- export { BaseFigure, BaseCallout } from "./primitives";
13
+ export { BaseFigure, BaseCallout, MediaFigure, ImageFigure } from "./primitives";
14
14
 
15
15
  export type {
16
16
  FrameProps,
@@ -19,6 +19,7 @@ export type {
19
19
  MdxAreaOverflow,
20
20
  PressProps,
21
21
  BaseFigureProps,
22
+ MediaFigureProps,
22
23
  BaseCalloutKind,
23
24
  BaseCalloutProps,
24
25
  Manifest,
@@ -1,13 +1,9 @@
1
- import type { BaseCalloutProps, BaseFigureProps } from "./types";
2
-
3
- function classNames(...values: Array<string | undefined>) {
4
- const joined = values.filter(Boolean).join(" ");
5
- return joined.length > 0 ? joined : undefined;
6
- }
1
+ import { cn } from "./cn";
2
+ import type { BaseCalloutProps, BaseFigureProps, MediaFigureProps } from "./types";
7
3
 
8
4
  export function BaseFigure({ caption, className, children, ...figureProps }: BaseFigureProps) {
9
5
  return (
10
- <figure {...figureProps} className={classNames("openpress-figure", className)}>
6
+ <figure {...figureProps} className={cn("openpress-figure", className)}>
11
7
  <div data-figure-body>{children}</div>
12
8
  {caption === undefined ? null : <figcaption>{caption}</figcaption>}
13
9
  </figure>
@@ -16,8 +12,33 @@ export function BaseFigure({ caption, className, children, ...figureProps }: Bas
16
12
 
17
13
  export function BaseCallout({ kind = "info", className, children, ...calloutProps }: BaseCalloutProps) {
18
14
  return (
19
- <aside {...calloutProps} className={classNames("openpress-callout", className)} data-callout-kind={kind}>
15
+ <aside {...calloutProps} className={cn("openpress-callout", className)} data-callout-kind={kind}>
20
16
  {children}
21
17
  </aside>
22
18
  );
23
19
  }
20
+
21
+ export function MediaFigure({
22
+ src,
23
+ alt,
24
+ caption,
25
+ className,
26
+ imgClassName,
27
+ loading = "eager",
28
+ ...figureProps
29
+ }: MediaFigureProps) {
30
+ return (
31
+ <BaseFigure {...figureProps} className={cn("openpress-media-figure", className)} caption={caption}>
32
+ <img src={resolveMediaSrc(src)} alt={alt} loading={loading} className={imgClassName} />
33
+ </BaseFigure>
34
+ );
35
+ }
36
+
37
+ export const ImageFigure = MediaFigure;
38
+
39
+ function resolveMediaSrc(src: string) {
40
+ const trimmed = String(src ?? "").trim();
41
+ if (!trimmed) return "";
42
+ if (/^(?:[a-z][a-z0-9+.-]*:|\/)/i.test(trimmed)) return trimmed;
43
+ return `/openpress/media/${trimmed.replace(/^\.?\/*/, "")}`;
44
+ }
@@ -38,6 +38,14 @@ export type BaseFigureProps = Omit<HTMLAttributes<HTMLElement>, "children"> & {
38
38
  children: ReactNode;
39
39
  };
40
40
 
41
+ export type MediaFigureProps = Omit<HTMLAttributes<HTMLElement>, "children"> & {
42
+ src: string;
43
+ alt: string;
44
+ caption: ReactNode;
45
+ imgClassName?: string;
46
+ loading?: "eager" | "lazy";
47
+ };
48
+
41
49
  export type BaseCalloutKind = "info" | "warn" | "success" | "error" | (string & {});
42
50
 
43
51
  export type BaseCalloutProps = Omit<HTMLAttributes<HTMLElement>, "children"> & {
@@ -3,7 +3,7 @@
3
3
  // `PublicPage` / `HtmlWorkbench` HMR-clean (Fast Refresh expects component
4
4
  // files to export only components).
5
5
 
6
- import type { DisplayPage } from "./workbenchTypes";
6
+ import type { DisplayPage } from "../reader";
7
7
 
8
8
  export function createAnchorPageMap(pages: DisplayPage[]) {
9
9
  const map = new Map<string, number>();
@@ -1,4 +1,4 @@
1
- import type { BlockSource } from "./types";
1
+ import type { BlockSource } from "./documentTypes";
2
2
 
3
3
  export type MediaAssetKind = "image" | "svg";
4
4
 
@@ -25,6 +25,7 @@ export interface DocumentSource {
25
25
  editMode?: string;
26
26
  styles?: DocumentStyle[];
27
27
  blockMap?: Record<string, SourceBlock>;
28
+ objectEntities?: Record<string, ObjectEntity>;
28
29
  }
29
30
 
30
31
  export interface DocumentStyle {
@@ -49,6 +50,9 @@ export interface SourceBlock {
49
50
  pageIndex?: number;
50
51
  pageNumber?: number;
51
52
  source?: SourceLocation;
53
+ frameKey?: string;
54
+ chainId?: string;
55
+ sectionSlug?: string;
52
56
  }
53
57
 
54
58
  export interface DocumentMeta {
@@ -93,4 +97,42 @@ export interface HtmlPageBlock {
93
97
  anchors?: string[];
94
98
  className?: string;
95
99
  source?: BlockSource;
100
+ frameKey?: string;
101
+ role?: string | null;
102
+ chrome?: boolean;
103
+ blockIds?: string[];
104
+ }
105
+
106
+ export type ObjectEntityKind =
107
+ | "page"
108
+ | "frame"
109
+ | "mdx-area"
110
+ | "mdx-block"
111
+ | "component"
112
+ | "media";
113
+
114
+ export interface EditableSourceRef {
115
+ path: string;
116
+ file?: string;
117
+ source?: SourceLocation;
118
+ line?: number;
119
+ column?: number;
120
+ }
121
+
122
+ export interface ObjectEntityRef {
123
+ id: string;
124
+ kind: ObjectEntityKind;
125
+ }
126
+
127
+ export interface ObjectEntity {
128
+ id: string;
129
+ kind: ObjectEntityKind;
130
+ label: string;
131
+ parentId?: string;
132
+ pageId?: string;
133
+ blockId?: string;
134
+ frameKey?: string;
135
+ chainId?: string;
136
+ source?: EditableSourceRef;
137
+ metadata?: Record<string, string | number | boolean | null>;
96
138
  }
@@ -0,0 +1,6 @@
1
+ export * from "./anchorMapModel";
2
+ export * from "./documentIndexes";
3
+ export * from "./documentTypes";
4
+ export * from "./objectEntityModel";
5
+ export * from "./projectIdentityModel";
6
+ export * from "./reactDocumentMetadataModel";
@@ -0,0 +1,51 @@
1
+ import type { ObjectEntity, ObjectEntityKind, ReaderDocument, SourceBlock } from "./documentTypes";
2
+
3
+ export function createObjectEntityId(kind: ObjectEntityKind, ...parts: Array<string | number>) {
4
+ return [kind, ...parts.map((part) => encodeURIComponent(String(part)))].join(":");
5
+ }
6
+
7
+ export function createBlockObjectEntityId(blockId: string) {
8
+ return createObjectEntityId("mdx-block", blockId);
9
+ }
10
+
11
+ export function createFrameObjectEntityId(frameKey: string) {
12
+ return createObjectEntityId("frame", frameKey);
13
+ }
14
+
15
+ export function createPageObjectEntityId(frameKey: string) {
16
+ return createObjectEntityId("page", frameKey);
17
+ }
18
+
19
+ export function createMdxAreaObjectEntityId(frameKey: string, chainId: string, indexInFrame: number) {
20
+ return createObjectEntityId("mdx-area", frameKey, chainId, indexInFrame);
21
+ }
22
+
23
+ export function getObjectEntityMap(document: Pick<ReaderDocument, "source"> | null | undefined): Record<string, ObjectEntity> {
24
+ return document?.source?.objectEntities ?? {};
25
+ }
26
+
27
+ export function getObjectEntity(document: Pick<ReaderDocument, "source"> | null | undefined, objectId: string): ObjectEntity | null {
28
+ return getObjectEntityMap(document)[objectId] ?? null;
29
+ }
30
+
31
+ export function sourceBlockToObjectEntity(block: SourceBlock): ObjectEntity {
32
+ return {
33
+ id: createBlockObjectEntityId(block.id),
34
+ kind: "mdx-block",
35
+ label: block.name ? `${block.name} ${block.id}` : block.id,
36
+ blockId: block.id,
37
+ frameKey: block.frameKey,
38
+ chainId: block.chainId,
39
+ pageId: block.frameKey ? createPageObjectEntityId(block.frameKey) : undefined,
40
+ source: {
41
+ path: block.path,
42
+ source: block.source,
43
+ line: block.source?.line,
44
+ column: block.source?.column,
45
+ },
46
+ metadata: {
47
+ blockKind: block.kind ?? null,
48
+ sectionSlug: block.sectionSlug ?? block.chapterSlug ?? null,
49
+ },
50
+ };
51
+ }
@@ -1,4 +1,4 @@
1
- import type { DocumentMeta } from "./types";
1
+ import type { DocumentMeta } from "./documentTypes";
2
2
 
3
3
  export interface ProjectIdentity {
4
4
  name: string;
@@ -1,7 +1,7 @@
1
1
  import type {
2
2
  ReaderDocument,
3
3
  SourceBlock,
4
- } from "./types";
4
+ } from "./documentTypes";
5
5
 
6
6
  export const PRESS_TREE_MDX_SOURCE_TYPE = "openpress-press-tree-mdx";
7
7
 
@@ -9,9 +9,10 @@
9
9
  // document type that wants section flow imports from here; documents that
10
10
  // do not (slides, folios, calendars) skip this module entirely.
11
11
 
12
- import { Fragment, useContext, type ReactNode } from "react";
13
- import { Frame, FrameContext, PressContext, useSource } from "../core";
12
+ import { Fragment, useContext, type ComponentType, type ReactNode } from "react";
13
+ import { Frame, FrameContext, MdxArea, PressContext, useSource } from "../core";
14
14
  import type { MdxAreaOverflow, ResolvedSource } from "../core";
15
+ import { createMdxAreaObjectEntityId } from "../document-model/objectEntityModel";
15
16
 
16
17
  // ---------------------------------------------------------------------------
17
18
  // <Sections>
@@ -38,11 +39,11 @@ export interface SectionsOpenerProps {
38
39
 
39
40
  export interface SectionsProps {
40
41
  source: string;
41
- page: React.ComponentType<SectionsPageProps>;
42
- opener?: React.ComponentType<SectionsOpenerProps>;
42
+ page?: ComponentType<SectionsPageProps>;
43
+ opener?: ComponentType<SectionsOpenerProps>;
43
44
  }
44
45
 
45
- export function Sections({ source: sourceId, page: Page, opener: Opener }: SectionsProps) {
46
+ export function Sections({ source: sourceId, page: Page = DefaultSectionPage, opener: Opener }: SectionsProps) {
46
47
  const source = useSource(sourceId);
47
48
  const press = useContext(PressContext);
48
49
  const hints = press?.hints ?? null;
@@ -92,6 +93,41 @@ export function Sections({ source: sourceId, page: Page, opener: Opener }: Secti
92
93
  export const Chapters = Sections;
93
94
  export type ChaptersProps = SectionsProps;
94
95
 
96
+ export function DefaultSectionPage({
97
+ frameKey,
98
+ chainId,
99
+ pageIndex,
100
+ totalPages,
101
+ sectionSlug,
102
+ sectionTitle,
103
+ sectionTone,
104
+ }: SectionsPageProps) {
105
+ return (
106
+ <Frame
107
+ frameKey={frameKey}
108
+ role="manuscript.content"
109
+ className="reader-page--content"
110
+ data-page-index={pageIndex}
111
+ data-total-pages={totalPages}
112
+ data-section-id={sectionSlug}
113
+ data-chapter-tone={sectionTone}
114
+ >
115
+ <div className="page-frame">
116
+ <header className="page-header" aria-hidden="true" />
117
+ <main className="page-body">
118
+ <MdxArea chainId={chainId} />
119
+ </main>
120
+ <footer className="page-footer" aria-hidden="true">
121
+ <span className="footer-left">{sectionTitle}</span>
122
+ <span className="footer-right">
123
+ {totalPages > 1 ? `${pageIndex + 1}/${totalPages}` : pageIndex + 1}
124
+ </span>
125
+ </footer>
126
+ </div>
127
+ </Frame>
128
+ );
129
+ }
130
+
95
131
  // ---------------------------------------------------------------------------
96
132
  // <Toc>
97
133
  // ---------------------------------------------------------------------------
@@ -178,15 +214,21 @@ function DefaultTocPage({ frameKey, chainId, pageIndex, totalPages, heading, cla
178
214
 
179
215
  export function TocArea({ chainId, maxLevel, overflow = "extend", className }: TocAreaProps) {
180
216
  const frame = useContext(FrameContext);
181
- const blocks = frame?.consumeArea(chainId) ?? null;
217
+ const consumed = frame?.consumeArea(chainId) ?? null;
218
+ const blocks = consumed?.blocks ?? null;
219
+ const objectId = frame && consumed
220
+ ? createMdxAreaObjectEntityId(frame.frameKey, chainId, consumed.indexInFrame)
221
+ : undefined;
182
222
  return (
183
223
  <div
184
224
  className="openpress-mdx-area openpress-toc-area"
185
225
  data-openpress-mdx-area="true"
186
226
  data-openpress-mdx-area-chain={chainId}
227
+ data-openpress-mdx-area-index={consumed?.indexInFrame}
228
+ data-openpress-object-id={objectId}
187
229
  data-openpress-toc-max-level={maxLevel}
188
230
  data-openpress-mdx-area-overflow={overflow}
189
- data-openpress-mdx-area-empty={blocks == null ? "true" : undefined}
231
+ data-openpress-mdx-area-empty={blocks == null ? "true" : "false"}
190
232
  >
191
233
  <ol className={["toc-list", className].filter(Boolean).join(" ") || undefined}>
192
234
  {blocks}