@open-press/core 1.2.1 → 1.3.1
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 +2 -2
- package/engine/commands/typecheck.mjs +1 -1
- package/engine/document-export.mjs +1 -1
- package/engine/output/page-block.mjs +11 -2
- package/engine/output/public-assets.mjs +41 -6
- package/engine/output/static-server.mjs +68 -15
- package/engine/react/caption-numbering.mjs +2 -2
- package/engine/react/comment-marker.mjs +1 -2
- package/engine/react/document-entry.mjs +64 -11
- package/engine/react/document-export.d.mts +6 -0
- package/engine/react/document-export.mjs +158 -28
- package/engine/react/mdx-compile.mjs +4 -4
- package/engine/react/measurement-css.mjs +3 -3
- package/engine/react/page-folio.mjs +37 -0
- package/engine/react/pagination/allocator.mjs +4 -4
- package/engine/react/pipeline/frame-measurement.mjs +34 -16
- package/engine/react/press-tree-inspection.mjs +43 -13
- package/engine/react/project-asset-endpoint.mjs +45 -11
- package/engine/react/sources/heading-numbering.mjs +2 -2
- package/engine/react/sources/mdx-resolver.mjs +3 -3
- package/engine/react/style-discovery.mjs +60 -11
- package/engine/react/text-source-transform.mjs +18 -4
- package/engine/runtime/config.mjs +22 -22
- package/engine/runtime/file-utils.mjs +57 -13
- package/engine/runtime/inspection.mjs +40 -15
- package/engine/runtime/page-geometry.mjs +6 -6
- package/engine/runtime/source-text-tools.mjs +28 -4
- package/engine/runtime/source-workspace.mjs +6 -9
- package/engine/runtime/validation.mjs +42 -24
- package/package.json +1 -1
- package/src/openpress/app/OpenPressApp.tsx +6 -15
- package/src/openpress/app/OpenPressRuntime.tsx +3 -3
- package/src/openpress/app/WorkspaceGalleryPage.tsx +1 -1
- package/src/openpress/core/PageFolio.tsx +115 -0
- package/src/openpress/core/Press.tsx +5 -10
- package/src/openpress/core/Slide.tsx +11 -0
- package/src/openpress/core/index.tsx +4 -0
- package/src/openpress/core/types.ts +21 -13
- package/src/openpress/core/useSource.ts +1 -1
- package/src/openpress/document-model/workspaceManifestModel.ts +4 -9
- package/vite.config.ts +82 -16
|
@@ -451,10 +451,13 @@ function applySourceBlockTableCellEditToText(documentText, {
|
|
|
451
451
|
export async function collectSourceTextFiles(config, { scope = "content" } = {}) {
|
|
452
452
|
const roots = await sourceRoots(config, scope);
|
|
453
453
|
const files = [];
|
|
454
|
+
const seen = new Set();
|
|
454
455
|
for (const rootInfo of roots) {
|
|
455
456
|
const visit = async (absolutePath) => {
|
|
457
|
+
if (seen.has(absolutePath)) return;
|
|
456
458
|
const extension = path.extname(absolutePath);
|
|
457
459
|
if (!rootInfo.extensions.has(extension)) return;
|
|
460
|
+
seen.add(absolutePath);
|
|
458
461
|
const relativePath = path.relative(config.root, absolutePath).replaceAll("\\", "/");
|
|
459
462
|
files.push({
|
|
460
463
|
scope: rootInfo.scope,
|
|
@@ -557,18 +560,30 @@ async function sourceRoots(config, scope) {
|
|
|
557
560
|
const roots = [
|
|
558
561
|
...contentRoots,
|
|
559
562
|
{ scope: "design-doc", kind: "file", absolutePath: sourceConfig.paths.designDoc, extensions: MARKDOWN_EXTENSIONS },
|
|
560
|
-
|
|
561
|
-
{ scope: "document-entry", kind: "file", absolutePath: sourceWorkspace.entryPath, extensions: REACT_IMPLEMENTATION_EXTENSIONS },
|
|
562
|
-
...implementationRoots(sourceWorkspace),
|
|
563
|
+
...await implementationRoots(sourceWorkspace),
|
|
563
564
|
];
|
|
564
565
|
return roots;
|
|
565
566
|
}
|
|
566
567
|
return contentRoots;
|
|
567
568
|
}
|
|
568
569
|
|
|
569
|
-
function implementationRoots(sourceWorkspace) {
|
|
570
|
+
async function implementationRoots(sourceWorkspace) {
|
|
570
571
|
const roots = [];
|
|
571
572
|
const seen = new Set();
|
|
573
|
+
const documentRoot = sourceWorkspace.config.paths.documentRoot;
|
|
574
|
+
const entries = await readDirectoryEntries(documentRoot);
|
|
575
|
+
for (const entry of entries) {
|
|
576
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
577
|
+
const absolutePath = path.join(documentRoot, entry.name);
|
|
578
|
+
if (seen.has(absolutePath)) continue;
|
|
579
|
+
seen.add(absolutePath);
|
|
580
|
+
roots.push({
|
|
581
|
+
scope: entry.name === "shared" ? "shared-source" : "press-source",
|
|
582
|
+
kind: "dir",
|
|
583
|
+
absolutePath,
|
|
584
|
+
extensions: ALL_SOURCE_EXTENSIONS,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
572
587
|
for (const root of sourceWorkspace.contentRoots ?? [{ kind: "dir", absolutePath: sourceWorkspace.sourceDir }]) {
|
|
573
588
|
const absolutePath = root.kind === "dir" ? root.absolutePath : path.dirname(root.absolutePath);
|
|
574
589
|
if (seen.has(absolutePath)) continue;
|
|
@@ -583,6 +598,15 @@ function implementationRoots(sourceWorkspace) {
|
|
|
583
598
|
return roots;
|
|
584
599
|
}
|
|
585
600
|
|
|
601
|
+
async function readDirectoryEntries(directory) {
|
|
602
|
+
try {
|
|
603
|
+
return await fs.readdir(directory, { withFileTypes: true });
|
|
604
|
+
} catch (error) {
|
|
605
|
+
if (error?.code === "ENOENT") return [];
|
|
606
|
+
throw error;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
586
610
|
function forEachLine(text, visit) {
|
|
587
611
|
const lineRe = /([^\r\n]*)(\r\n|\n|\r|$)/g;
|
|
588
612
|
let lineNumber = 1;
|
|
@@ -12,7 +12,7 @@ export async function resolveActiveSourceWorkspace(config) {
|
|
|
12
12
|
const reactEntry = await loadReactDocumentEntry(config.root);
|
|
13
13
|
if (!reactEntry) {
|
|
14
14
|
throw new Error(
|
|
15
|
-
"React/MDX document entry not found. Expected press
|
|
15
|
+
"React/MDX document entry not found. Expected one or more press/*/press.tsx files before using workspace source tools.",
|
|
16
16
|
);
|
|
17
17
|
}
|
|
18
18
|
// Aggregate sources across every Press in the Workspace. Workspace
|
|
@@ -26,6 +26,7 @@ export async function resolveActiveSourceWorkspace(config) {
|
|
|
26
26
|
}
|
|
27
27
|
const contentRoots = contentRootsFromSources(aggregateSources, reactEntry.config);
|
|
28
28
|
const sourceDir = firstDirectoryRoot(contentRoots) ?? reactEntry.config.paths.documentRoot;
|
|
29
|
+
const hasRegisteredSources = contentRoots.length > 0;
|
|
29
30
|
|
|
30
31
|
return {
|
|
31
32
|
kind: "react-mdx",
|
|
@@ -34,11 +35,12 @@ export async function resolveActiveSourceWorkspace(config) {
|
|
|
34
35
|
entryPath: reactEntry.entryPath,
|
|
35
36
|
sourceDir,
|
|
36
37
|
contentRoots,
|
|
38
|
+
hasRegisteredSources,
|
|
37
39
|
contentExtensions: REACT_MDX_CONTENT_EXTENSIONS,
|
|
38
40
|
contentLabel: "React MDX chapter source",
|
|
39
41
|
missingCode: "react-source.missing",
|
|
40
42
|
emptyCode: "react-source.empty",
|
|
41
|
-
missingMessage: "Registered React MDX sources do not exist yet; create the files or roots declared in press
|
|
43
|
+
missingMessage: "Registered React MDX sources do not exist yet; create the files or roots declared in press/*/press.tsx `sources` before running export.",
|
|
42
44
|
emptyMessage: "Registered React MDX sources contain no `*.mdx` files; the document will export with zero source blocks.",
|
|
43
45
|
};
|
|
44
46
|
}
|
|
@@ -78,6 +80,7 @@ export async function collectActiveContentFiles(sourceWorkspace, { skipUnderscor
|
|
|
78
80
|
|
|
79
81
|
export async function sourceDirectoryExists(sourceWorkspace) {
|
|
80
82
|
const roots = sourceWorkspace.contentRoots ?? [{ kind: "dir", absolutePath: sourceWorkspace.sourceDir }];
|
|
83
|
+
if (roots.length === 0) return true;
|
|
81
84
|
for (const root of roots) {
|
|
82
85
|
try {
|
|
83
86
|
const stat = await fs.stat(root.absolutePath);
|
|
@@ -93,13 +96,7 @@ export async function sourceDirectoryExists(sourceWorkspace) {
|
|
|
93
96
|
function contentRootsFromSources(sources, config) {
|
|
94
97
|
const entries = Object.entries(sources ?? {});
|
|
95
98
|
if (entries.length === 0) {
|
|
96
|
-
return [
|
|
97
|
-
kind: "dir",
|
|
98
|
-
absolutePath: config.paths.sourceDir,
|
|
99
|
-
basePath: config.paths.sourceDir,
|
|
100
|
-
sourceId: "default",
|
|
101
|
-
preset: "section-folders",
|
|
102
|
-
}];
|
|
99
|
+
return [];
|
|
103
100
|
}
|
|
104
101
|
|
|
105
102
|
const roots = [];
|
|
@@ -17,12 +17,20 @@ const PUBLIC_DEPLOY_ADAPTERS = new Set([
|
|
|
17
17
|
"vercel",
|
|
18
18
|
]);
|
|
19
19
|
|
|
20
|
-
// A directory is an OpenPress workspace if it contains
|
|
21
|
-
//
|
|
20
|
+
// A directory is an OpenPress workspace if it contains folder-convention
|
|
21
|
+
// Press entries, or a package.json with an "openpress" field.
|
|
22
22
|
async function isWorkspaceRoot(dir) {
|
|
23
23
|
try {
|
|
24
|
-
await fs.
|
|
25
|
-
|
|
24
|
+
const pressEntries = await fs.readdir(path.join(dir, "press"), { withFileTypes: true });
|
|
25
|
+
if (pressEntries.some((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "shared")) {
|
|
26
|
+
for (const entry of pressEntries) {
|
|
27
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "shared") continue;
|
|
28
|
+
try {
|
|
29
|
+
await fs.access(path.join(dir, "press", entry.name, "press.tsx"));
|
|
30
|
+
return true;
|
|
31
|
+
} catch {}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
26
34
|
} catch {}
|
|
27
35
|
try {
|
|
28
36
|
const pkg = JSON.parse(await fs.readFile(path.join(dir, "package.json"), "utf8"));
|
|
@@ -60,11 +68,7 @@ export async function validateWorkspace(root) {
|
|
|
60
68
|
|
|
61
69
|
mark("config");
|
|
62
70
|
for (const [key, target] of [
|
|
63
|
-
["sourceDir", sourceWorkspace.sourceDir],
|
|
64
|
-
["mediaDir", activeConfig.paths.mediaDir],
|
|
65
|
-
["themeDir", activeConfig.paths.themeDir],
|
|
66
71
|
["designDoc", activeConfig.paths.designDoc],
|
|
67
|
-
["componentsDir", activeConfig.paths.componentsDir],
|
|
68
72
|
]) {
|
|
69
73
|
if (!(await exists(target))) add("error", `config.${key}`, `Configured OpenPress path \`${key}\` does not exist.`, target);
|
|
70
74
|
}
|
|
@@ -87,11 +91,11 @@ export async function validateWorkspace(root) {
|
|
|
87
91
|
|
|
88
92
|
mark(sourceWorkspace.checkedName);
|
|
89
93
|
if (!(typeof activeConfig.title === "string" && activeConfig.title.trim())) {
|
|
90
|
-
add("warning", "press.title", "<Press title> is missing in press
|
|
94
|
+
add("warning", "press.title", "<Press title> is missing in press/*/press.tsx; the workbench will show the default placeholder.", activeConfig.configPath);
|
|
91
95
|
}
|
|
92
|
-
if (!(await sourceDirectoryExists(sourceWorkspace))) {
|
|
96
|
+
if (sourceWorkspace.hasRegisteredSources && !(await sourceDirectoryExists(sourceWorkspace))) {
|
|
93
97
|
add("warning", sourceWorkspace.missingCode, sourceWorkspace.missingMessage, sourceWorkspace.sourceDir);
|
|
94
|
-
} else {
|
|
98
|
+
} else if (sourceWorkspace.hasRegisteredSources) {
|
|
95
99
|
const contentFiles = await collectActiveContentFiles(sourceWorkspace, { skipUnderscoreFiles: true });
|
|
96
100
|
if (contentFiles.length === 0) {
|
|
97
101
|
add("warning", sourceWorkspace.emptyCode, sourceWorkspace.emptyMessage, sourceWorkspace.sourceDir);
|
|
@@ -119,19 +123,19 @@ export async function validateWorkspace(root) {
|
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
mark("react-source");
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
126
|
+
for (const exported of await readExportedPressDocuments(activeConfig.paths.publicDir)) {
|
|
127
|
+
const pressWarnings = exported.document?.source?.warnings;
|
|
128
|
+
if (Array.isArray(pressWarnings)) {
|
|
129
|
+
for (const warning of pressWarnings) {
|
|
130
|
+
const code = typeof warning?.code === "string" && warning.code ? warning.code : "warning";
|
|
131
|
+
add(
|
|
132
|
+
"warning",
|
|
133
|
+
`react-source.${code}`,
|
|
134
|
+
pressWarningMessage(warning),
|
|
135
|
+
exported.path,
|
|
136
|
+
warning,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
135
139
|
}
|
|
136
140
|
}
|
|
137
141
|
|
|
@@ -173,6 +177,20 @@ async function readJsonIfExists(filePath) {
|
|
|
173
177
|
}
|
|
174
178
|
}
|
|
175
179
|
|
|
180
|
+
async function readExportedPressDocuments(publicDir) {
|
|
181
|
+
const manifestPath = path.join(publicDir, "workspace.json");
|
|
182
|
+
const manifest = await readJsonIfExists(manifestPath);
|
|
183
|
+
if (!Array.isArray(manifest?.presses)) return [];
|
|
184
|
+
const out = [];
|
|
185
|
+
for (const press of manifest.presses) {
|
|
186
|
+
if (typeof press?.slug !== "string" || !press.slug.trim()) continue;
|
|
187
|
+
const documentJsonPath = path.join(publicDir, press.slug.trim(), "document.json");
|
|
188
|
+
const document = await readJsonIfExists(documentJsonPath);
|
|
189
|
+
if (document) out.push({ path: documentJsonPath, document });
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
|
|
176
194
|
async function exists(filePath) {
|
|
177
195
|
try {
|
|
178
196
|
await fs.access(filePath);
|
package/package.json
CHANGED
|
@@ -24,9 +24,7 @@ type LoadState =
|
|
|
24
24
|
document: ReaderDocument;
|
|
25
25
|
deploymentInfo: DeploymentInfo;
|
|
26
26
|
manifest: WorkspaceManifest | null;
|
|
27
|
-
//
|
|
28
|
-
// or for the root entry of a multi-Press workspace. Otherwise the
|
|
29
|
-
// active press's slug — used by refresh/back/forward to re-resolve.
|
|
27
|
+
// Active Press slug, used by refresh/back/forward to re-resolve.
|
|
30
28
|
activeSlug: string;
|
|
31
29
|
runtimeMode: OpenPressRuntimeMode;
|
|
32
30
|
}
|
|
@@ -75,16 +73,10 @@ export function OpenPressApp() {
|
|
|
75
73
|
route: WorkspaceRoute,
|
|
76
74
|
deploymentInfo: DeploymentInfo,
|
|
77
75
|
) => {
|
|
78
|
-
// No manifest (legacy deploy): always load /openpress/document.json.
|
|
79
76
|
if (!manifest || manifest.presses.length === 0) {
|
|
80
|
-
const document = await loadReaderDocument("/openpress/document.json");
|
|
81
77
|
setState({
|
|
82
|
-
status: "
|
|
83
|
-
|
|
84
|
-
deploymentInfo,
|
|
85
|
-
manifest,
|
|
86
|
-
activeSlug: "",
|
|
87
|
-
runtimeMode: resolveRuntimeMode(document, route.mode),
|
|
78
|
+
status: "error",
|
|
79
|
+
message: "OpenPress workspace manifest is missing or empty. Run open-press render to generate /openpress/workspace.json.",
|
|
88
80
|
});
|
|
89
81
|
return;
|
|
90
82
|
}
|
|
@@ -124,8 +116,8 @@ export function OpenPressApp() {
|
|
|
124
116
|
const press = state.manifest
|
|
125
117
|
? findManifestPress(state.manifest, state.activeSlug)
|
|
126
118
|
: null;
|
|
127
|
-
|
|
128
|
-
const document = await loadReaderDocument(
|
|
119
|
+
if (!press) return;
|
|
120
|
+
const document = await loadReaderDocument(press.documentUrl);
|
|
129
121
|
setState((latest) => {
|
|
130
122
|
if (latest.status !== "ready") return latest;
|
|
131
123
|
return { ...latest, document };
|
|
@@ -133,8 +125,7 @@ export function OpenPressApp() {
|
|
|
133
125
|
}, [state]);
|
|
134
126
|
|
|
135
127
|
// Gallery click → pushState + load. Bypasses resolveFromRoute's
|
|
136
|
-
// "empty slug + multi-Press → gallery" branch
|
|
137
|
-
// the unslugged root Press must enter it, not bounce back to gallery.
|
|
128
|
+
// "empty slug + multi-Press → gallery" branch.
|
|
138
129
|
const enterPress = useCallback(async (press: WorkspaceManifestPress) => {
|
|
139
130
|
if (state.status !== "gallery") return;
|
|
140
131
|
pushPressRoute(press.slug, "preview");
|
|
@@ -48,7 +48,7 @@ export function OpenPressRuntime({
|
|
|
48
48
|
// (e.g. /<slug>/present -> /<slug>/preview after exiting the slide
|
|
49
49
|
// presenter), so the SlidePresentationPage exits to the wrong
|
|
50
50
|
// route-driven branch (PublicViewer instead of HtmlWorkbench) and the
|
|
51
|
-
// user sees
|
|
51
|
+
// user sees stale public-viewer chrome until a hard reload.
|
|
52
52
|
// Bump a version on every pathname/search change so the memos
|
|
53
53
|
// re-evaluate exactly when the URL does.
|
|
54
54
|
const routeVersion = useLocationVersion();
|
|
@@ -125,11 +125,11 @@ function EmptyState({ style, workspaceMode }: { style: CSSProperties; workspaceM
|
|
|
125
125
|
<p className="openpress-empty-state__eyebrow">OpenPress</p>
|
|
126
126
|
<h1 className="openpress-empty-state__title">This document has no content yet.</h1>
|
|
127
127
|
<p className="openpress-empty-state__body">
|
|
128
|
-
Add React MDX chapter files under <code>press
|
|
128
|
+
Add React MDX chapter files under <code>press/<slug>/chapters/**/content/</code>, then re-build.
|
|
129
129
|
</p>
|
|
130
130
|
{workspaceMode ? (
|
|
131
131
|
<ol className="openpress-empty-state__steps">
|
|
132
|
-
<li><code>npm run build</code> — validates and refreshes <code>public/openpress/
|
|
132
|
+
<li><code>npm run build</code> — validates and refreshes <code>public/openpress/workspace.json</code></li>
|
|
133
133
|
<li>Reload this page</li>
|
|
134
134
|
</ol>
|
|
135
135
|
) : (
|
|
@@ -201,7 +201,7 @@ async function fetchFirstPage(url: string): Promise<HtmlPageBlock | null> {
|
|
|
201
201
|
// Convert a CSS length string (px / mm / cm / in) into device pixels
|
|
202
202
|
// at 96 dpi. A4 pages are stored as "210mm" / "297mm" so the gallery
|
|
203
203
|
// and thumbnail scalers need this to compute their fit ratio — using
|
|
204
|
-
// the bare string would
|
|
204
|
+
// the bare string would otherwise resolve to the default asset path.
|
|
205
205
|
function parsePxLength(value: string | undefined): number | null {
|
|
206
206
|
if (!value) return null;
|
|
207
207
|
const match = value.trim().match(/^([\d.]+)\s*(px|mm|cm|in)$/i);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "./cn";
|
|
3
|
+
|
|
4
|
+
export type PageFolioVariant = "current" | "total" | "slash" | "of" | "prefix";
|
|
5
|
+
export type PageFolioNumberFormat = "plain" | "2-digit" | "3-digit";
|
|
6
|
+
|
|
7
|
+
export type PageFolioProps = Omit<HTMLAttributes<HTMLSpanElement>, "children"> & {
|
|
8
|
+
variant?: PageFolioVariant;
|
|
9
|
+
currentFormat?: PageFolioNumberFormat;
|
|
10
|
+
totalFormat?: PageFolioNumberFormat;
|
|
11
|
+
prefix?: string;
|
|
12
|
+
separator?: string;
|
|
13
|
+
ofLabel?: string;
|
|
14
|
+
ariaLabel?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function PageFolio({
|
|
18
|
+
variant = "current",
|
|
19
|
+
currentFormat = "plain",
|
|
20
|
+
totalFormat = "plain",
|
|
21
|
+
prefix = "",
|
|
22
|
+
separator = "/",
|
|
23
|
+
ofLabel = "of",
|
|
24
|
+
ariaLabel,
|
|
25
|
+
className,
|
|
26
|
+
...rest
|
|
27
|
+
}: PageFolioProps) {
|
|
28
|
+
const current = placeholderForFormat(currentFormat);
|
|
29
|
+
const total = placeholderForFormat(totalFormat);
|
|
30
|
+
const label = ariaLabel ?? defaultAriaLabel(variant);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<span
|
|
34
|
+
{...rest}
|
|
35
|
+
className={cn("openpress-page-folio", `openpress-page-folio--${variant}`, className)}
|
|
36
|
+
data-openpress-page-folio="true"
|
|
37
|
+
data-openpress-page-folio-variant={variant}
|
|
38
|
+
data-openpress-page-folio-current-format={currentFormat}
|
|
39
|
+
data-openpress-page-folio-total-format={totalFormat}
|
|
40
|
+
data-openpress-page-folio-prefix={prefix}
|
|
41
|
+
data-openpress-page-folio-separator={separator}
|
|
42
|
+
data-openpress-page-folio-of-label={ofLabel}
|
|
43
|
+
aria-label={label}
|
|
44
|
+
>
|
|
45
|
+
{renderFolioParts({ variant, current, total, currentFormat, totalFormat, prefix, separator, ofLabel })}
|
|
46
|
+
</span>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderFolioParts({
|
|
51
|
+
variant,
|
|
52
|
+
current,
|
|
53
|
+
total,
|
|
54
|
+
currentFormat,
|
|
55
|
+
totalFormat,
|
|
56
|
+
prefix,
|
|
57
|
+
separator,
|
|
58
|
+
ofLabel,
|
|
59
|
+
}: {
|
|
60
|
+
variant: PageFolioVariant;
|
|
61
|
+
current: string;
|
|
62
|
+
total: string;
|
|
63
|
+
currentFormat: PageFolioNumberFormat;
|
|
64
|
+
totalFormat: PageFolioNumberFormat;
|
|
65
|
+
prefix: string;
|
|
66
|
+
separator: string;
|
|
67
|
+
ofLabel: string;
|
|
68
|
+
}) {
|
|
69
|
+
if (variant === "total") {
|
|
70
|
+
return <span className="openpress-page-folio__total" data-openpress-page-folio-total="true" data-openpress-page-folio-format={totalFormat}>{total}</span>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (variant === "slash") {
|
|
74
|
+
return (
|
|
75
|
+
<>
|
|
76
|
+
<span className="openpress-page-folio__current" data-openpress-page-folio-current="true" data-openpress-page-folio-format={currentFormat}>{current}</span>
|
|
77
|
+
<span className="openpress-page-folio__separator" data-openpress-page-folio-separator-text="true">{separator}</span>
|
|
78
|
+
<span className="openpress-page-folio__total" data-openpress-page-folio-total="true" data-openpress-page-folio-format={totalFormat}>{total}</span>
|
|
79
|
+
</>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (variant === "of") {
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<span className="openpress-page-folio__current" data-openpress-page-folio-current="true" data-openpress-page-folio-format={currentFormat}>{current}</span>
|
|
87
|
+
<span className="openpress-page-folio__separator" data-openpress-page-folio-of-text="true">{ofLabel}</span>
|
|
88
|
+
<span className="openpress-page-folio__total" data-openpress-page-folio-total="true" data-openpress-page-folio-format={totalFormat}>{total}</span>
|
|
89
|
+
</>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (variant === "prefix") {
|
|
94
|
+
return (
|
|
95
|
+
<>
|
|
96
|
+
<span className="openpress-page-folio__prefix" data-openpress-page-folio-prefix-text="true">{prefix}</span>
|
|
97
|
+
<span className="openpress-page-folio__current" data-openpress-page-folio-current="true" data-openpress-page-folio-format={currentFormat}>{current}</span>
|
|
98
|
+
</>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return <span className="openpress-page-folio__current" data-openpress-page-folio-current="true" data-openpress-page-folio-format={currentFormat}>{current}</span>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function placeholderForFormat(format: PageFolioNumberFormat) {
|
|
106
|
+
if (format === "3-digit") return "000";
|
|
107
|
+
if (format === "2-digit") return "00";
|
|
108
|
+
return "0";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function defaultAriaLabel(variant: PageFolioVariant) {
|
|
112
|
+
if (variant === "total") return "Total pages";
|
|
113
|
+
if (variant === "slash" || variant === "of") return "Page number and total pages";
|
|
114
|
+
return "Page number";
|
|
115
|
+
}
|
|
@@ -14,17 +14,16 @@ export interface AllocationHints {
|
|
|
14
14
|
totalPagesPerChain: Record<string, number>;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
// Metadata read from <Press> props by the engine pipeline.
|
|
18
|
-
//
|
|
19
|
-
// instead and leaves these as undefined. The engine merges both sources
|
|
20
|
-
// (props override config) until v1.0 removes config support.
|
|
17
|
+
// Metadata read from <Press> props by the engine pipeline. Each
|
|
18
|
+
// press/<slug>/press.tsx entry declares its document metadata here.
|
|
21
19
|
export interface PressMetadata {
|
|
22
20
|
title?: string;
|
|
23
21
|
type?: PressProps["type"];
|
|
24
22
|
page?: PressProps["page"];
|
|
25
23
|
slug?: string;
|
|
26
24
|
theme?: string;
|
|
27
|
-
componentsDir?:
|
|
25
|
+
componentsDir?: PressProps["componentsDir"];
|
|
26
|
+
mediaDir?: PressProps["mediaDir"];
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
export interface PressContextValue {
|
|
@@ -36,9 +35,7 @@ export interface PressContextValue {
|
|
|
36
35
|
// the first measurement pass.
|
|
37
36
|
hints: AllocationHints | null;
|
|
38
37
|
toc: Record<string, TocEntry[]> | null;
|
|
39
|
-
// Metadata declared on <Press> props
|
|
40
|
-
// omit this on v0.x; consumers should treat undefined as "no metadata
|
|
41
|
-
// declared on Press — fall back to openpress.config.mjs values".
|
|
38
|
+
// Metadata declared on <Press> props.
|
|
42
39
|
metadata?: PressMetadata;
|
|
43
40
|
}
|
|
44
41
|
|
|
@@ -48,8 +45,6 @@ export function Press(props: PressProps) {
|
|
|
48
45
|
// Press is intentionally inert at render time — the engine reads its
|
|
49
46
|
// props and children through React.Children inspection during the
|
|
50
47
|
// export pipeline, then injects context above any nested helpers.
|
|
51
|
-
// For the v0.x shape (children-only usage), this still passes children
|
|
52
|
-
// through unchanged.
|
|
53
48
|
return <Fragment>{props.children}</Fragment>;
|
|
54
49
|
}
|
|
55
50
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Frame } from "./Frame";
|
|
2
|
+
import type { SlideProps } from "./types";
|
|
3
|
+
|
|
4
|
+
export function Slide({
|
|
5
|
+
id,
|
|
6
|
+
role = "canvas.slide",
|
|
7
|
+
chrome = false,
|
|
8
|
+
...rest
|
|
9
|
+
}: SlideProps) {
|
|
10
|
+
return <Frame {...rest} frameKey={id} role={role} chrome={chrome} />;
|
|
11
|
+
}
|
|
@@ -8,13 +8,16 @@
|
|
|
8
8
|
export { Press, PressContext, PRESS_MARKER } from "./Press";
|
|
9
9
|
export { Workspace, WorkspaceContext, WORKSPACE_MARKER } from "./Workspace";
|
|
10
10
|
export { Frame, FRAME_MARKER } from "./Frame";
|
|
11
|
+
export { Slide } from "./Slide";
|
|
11
12
|
export { FrameContext } from "./FrameContext";
|
|
13
|
+
export { PageFolio } from "./PageFolio";
|
|
12
14
|
export { MdxArea } from "./MdxArea";
|
|
13
15
|
export { useSource } from "./useSource";
|
|
14
16
|
export { ObjectEntity, Text, BaseFigure, BaseCallout, MediaFigure, ImageFigure } from "./primitives";
|
|
15
17
|
|
|
16
18
|
export type {
|
|
17
19
|
FrameProps,
|
|
20
|
+
SlideProps,
|
|
18
21
|
FrameRole,
|
|
19
22
|
MdxAreaProps,
|
|
20
23
|
MdxAreaOverflow,
|
|
@@ -45,3 +48,4 @@ export type {
|
|
|
45
48
|
export type { PressContextValue, AllocationHints, PressMetadata } from "./Press";
|
|
46
49
|
export type { WorkspaceContextValue } from "./Workspace";
|
|
47
50
|
export type { FrameContextValue } from "./FrameContext";
|
|
51
|
+
export type { PageFolioNumberFormat, PageFolioProps, PageFolioVariant } from "./PageFolio";
|
|
@@ -18,6 +18,12 @@ export type FrameProps = Omit<HTMLAttributes<HTMLElement>, "role" | "children">
|
|
|
18
18
|
children?: ReactNode;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
export type SlideProps = Omit<FrameProps, "frameKey" | "role" | "chrome" | "title"> & {
|
|
22
|
+
id: string;
|
|
23
|
+
role?: FrameRole;
|
|
24
|
+
chrome?: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
21
27
|
export type MdxAreaOverflow = "extend" | "truncate" | "error";
|
|
22
28
|
|
|
23
29
|
export type MdxAreaProps = Omit<HTMLAttributes<HTMLElement>, "children"> & {
|
|
@@ -47,9 +53,9 @@ export interface PressProps {
|
|
|
47
53
|
// Document tree — Frames, manuscript helpers, etc.
|
|
48
54
|
children: ReactNode;
|
|
49
55
|
// -------------------------------------------------------------------------
|
|
50
|
-
//
|
|
56
|
+
// Press metadata props.
|
|
51
57
|
// -------------------------------------------------------------------------
|
|
52
|
-
// Document title.
|
|
58
|
+
// Document title. Used for PDF metadata, HTML <title>,
|
|
53
59
|
// OG tags, and the Workspace gallery / tab-bar label.
|
|
54
60
|
title?: string;
|
|
55
61
|
// Creation mode. Pages are source-driven with MDX allocation; slides are
|
|
@@ -61,15 +67,17 @@ export interface PressProps {
|
|
|
61
67
|
// Array of source registrations from mdxSource(). Replaces the v0.x
|
|
62
68
|
// `export const sources` named export.
|
|
63
69
|
sources?: ReadonlyArray<PressSource>;
|
|
64
|
-
// URL / output slug for this Press
|
|
65
|
-
// "/" when only one Press exists in the Workspace; required when the
|
|
66
|
-
// Workspace has multiple Press children.
|
|
70
|
+
// URL / output slug for this Press. Must match the Press folder name.
|
|
67
71
|
slug?: string;
|
|
68
|
-
// Optional per-Press theme directory. Defaults
|
|
69
|
-
//
|
|
72
|
+
// Optional per-Press theme directory. Defaults include the folder-local
|
|
73
|
+
// "./theme"; shared CSS lives in press/shared/theme.
|
|
70
74
|
theme?: string;
|
|
71
|
-
// Optional per-Press components
|
|
72
|
-
|
|
75
|
+
// Optional per-Press components directories. Defaults include the
|
|
76
|
+
// folder-local "./components" and workspace shared components.
|
|
77
|
+
componentsDir?: string | string[];
|
|
78
|
+
// Optional per-Press media directories. Defaults include the folder-local
|
|
79
|
+
// "./media" and workspace shared media.
|
|
80
|
+
mediaDir?: string | string[];
|
|
73
81
|
// Optional caption numbering overrides. Engine defaults to
|
|
74
82
|
// { figure: "Figure", table: "Table", separator: " " }.
|
|
75
83
|
captionNumbering?: {
|
|
@@ -80,7 +88,8 @@ export interface PressProps {
|
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
// ---------------------------------------------------------------------------
|
|
83
|
-
// Workspace —
|
|
91
|
+
// Workspace — engine-owned grouping component holding one or more Press
|
|
92
|
+
// children from discovered press/*/press.tsx entries.
|
|
84
93
|
// ---------------------------------------------------------------------------
|
|
85
94
|
|
|
86
95
|
export interface WorkspaceProps {
|
|
@@ -90,10 +99,9 @@ export interface WorkspaceProps {
|
|
|
90
99
|
// Project label surfaced in the gallery header, tab bar, and PDF
|
|
91
100
|
// metadata. Optional.
|
|
92
101
|
name?: string;
|
|
93
|
-
//
|
|
94
|
-
// set their own `theme` prop inherit from this. Default "./theme".
|
|
102
|
+
// Reserved for future workspace-level shared theme overrides.
|
|
95
103
|
theme?: string;
|
|
96
|
-
//
|
|
104
|
+
// Reserved for future workspace-level shared media overrides.
|
|
97
105
|
media?: string;
|
|
98
106
|
}
|
|
99
107
|
|
|
@@ -21,7 +21,7 @@ export function useSource<T extends ResolvedSource = ResolvedSource>(id: string)
|
|
|
21
21
|
const knownText = known.length > 0 ? known.join(", ") : "(none)";
|
|
22
22
|
throw new Error(
|
|
23
23
|
`Unknown source "${id}". Available sources: ${knownText}. ` +
|
|
24
|
-
`Register it as a <Press sources={[mdxSource({ id: "${id}", ... })]}> entry in press
|
|
24
|
+
`Register it as a <Press sources={[mdxSource({ id: "${id}", ... })]}> entry in press/<slug>/press.tsx.`,
|
|
25
25
|
);
|
|
26
26
|
}
|
|
27
27
|
return source as T;
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
// Shape of /openpress/workspace.json — the reader fetches this on
|
|
2
2
|
// boot to decide between gallery routing (multi-Press) and direct
|
|
3
|
-
// load (single Press). One entry per
|
|
4
|
-
//
|
|
5
|
-
// Single-Press workspaces emit one entry with slug = "" and the
|
|
6
|
-
// legacy /openpress/document.json path. Multi-Press emits one entry
|
|
7
|
-
// per slug; each `documentUrl` resolves to /openpress/<slug>/document.json.
|
|
3
|
+
// load (single Press). One entry per discovered Press folder.
|
|
8
4
|
import type { PressType } from "./documentTypes";
|
|
9
5
|
|
|
10
6
|
export interface WorkspaceManifest {
|
|
@@ -16,13 +12,12 @@ export interface WorkspaceManifest {
|
|
|
16
12
|
}
|
|
17
13
|
|
|
18
14
|
export interface WorkspaceManifestPress {
|
|
19
|
-
// Slug for this Press.
|
|
20
|
-
// (legacy root); slug-shaped string for multi-Press.
|
|
15
|
+
// Slug for this Press. Matches the folder-convention Press slug.
|
|
21
16
|
slug: string;
|
|
22
17
|
// <Press title="..."> prop. Required in v1.0 contract.
|
|
23
18
|
title: string;
|
|
24
|
-
// Creation mode declared by <Press type>.
|
|
25
|
-
//
|
|
19
|
+
// Creation mode declared by <Press type>. The reader uses this for
|
|
20
|
+
// mode-specific navigation affordances.
|
|
26
21
|
type: PressType;
|
|
27
22
|
// Page geometry summary. Same shape as the reader's
|
|
28
23
|
// ReaderDocument.theme — readers can show a thumb in the gallery
|