@open-press/core 0.3.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 (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +36 -0
  3. package/engine/chrome-pdf.d.mts +34 -0
  4. package/engine/chrome-pdf.mjs +344 -0
  5. package/engine/cli.mjs +93 -0
  6. package/engine/commands/_shared.mjs +170 -0
  7. package/engine/commands/deploy.mjs +31 -0
  8. package/engine/commands/dev.mjs +26 -0
  9. package/engine/commands/export.mjs +8 -0
  10. package/engine/commands/init.mjs +24 -0
  11. package/engine/commands/inspect.mjs +35 -0
  12. package/engine/commands/migrate-to-react.mjs +27 -0
  13. package/engine/commands/pdf.mjs +26 -0
  14. package/engine/commands/preview.mjs +26 -0
  15. package/engine/commands/render.mjs +17 -0
  16. package/engine/commands/replace.mjs +41 -0
  17. package/engine/commands/search.mjs +33 -0
  18. package/engine/commands/typecheck.mjs +5 -0
  19. package/engine/commands/validate.mjs +17 -0
  20. package/engine/config.d.mts +40 -0
  21. package/engine/config.mjs +160 -0
  22. package/engine/deploy-sync.mjs +15 -0
  23. package/engine/document-export.mjs +15 -0
  24. package/engine/file-utils.mjs +106 -0
  25. package/engine/fonts.mjs +62 -0
  26. package/engine/init.mjs +90 -0
  27. package/engine/inspection.mjs +348 -0
  28. package/engine/issue-report.mjs +44 -0
  29. package/engine/katex-assets.mjs +45 -0
  30. package/engine/page-block.mjs +30 -0
  31. package/engine/page-renderer.mjs +217 -0
  32. package/engine/pdf-media.mjs +45 -0
  33. package/engine/public-assets.mjs +19 -0
  34. package/engine/react/chapter-css.mjs +53 -0
  35. package/engine/react/comment-endpoint.d.mts +11 -0
  36. package/engine/react/comment-endpoint.mjs +128 -0
  37. package/engine/react/comment-marker.mjs +306 -0
  38. package/engine/react/document-entry.mjs +253 -0
  39. package/engine/react/document-export.mjs +392 -0
  40. package/engine/react/mdx-compile.mjs +295 -0
  41. package/engine/react/measurement-css.mjs +44 -0
  42. package/engine/react/migrate-to-react.mjs +355 -0
  43. package/engine/react/pagination-constants.mjs +3 -0
  44. package/engine/react/pagination.mjs +121 -0
  45. package/engine/react/project-asset-endpoint.d.mts +10 -0
  46. package/engine/react/project-asset-endpoint.mjs +379 -0
  47. package/engine/react/workspace-discovery.mjs +156 -0
  48. package/engine/source-text-tools.mjs +280 -0
  49. package/engine/source-workspace.mjs +76 -0
  50. package/engine/static-server.mjs +493 -0
  51. package/engine/validation.mjs +172 -0
  52. package/index.html +13 -0
  53. package/package.json +86 -0
  54. package/src/openpress/App.tsx +127 -0
  55. package/src/openpress/composerMentions.ts +188 -0
  56. package/src/openpress/core/basePages.tsx +87 -0
  57. package/src/openpress/core/index.tsx +20 -0
  58. package/src/openpress/core/types.ts +71 -0
  59. package/src/openpress/frameScheduler.ts +32 -0
  60. package/src/openpress/indexes.ts +329 -0
  61. package/src/openpress/inspector.ts +282 -0
  62. package/src/openpress/pageRoute.ts +21 -0
  63. package/src/openpress/pagination.ts +845 -0
  64. package/src/openpress/projectIdentity.ts +15 -0
  65. package/src/openpress/projectSources.ts +24 -0
  66. package/src/openpress/projectWorkspace.tsx +919 -0
  67. package/src/openpress/publicPage.tsx +469 -0
  68. package/src/openpress/reactDocumentMetadata.ts +41 -0
  69. package/src/openpress/readerPageRegistry.ts +41 -0
  70. package/src/openpress/readerRuntime.ts +230 -0
  71. package/src/openpress/readerScroll.ts +92 -0
  72. package/src/openpress/readerState.ts +15 -0
  73. package/src/openpress/renderer.tsx +91 -0
  74. package/src/openpress/runtimeMode.ts +22 -0
  75. package/src/openpress/types.ts +112 -0
  76. package/src/openpress/workbench.tsx +1299 -0
  77. package/src/openpress/workbenchPanels.tsx +122 -0
  78. package/src/openpress/workbenchTypes.ts +4 -0
  79. package/src/styles/openpress/app-shell.css +251 -0
  80. package/src/styles/openpress/media-workspace.css +230 -0
  81. package/src/styles/openpress/print-route.css +186 -0
  82. package/src/styles/openpress/project-workspace.css +1318 -0
  83. package/src/styles/openpress/public-viewer.css +983 -0
  84. package/src/styles/openpress/reader-runtime.css +792 -0
  85. package/src/styles/openpress/responsive.css +384 -0
  86. package/src/styles/openpress/workbench-panels.css +558 -0
  87. package/src/styles/openpress/workbench.css +720 -0
  88. package/src/styles/openpress.css +14 -0
  89. package/tsconfig.json +37 -0
  90. package/vite.config.ts +512 -0
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "@open-press/core",
3
+ "version": "0.3.0",
4
+ "type": "module",
5
+ "description": "open-press core — runtime primitives, CLI, and render pipeline for AI-first fixed-layout documents.",
6
+ "license": "MIT",
7
+ "author": "quan0715",
8
+ "homepage": "https://github.com/quan0715/open-press#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/quan0715/open-press.git",
12
+ "directory": "packages/core"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/quan0715/open-press/issues"
16
+ },
17
+ "keywords": [
18
+ "document",
19
+ "mdx",
20
+ "react",
21
+ "pdf",
22
+ "a4",
23
+ "fixed-layout",
24
+ "ai-first"
25
+ ],
26
+ "main": "./src/openpress/core/index.tsx",
27
+ "types": "./src/openpress/core/index.tsx",
28
+ "exports": {
29
+ ".": "./src/openpress/core/index.tsx"
30
+ },
31
+ "bin": {
32
+ "open-press": "engine/cli.mjs"
33
+ },
34
+ "files": [
35
+ "engine",
36
+ "src/openpress",
37
+ "src/styles",
38
+ "index.html",
39
+ "vite.config.ts",
40
+ "tsconfig.json"
41
+ ],
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "dependencies": {
46
+ "@mdx-js/mdx": "^3.1.1",
47
+ "@mdx-js/react": "^3.1.1",
48
+ "js-yaml": "^4.1.1",
49
+ "katex": "^0.16.47",
50
+ "lucide-react": "^1.16.0",
51
+ "react": "^19.2.6",
52
+ "react-dom": "^19.2.6",
53
+ "rehype-katex": "^7.0.1",
54
+ "remark-gfm": "^4.0.1",
55
+ "remark-math": "^6.0.0"
56
+ },
57
+ "devDependencies": {
58
+ "@playwright/test": "^1.60.0",
59
+ "@testing-library/react": "^16.3.2",
60
+ "@types/node": "^25.8.0",
61
+ "@types/react": "^19.2.14",
62
+ "@types/react-dom": "^19.2.3",
63
+ "@vitejs/plugin-react": "^6.0.2",
64
+ "jsdom": "^26.1.0",
65
+ "typescript": "^6.0.3",
66
+ "vite": "^8.0.13",
67
+ "vitest": "^4.1.6"
68
+ },
69
+ "scripts": {
70
+ "dev": "node engine/cli.mjs dev . --renderer react",
71
+ "build": "node engine/cli.mjs render . --renderer react",
72
+ "preview": "node engine/cli.mjs preview . --renderer react",
73
+ "typecheck": "node engine/cli.mjs typecheck .",
74
+ "test": "pnpm run test:node && pnpm run test:react",
75
+ "test:e2e:reader": "playwright test --config playwright.reader.config.ts",
76
+ "test:node": "node --test tests/*.test.mjs",
77
+ "test:react": "vitest run",
78
+ "openpress:validate": "node engine/cli.mjs validate .",
79
+ "openpress:export": "node engine/cli.mjs export .",
80
+ "openpress:pdf": "node engine/cli.mjs pdf .",
81
+ "openpress:render": "node engine/cli.mjs render . --renderer react",
82
+ "openpress:preview": "node engine/cli.mjs preview . --renderer react",
83
+ "openpress:deploy": "node engine/cli.mjs deploy .",
84
+ "openpress:deploy:dry-run": "node engine/cli.mjs deploy . --confirm --dry-run"
85
+ }
86
+ }
@@ -0,0 +1,127 @@
1
+ import { useEffect, useState } from "react";
2
+ import { Renderer } from "./renderer";
3
+ import { isLocalWorkspaceHost } from "./runtimeMode";
4
+ import type { DeploymentInfo, ReaderDocument } from "./types";
5
+
6
+ type LoadState =
7
+ | { status: "loading" }
8
+ | {
9
+ status: "ready";
10
+ document: ReaderDocument;
11
+ deploymentInfo: DeploymentInfo;
12
+ }
13
+ | { status: "error"; message: string };
14
+
15
+ interface DeployConfig {
16
+ pdf?: string;
17
+ deployed_at?: string;
18
+ public_url?: string;
19
+ dirty?: boolean;
20
+ deploy_configured?: boolean;
21
+ deploy_adapter?: string;
22
+ deploy_source?: string;
23
+ deploy_project_name?: string | null;
24
+ deploy_setup_message?: string;
25
+ }
26
+
27
+ const offlineDeploymentInfo: DeploymentInfo = { online: false };
28
+
29
+ function LoadingScreen() {
30
+ return (
31
+ <div className="openpress-loading-screen" aria-label="載入中" role="status">
32
+ <div className="openpress-loading-screen__inner">
33
+ <div className="openpress-loading-dots" aria-hidden="true">
34
+ <span /><span /><span />
35
+ </div>
36
+ <span className="openpress-loading-screen__label">載入文件</span>
37
+ </div>
38
+ </div>
39
+ );
40
+ }
41
+
42
+ export function App() {
43
+ const [state, setState] = useState<LoadState>({ status: "loading" });
44
+
45
+ useEffect(() => {
46
+ let cancelled = false;
47
+
48
+ async function loadDocument() {
49
+ try {
50
+ const [response, deploymentInfo] = await Promise.all([
51
+ fetch("/openpress/document.json", { cache: "no-store" }),
52
+ loadDeploymentInfo(),
53
+ ]);
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
+ if (!cancelled) {
59
+ setState({ status: "ready", document, deploymentInfo });
60
+ }
61
+ } catch (error) {
62
+ if (!cancelled) {
63
+ setState({
64
+ status: "error",
65
+ message: error instanceof Error ? error.message : "Unable to load OpenPress document.",
66
+ });
67
+ }
68
+ }
69
+ }
70
+
71
+ void loadDocument();
72
+ return () => {
73
+ cancelled = true;
74
+ };
75
+ }, []);
76
+
77
+ if (state.status === "loading") return <LoadingScreen />;
78
+
79
+ if (state.status === "error") {
80
+ return <div className="openpress-load-state openpress-load-state--error">{state.message}</div>;
81
+ }
82
+
83
+ return (
84
+ <Renderer
85
+ document={state.document}
86
+ deploymentInfo={state.deploymentInfo}
87
+ />
88
+ );
89
+ }
90
+
91
+ async function loadDeploymentInfo(): Promise<DeploymentInfo> {
92
+ if (typeof window !== "undefined" && isLocalWorkspaceHost(window.location.hostname)) {
93
+ const localInfo = await loadDeploymentInfoFrom("/__openpress/status");
94
+ if (localInfo) return localInfo;
95
+ }
96
+
97
+ return (await loadDeploymentInfoFrom("/openpress/deploy.json")) ?? offlineDeploymentInfo;
98
+ }
99
+
100
+ async function loadDeploymentInfoFrom(path: string): Promise<DeploymentInfo | null> {
101
+ try {
102
+ const response = await fetch(path, { cache: "no-store" });
103
+ if (!response.ok) {
104
+ return null;
105
+ }
106
+ const config = (await response.json()) as DeployConfig;
107
+ return deploymentConfigToInfo(config);
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+
113
+ function deploymentConfigToInfo(config: DeployConfig): DeploymentInfo {
114
+ const configured = config.deploy_configured !== false;
115
+ return {
116
+ online: configured && Boolean(config.deployed_at || config.public_url),
117
+ deployedAt: config.deployed_at,
118
+ pdf: typeof config.pdf === "string" ? config.pdf : undefined,
119
+ publicUrl: typeof config.public_url === "string" ? config.public_url : undefined,
120
+ dirty: config.dirty === true,
121
+ configured,
122
+ adapter: typeof config.deploy_adapter === "string" ? config.deploy_adapter : undefined,
123
+ source: typeof config.deploy_source === "string" ? config.deploy_source : undefined,
124
+ projectName: typeof config.deploy_project_name === "string" ? config.deploy_project_name : undefined,
125
+ setupMessage: typeof config.deploy_setup_message === "string" ? config.deploy_setup_message : undefined,
126
+ };
127
+ }
@@ -0,0 +1,188 @@
1
+ import { useEffect, useMemo, useState, type KeyboardEvent, type RefObject } from "react";
2
+
3
+ export type ComposerMentionItem = {
4
+ trigger: "@" | "/";
5
+ value: string;
6
+ label: string;
7
+ meta: string;
8
+ kind: "media" | "component" | "skill" | "chapter" | "section" | "prefix";
9
+ };
10
+
11
+ export type ActiveComposerMention = {
12
+ trigger: "@" | "/";
13
+ query: string;
14
+ start: number;
15
+ end: number;
16
+ };
17
+
18
+ export function useComposerMentions({
19
+ text,
20
+ items,
21
+ textareaRef,
22
+ onTextChange,
23
+ enabled = true,
24
+ maxSuggestions = 7,
25
+ }: {
26
+ text: string;
27
+ items: ComposerMentionItem[];
28
+ textareaRef: RefObject<HTMLTextAreaElement | null>;
29
+ onTextChange: (value: string) => void;
30
+ enabled?: boolean;
31
+ maxSuggestions?: number;
32
+ }) {
33
+ const [composerCursor, setComposerCursor] = useState(0);
34
+ const [highlightedMentionIndex, setHighlightedMentionIndex] = useState(0);
35
+ const [dismissedMentionKey, setDismissedMentionKey] = useState<string | null>(null);
36
+ const activeMention = enabled ? resolveComposerMention(text, composerCursor) : null;
37
+ const mentionKey = activeMention ? `${activeMention.trigger}:${activeMention.start}:${activeMention.query}` : null;
38
+ const mentionSuggestions = useMemo(() => {
39
+ if (!activeMention) return [];
40
+ if (mentionKey && dismissedMentionKey === mentionKey) return [];
41
+ return createMentionSuggestions(activeMention, items, maxSuggestions);
42
+ }, [activeMention, dismissedMentionKey, items, maxSuggestions, mentionKey]);
43
+
44
+ useEffect(() => {
45
+ setHighlightedMentionIndex(0);
46
+ }, [mentionKey, mentionSuggestions.length]);
47
+
48
+ const syncCursor = () => {
49
+ const textarea = textareaRef.current;
50
+ if (textarea) setComposerCursor(textarea.selectionStart ?? text.length);
51
+ };
52
+
53
+ const insertMention = (item: ComposerMentionItem) => {
54
+ if (!activeMention) return;
55
+ const suffix = item.kind === "prefix" ? "" : " ";
56
+ const nextText = `${text.slice(0, activeMention.start)}${item.value}${suffix}${text.slice(activeMention.end)}`;
57
+ const nextCursor = activeMention.start + item.value.length + suffix.length;
58
+ setDismissedMentionKey(null);
59
+ onTextChange(nextText);
60
+ if (typeof window === "undefined") return;
61
+ window.requestAnimationFrame(() => {
62
+ textareaRef.current?.focus();
63
+ textareaRef.current?.setSelectionRange(nextCursor, nextCursor);
64
+ setComposerCursor(nextCursor);
65
+ });
66
+ };
67
+
68
+ const handleMentionKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
69
+ if (!activeMention || mentionSuggestions.length === 0) return false;
70
+ if (event.key === "ArrowDown") {
71
+ event.preventDefault();
72
+ setHighlightedMentionIndex((index) => (index + 1) % mentionSuggestions.length);
73
+ return true;
74
+ }
75
+ if (event.key === "ArrowUp") {
76
+ event.preventDefault();
77
+ setHighlightedMentionIndex((index) => (index - 1 + mentionSuggestions.length) % mentionSuggestions.length);
78
+ return true;
79
+ }
80
+ if (event.key === "Enter" || event.key === "Tab") {
81
+ event.preventDefault();
82
+ insertMention(mentionSuggestions[highlightedMentionIndex] ?? mentionSuggestions[0]);
83
+ return true;
84
+ }
85
+ if (event.key === "Escape") {
86
+ event.preventDefault();
87
+ setDismissedMentionKey(mentionKey);
88
+ return true;
89
+ }
90
+ return false;
91
+ };
92
+
93
+ return {
94
+ activeMention,
95
+ handleMentionKeyDown,
96
+ highlightedMentionIndex,
97
+ setHighlightedMentionIndex,
98
+ mentionSuggestions,
99
+ setComposerCursor,
100
+ syncCursor,
101
+ insertMention,
102
+ };
103
+ }
104
+
105
+ export function appendComposerToken(text: string, token: string) {
106
+ const trimmedToken = token.trim();
107
+ if (!trimmedToken) return text;
108
+ if (!text.trim()) return `${trimmedToken} `;
109
+ return `${text.replace(/\s*$/, " ")}${trimmedToken} `;
110
+ }
111
+
112
+ export function createMentionSuggestions(
113
+ activeMention: ActiveComposerMention,
114
+ items: ComposerMentionItem[],
115
+ maxSuggestions: number,
116
+ ) {
117
+ if (activeMention.trigger === "@") {
118
+ const prefixItems = createMentionPrefixItems(items);
119
+ const normalizedQuery = activeMention.query.trim().toLowerCase();
120
+ if (!normalizedQuery) return prefixItems.slice(0, maxSuggestions);
121
+ if (!normalizedQuery.includes("/")) {
122
+ return uniqueMentionItems([
123
+ ...prefixItems.filter((item) => mentionMatches(item, normalizedQuery)),
124
+ ...items.filter((item) => item.trigger === "@" && mentionMatches(item, normalizedQuery)),
125
+ ]).slice(0, maxSuggestions);
126
+ }
127
+ }
128
+
129
+ return items
130
+ .filter((item) => item.trigger === activeMention.trigger && mentionMatches(item, activeMention.query))
131
+ .slice(0, maxSuggestions);
132
+ }
133
+
134
+ function createMentionPrefixItems(items: ComposerMentionItem[]): ComposerMentionItem[] {
135
+ const availableKinds = new Set(items.filter((item) => item.trigger === "@").map((item) => item.kind));
136
+ return MENTION_PREFIX_DEFINITIONS
137
+ .filter((item) => availableKinds.has(item.kind))
138
+ .map((item) => ({
139
+ trigger: "@" as const,
140
+ value: `@${item.kind}/`,
141
+ label: item.kind,
142
+ meta: item.meta,
143
+ kind: "prefix" as const,
144
+ }));
145
+ }
146
+
147
+ function uniqueMentionItems(items: ComposerMentionItem[]) {
148
+ const seen = new Set<string>();
149
+ return items.filter((item) => {
150
+ const key = `${item.kind}:${item.value}`;
151
+ if (seen.has(key)) return false;
152
+ seen.add(key);
153
+ return true;
154
+ });
155
+ }
156
+
157
+ function resolveComposerMention(text: string, cursor: number): ActiveComposerMention | null {
158
+ const safeCursor = clampNumber(cursor, 0, text.length);
159
+ const beforeCursor = text.slice(0, safeCursor);
160
+ const match = beforeCursor.match(/(^|\s)([@/])([^\s@]*)$/);
161
+ if (!match) return null;
162
+ const start = beforeCursor.length - match[0].length + match[1].length;
163
+ return {
164
+ trigger: match[2] as "@" | "/",
165
+ query: match[3] ?? "",
166
+ start,
167
+ end: safeCursor,
168
+ };
169
+ }
170
+
171
+ function mentionMatches(item: ComposerMentionItem, query: string) {
172
+ const normalizedQuery = query.trim().toLowerCase();
173
+ if (!normalizedQuery) return true;
174
+ return item.value.toLowerCase().includes(normalizedQuery)
175
+ || item.label.toLowerCase().includes(normalizedQuery)
176
+ || item.meta.toLowerCase().includes(normalizedQuery);
177
+ }
178
+
179
+ function clampNumber(value: number, min: number, max: number) {
180
+ return Math.min(Math.max(value, min), Math.max(min, max));
181
+ }
182
+
183
+ const MENTION_PREFIX_DEFINITIONS: Array<{ kind: "media" | "chapter" | "section" | "component"; meta: string }> = [
184
+ { kind: "media", meta: "prefix · images" },
185
+ { kind: "chapter", meta: "prefix · chapters" },
186
+ { kind: "section", meta: "prefix · sections" },
187
+ { kind: "component", meta: "prefix · components" },
188
+ ];
@@ -0,0 +1,87 @@
1
+ import type {
2
+ BaseCalloutProps,
3
+ BaseFigureProps,
4
+ BasePageProps,
5
+ BaseContentPageProps,
6
+ BaseShellPageProps,
7
+ } from "./types";
8
+
9
+ function classNames(...values: Array<string | undefined>) {
10
+ const joined = values.filter(Boolean).join(" ");
11
+ return joined.length > 0 ? joined : undefined;
12
+ }
13
+
14
+ export function BasePage({ kind, footer = true, className, children, ...sectionProps }: BasePageProps) {
15
+ return (
16
+ <section
17
+ {...sectionProps}
18
+ className={classNames("reader-page", `reader-page--${kind}`, className)}
19
+ data-page-footer={footer ? "true" : "false"}
20
+ data-page-kind={kind}
21
+ >
22
+ {children}
23
+ </section>
24
+ );
25
+ }
26
+
27
+ export function BaseCoverPage(props: BaseShellPageProps) {
28
+ return <BasePage {...props} footer={false} kind="cover" />;
29
+ }
30
+
31
+ export function BaseTocPage(props: BaseShellPageProps) {
32
+ return <BasePage {...props} footer={false} kind="toc" />;
33
+ }
34
+
35
+ export function BaseContentPage({
36
+ pageIndex,
37
+ totalPages,
38
+ chapterSlug,
39
+ chapterTone,
40
+ runningHeader,
41
+ footerLeft,
42
+ footerRight,
43
+ children,
44
+ ...sectionProps
45
+ }: BaseContentPageProps) {
46
+ return (
47
+ <BasePage
48
+ {...sectionProps}
49
+ data-chapter-slug={chapterSlug}
50
+ data-chapter-tone={chapterTone}
51
+ data-page-index={pageIndex}
52
+ data-total-pages={totalPages}
53
+ footer
54
+ kind="content"
55
+ >
56
+ {runningHeader === undefined ? null : <header data-page-running-header>{runningHeader}</header>}
57
+ {children}
58
+ {footerLeft === undefined && footerRight === undefined ? null : (
59
+ <footer data-page-footer-content>
60
+ {footerLeft === undefined ? null : <span data-page-footer-left>{footerLeft}</span>}
61
+ {footerRight === undefined ? null : <span data-page-footer-right>{footerRight}</span>}
62
+ </footer>
63
+ )}
64
+ </BasePage>
65
+ );
66
+ }
67
+
68
+ export function BaseBackCoverPage(props: BaseShellPageProps) {
69
+ return <BasePage {...props} footer={false} kind="back-cover" />;
70
+ }
71
+
72
+ export function BaseFigure({ caption, className, children, ...figureProps }: BaseFigureProps) {
73
+ return (
74
+ <figure {...figureProps} className={classNames("openpress-figure", className)}>
75
+ <div data-figure-body>{children}</div>
76
+ {caption === undefined ? null : <figcaption>{caption}</figcaption>}
77
+ </figure>
78
+ );
79
+ }
80
+
81
+ export function BaseCallout({ kind = "info", className, children, ...calloutProps }: BaseCalloutProps) {
82
+ return (
83
+ <aside {...calloutProps} className={classNames("openpress-callout", className)} data-callout-kind={kind}>
84
+ {children}
85
+ </aside>
86
+ );
87
+ }
@@ -0,0 +1,20 @@
1
+ export {
2
+ BaseBackCoverPage,
3
+ BaseCallout,
4
+ BaseCoverPage,
5
+ BaseFigure,
6
+ BasePage,
7
+ BaseContentPage,
8
+ BaseTocPage,
9
+ } from "./basePages";
10
+ export type {
11
+ BaseCalloutKind,
12
+ BaseCalloutProps,
13
+ BaseFigureProps,
14
+ BasePageProps,
15
+ BaseContentPageProps,
16
+ BaseShellPageProps,
17
+ PageKind,
18
+ PageProps,
19
+ Manifest,
20
+ } from "./types";
@@ -0,0 +1,71 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+
3
+ export type PageKind = "cover" | "toc" | "content" | "back-cover";
4
+
5
+ export interface PageProps {
6
+ pageIndex: number;
7
+ totalPages: number;
8
+ chapterSlug?: string;
9
+ chapterTone?: string;
10
+ children: ReactNode;
11
+ }
12
+
13
+ export type BasePageProps = Omit<HTMLAttributes<HTMLElement>, "children"> & {
14
+ kind: PageKind;
15
+ footer?: boolean;
16
+ children?: ReactNode;
17
+ };
18
+
19
+ export type BaseShellPageProps = Omit<BasePageProps, "kind" | "footer">;
20
+
21
+ export type BaseContentPageProps = Omit<BasePageProps, "kind" | "footer" | "children"> &
22
+ PageProps & {
23
+ runningHeader?: ReactNode;
24
+ footerLeft?: ReactNode;
25
+ footerRight?: ReactNode;
26
+ };
27
+
28
+ export type BaseFigureProps = Omit<HTMLAttributes<HTMLElement>, "children"> & {
29
+ caption?: ReactNode;
30
+ children: ReactNode;
31
+ };
32
+
33
+ export type BaseCalloutKind = "info" | "warn" | "success" | "error" | (string & {});
34
+
35
+ export type BaseCalloutProps = Omit<HTMLAttributes<HTMLElement>, "children"> & {
36
+ kind?: BaseCalloutKind;
37
+ children: ReactNode;
38
+ };
39
+
40
+ export interface Manifest {
41
+ title: string;
42
+ subtitle?: string;
43
+ organization?: string;
44
+ workspaceLabel?: string;
45
+ documentDir?: string;
46
+ sourceDir?: string;
47
+ componentsDir?: string;
48
+ mediaDir?: string;
49
+ themeDir?: string;
50
+ designDoc?: string;
51
+ publicDir?: string;
52
+ outputDir?: string;
53
+ pdf?: {
54
+ filename?: string;
55
+ };
56
+ deploy?: {
57
+ adapter?: string;
58
+ source?: string;
59
+ projectName?: string;
60
+ commitDirty?: boolean;
61
+ requiresConfirmation?: boolean;
62
+ };
63
+ paths?: {
64
+ chaptersDir?: string;
65
+ sourceDir?: string;
66
+ componentsDir?: string;
67
+ mediaDir?: string;
68
+ themeDir?: string;
69
+ designDoc?: string;
70
+ };
71
+ }
@@ -0,0 +1,32 @@
1
+ type BrowserFrameCallback = (timestamp: number) => void;
2
+
3
+ export function scheduleBrowserFrame(callback: BrowserFrameCallback) {
4
+ if (canUseAnimationFrame()) {
5
+ const frame = window.requestAnimationFrame(callback);
6
+ return () => window.cancelAnimationFrame(frame);
7
+ }
8
+
9
+ const timer = window.setTimeout(() => callback(now()), 0);
10
+ return () => window.clearTimeout(timer);
11
+ }
12
+
13
+ export function waitForBrowserFrame() {
14
+ return new Promise<void>((resolve) => {
15
+ scheduleBrowserFrame(() => resolve());
16
+ });
17
+ }
18
+
19
+ function canUseAnimationFrame() {
20
+ return (
21
+ typeof window !== "undefined" &&
22
+ typeof window.requestAnimationFrame === "function" &&
23
+ typeof document !== "undefined" &&
24
+ document.visibilityState !== "hidden"
25
+ );
26
+ }
27
+
28
+ function now() {
29
+ return typeof performance !== "undefined" && typeof performance.now === "function"
30
+ ? performance.now()
31
+ : Date.now();
32
+ }