@open-press/cli 0.7.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/README.md +29 -13
  2. package/dist/cli.js +44 -195
  3. package/package.json +4 -5
  4. package/template/core/AGENTS.md +18 -14
  5. package/template/core/CHANGELOG.md +57 -9
  6. package/template/core/README.md +6 -3
  7. package/template/core/engine/cli.mjs +8 -8
  8. package/template/core/engine/commands/_shared.mjs +37 -15
  9. package/template/core/engine/commands/dev.mjs +2 -2
  10. package/template/core/engine/commands/image.mjs +29 -0
  11. package/template/core/engine/commands/skills-sync.mjs +71 -0
  12. package/template/core/engine/commands/typecheck.mjs +63 -1
  13. package/template/core/engine/commands/upgrade.mjs +3 -3
  14. package/template/core/engine/document-export.mjs +1 -1
  15. package/template/core/engine/output/chrome-pdf.mjs +110 -3
  16. package/template/core/engine/output/static-server.mjs +87 -9
  17. package/template/core/engine/react/comment-endpoint.mjs +13 -39
  18. package/template/core/engine/react/comment-marker.mjs +43 -19
  19. package/template/core/engine/react/document-entry.mjs +46 -28
  20. package/template/core/engine/react/document-export.mjs +328 -164
  21. package/template/core/engine/react/http-json.mjs +24 -0
  22. package/template/core/engine/react/mdx-compile.mjs +126 -3
  23. package/template/core/engine/react/measurement-css.mjs +114 -1
  24. package/template/core/engine/react/object-entities.mjs +204 -0
  25. package/template/core/engine/react/pagination/allocator.mjs +48 -3
  26. package/template/core/engine/react/pagination.mjs +1 -1
  27. package/template/core/engine/react/pipeline/allocate.mjs +41 -72
  28. package/template/core/engine/react/pipeline/frame-measurement.mjs +6 -0
  29. package/template/core/engine/react/press-tree-inspection.mjs +172 -0
  30. package/template/core/engine/react/project-asset-endpoint.mjs +6 -24
  31. package/template/core/engine/react/source-edit-endpoint.d.mts +10 -0
  32. package/template/core/engine/react/source-edit-endpoint.mjs +75 -0
  33. package/template/core/engine/react/sources/mdx-resolver.mjs +13 -15
  34. package/template/core/engine/react/style-discovery.mjs +23 -8
  35. package/template/core/engine/runtime/config.d.mts +8 -0
  36. package/template/core/engine/runtime/config.mjs +57 -60
  37. package/template/core/engine/runtime/file-utils.mjs +9 -1
  38. package/template/core/engine/runtime/file-walk.mjs +22 -0
  39. package/template/core/engine/runtime/inspection.mjs +1 -20
  40. package/template/core/engine/runtime/page-geometry.mjs +131 -0
  41. package/template/core/engine/runtime/path-utils.mjs +20 -0
  42. package/template/core/engine/runtime/source-text-tools.d.mts +102 -0
  43. package/template/core/engine/runtime/source-text-tools.mjs +551 -16
  44. package/template/core/engine/runtime/source-workspace.mjs +16 -34
  45. package/template/core/engine/runtime/validation.mjs +19 -10
  46. package/template/core/openpress.config.mjs +3 -7
  47. package/template/core/package.json +3 -5
  48. package/template/core/src/main.tsx +2 -2
  49. package/template/core/src/openpress/app/OpenPressApp.tsx +296 -0
  50. package/template/core/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +20 -9
  51. package/template/core/src/openpress/app/WorkspaceGalleryPage.tsx +219 -0
  52. package/template/core/src/openpress/app/index.ts +2 -0
  53. package/template/core/src/openpress/core/Frame.tsx +26 -15
  54. package/template/core/src/openpress/core/FrameContext.tsx +10 -3
  55. package/template/core/src/openpress/core/MdxArea.tsx +11 -12
  56. package/template/core/src/openpress/core/Press.tsx +25 -4
  57. package/template/core/src/openpress/core/Workspace.tsx +36 -0
  58. package/template/core/src/openpress/core/cn.ts +4 -0
  59. package/template/core/src/openpress/core/index.tsx +11 -3
  60. package/template/core/src/openpress/core/primitives.tsx +74 -6
  61. package/template/core/src/openpress/core/types.ts +94 -41
  62. package/template/core/src/openpress/core/useSource.ts +1 -1
  63. package/template/core/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
  64. package/template/core/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
  65. package/template/core/src/openpress/{types.ts → document-model/documentTypes.ts} +51 -0
  66. package/template/core/src/openpress/document-model/index.ts +7 -0
  67. package/template/core/src/openpress/document-model/objectEntityModel.ts +55 -0
  68. package/template/core/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
  69. package/template/core/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
  70. package/template/core/src/openpress/document-model/workspaceManifestModel.ts +57 -0
  71. package/template/core/src/openpress/manuscript/index.tsx +49 -7
  72. package/template/core/src/openpress/mdx/index.ts +15 -7
  73. package/template/core/src/openpress/reader/PageThumbnailsPanel.tsx +168 -0
  74. package/template/core/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
  75. package/template/core/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
  76. package/template/core/src/openpress/reader/index.ts +11 -0
  77. package/template/core/src/openpress/reader/pageViewportScaleModel.ts +73 -0
  78. package/template/core/src/openpress/reader/readerTypes.ts +4 -0
  79. package/template/core/src/openpress/reader/usePageViewportScale.ts +119 -0
  80. package/template/core/src/openpress/reader/usePanelState.ts +56 -0
  81. package/template/core/src/openpress/reader/useReaderHashSync.ts +61 -0
  82. package/template/core/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
  83. package/template/core/src/openpress/reader/useReaderRuntime.ts +146 -0
  84. package/template/core/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
  85. package/template/core/src/openpress/shared/Panel.tsx +77 -0
  86. package/template/core/src/openpress/shared/index.ts +4 -0
  87. package/template/core/src/openpress/shared/numberUtils.ts +3 -0
  88. package/template/core/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
  89. package/template/core/src/openpress/workbench/Workbench.tsx +506 -0
  90. package/template/core/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
  91. package/template/core/src/openpress/workbench/actions/ExportImageControl.tsx +96 -0
  92. package/template/core/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
  93. package/template/core/src/openpress/workbench/actions/SearchControl.tsx +345 -0
  94. package/template/core/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
  95. package/template/core/src/openpress/workbench/actions/index.ts +6 -0
  96. package/template/core/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
  97. package/template/core/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
  98. package/template/core/src/openpress/workbench/dialog/index.ts +1 -0
  99. package/template/core/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
  100. package/template/core/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
  101. package/template/core/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
  102. package/template/core/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
  103. package/template/core/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
  104. package/template/core/src/openpress/workbench/document/index.ts +10 -0
  105. package/template/core/src/openpress/workbench/index.ts +2 -0
  106. package/template/core/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
  107. package/template/core/src/openpress/workbench/inspector/index.ts +5 -0
  108. package/template/core/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
  109. package/template/core/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
  110. package/template/core/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
  111. package/template/core/src/openpress/workbench/inspector/useInspectorComments.ts +254 -0
  112. package/template/core/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
  113. package/template/core/src/openpress/workbench/mentions/index.ts +2 -0
  114. package/template/core/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
  115. package/template/core/src/openpress/workbench/panels/Panel.tsx +1 -0
  116. package/template/core/src/openpress/workbench/panels/PendingCommentsPanel.tsx +80 -0
  117. package/template/core/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
  118. package/template/core/src/openpress/workbench/panels/index.ts +3 -0
  119. package/template/core/src/openpress/workbench/project/ProjectEntryPanel.tsx +525 -0
  120. package/template/core/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
  121. package/template/core/src/openpress/workbench/project/index.ts +2 -0
  122. package/template/core/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
  123. package/template/core/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
  124. package/template/core/src/openpress/workbench/shell/index.ts +1 -0
  125. package/template/core/src/openpress/workbench/workbenchFormatters.ts +120 -0
  126. package/template/core/src/openpress/workbench/workbenchTypes.ts +35 -0
  127. package/template/core/src/styles/openpress/print-route.css +0 -2
  128. package/template/core/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
  129. package/template/core/src/styles/openpress/public-viewer.css +25 -320
  130. package/template/core/src/styles/openpress/reader-runtime.css +252 -55
  131. package/template/core/src/styles/openpress/responsive.css +145 -270
  132. package/template/core/src/styles/openpress/workbench-panels.css +327 -178
  133. package/template/core/src/styles/openpress/workbench.css +986 -451
  134. package/template/core/src/styles/openpress/workspace-gallery.css +300 -0
  135. package/template/core/src/styles/openpress.css +2 -1
  136. package/template/core/tsconfig.json +1 -1
  137. package/template/core/vite.config.ts +50 -0
  138. package/template/core/engine/commands/init.mjs +0 -24
  139. package/template/core/engine/init.mjs +0 -90
  140. package/template/core/src/openpress/App.tsx +0 -127
  141. package/template/core/src/openpress/inspector.ts +0 -282
  142. package/template/core/src/openpress/projectWorkspace.tsx +0 -919
  143. package/template/core/src/openpress/readerRuntime.ts +0 -230
  144. package/template/core/src/openpress/workbench.tsx +0 -1265
  145. package/template/core/src/openpress/workbenchTypes.ts +0 -4
  146. package/template/packs/academic-paper/document/chapters/01-introduction/content/01-introduction.mdx +0 -35
  147. package/template/packs/academic-paper/document/chapters/02-methods/content/01-methods.mdx +0 -50
  148. package/template/packs/academic-paper/document/chapters/03-results-and-discussion/content/01-results.mdx +0 -47
  149. package/template/packs/academic-paper/document/chapters/04-acknowledgment/content/01-acknowledgment.mdx +0 -26
  150. package/template/packs/academic-paper/document/chapters/05-references/content/01-references.mdx +0 -32
  151. package/template/packs/academic-paper/document/components/ChapterOpenerVisual/index.tsx +0 -76
  152. package/template/packs/academic-paper/document/components/Page.tsx +0 -60
  153. package/template/packs/academic-paper/document/components/TokenSwatchGrid/index.tsx +0 -46
  154. package/template/packs/academic-paper/document/components/TokenSwatchGrid/style.css +0 -63
  155. package/template/packs/academic-paper/document/components/TypeSpecimen/index.tsx +0 -38
  156. package/template/packs/academic-paper/document/components/TypeSpecimen/style.css +0 -111
  157. package/template/packs/academic-paper/document/design.md +0 -279
  158. package/template/packs/academic-paper/document/index.tsx +0 -123
  159. package/template/packs/academic-paper/document/media/README.md +0 -13
  160. package/template/packs/academic-paper/document/media/figure-placeholder.svg +0 -9
  161. package/template/packs/academic-paper/document/openpress.config.mjs +0 -26
  162. package/template/packs/academic-paper/document/theme/README.md +0 -11
  163. package/template/packs/academic-paper/document/theme/base/page-contract.css +0 -522
  164. package/template/packs/academic-paper/document/theme/base/print.css +0 -93
  165. package/template/packs/academic-paper/document/theme/base/typography.css +0 -333
  166. package/template/packs/academic-paper/document/theme/fonts.css +0 -3
  167. package/template/packs/academic-paper/document/theme/page-surfaces/back-cover.css +0 -43
  168. package/template/packs/academic-paper/document/theme/page-surfaces/chapter-opener.css +0 -205
  169. package/template/packs/academic-paper/document/theme/page-surfaces/cover.css +0 -294
  170. package/template/packs/academic-paper/document/theme/page-surfaces/toc.css +0 -149
  171. package/template/packs/academic-paper/document/theme/patterns/_chart-frame.css +0 -49
  172. package/template/packs/academic-paper/document/theme/patterns/figure-grid.css +0 -68
  173. package/template/packs/academic-paper/document/theme/patterns/table-utilities.css +0 -66
  174. package/template/packs/academic-paper/document/theme/shell/reader-controls.css +0 -761
  175. package/template/packs/academic-paper/document/theme/tokens.css +0 -80
  176. package/template/packs/academic-paper/openpress.config.mjs +0 -5
  177. package/template/packs/claude-document/document/chapters/01-document-shape/content/01-document-shape.mdx +0 -51
  178. package/template/packs/claude-document/document/chapters/02-review-loop/content/01-review-loop.mdx +0 -31
  179. package/template/packs/claude-document/document/components/ChapterOpenerVisual.tsx +0 -96
  180. package/template/packs/claude-document/document/components/Page.tsx +0 -37
  181. package/template/packs/claude-document/document/design.md +0 -142
  182. package/template/packs/claude-document/document/index.tsx +0 -94
  183. package/template/packs/claude-document/document/media/README.md +0 -13
  184. package/template/packs/claude-document/document/openpress.config.mjs +0 -26
  185. package/template/packs/claude-document/document/theme/README.md +0 -15
  186. package/template/packs/claude-document/document/theme/base/page-contract.css +0 -525
  187. package/template/packs/claude-document/document/theme/base/print.css +0 -93
  188. package/template/packs/claude-document/document/theme/base/typography.css +0 -612
  189. package/template/packs/claude-document/document/theme/fonts.css +0 -4
  190. package/template/packs/claude-document/document/theme/page-surfaces/back-cover.css +0 -72
  191. package/template/packs/claude-document/document/theme/page-surfaces/chapter-opener.css +0 -236
  192. package/template/packs/claude-document/document/theme/page-surfaces/cover.css +0 -309
  193. package/template/packs/claude-document/document/theme/page-surfaces/toc.css +0 -225
  194. package/template/packs/claude-document/document/theme/patterns/_chart-frame.css +0 -53
  195. package/template/packs/claude-document/document/theme/patterns/figure-grid.css +0 -68
  196. package/template/packs/claude-document/document/theme/patterns/table-utilities.css +0 -66
  197. package/template/packs/claude-document/document/theme/shell/reader-controls.css +0 -789
  198. package/template/packs/claude-document/document/theme/tokens.css +0 -89
  199. package/template/packs/claude-document/openpress.config.mjs +0 -5
  200. package/template/packs/editorial-monograph/document/chapters/01-product-and-use-cases/content/01-product-and-use-cases.mdx +0 -31
  201. package/template/packs/editorial-monograph/document/chapters/02-workflow/content/01-workflow.mdx +0 -89
  202. package/template/packs/editorial-monograph/document/chapters/03-agent-skills-contributors/content/01-agent-skills-contributors.mdx +0 -51
  203. package/template/packs/editorial-monograph/document/chapters/04-validation-deploy/content/01-validation-deploy.mdx +0 -39
  204. package/template/packs/editorial-monograph/document/components/ChapterOpenerVisual/index.tsx +0 -76
  205. package/template/packs/editorial-monograph/document/components/Page.tsx +0 -37
  206. package/template/packs/editorial-monograph/document/components/TokenSwatchGrid/index.tsx +0 -46
  207. package/template/packs/editorial-monograph/document/components/TokenSwatchGrid/style.css +0 -63
  208. package/template/packs/editorial-monograph/document/components/TypeSpecimen/index.tsx +0 -38
  209. package/template/packs/editorial-monograph/document/components/TypeSpecimen/style.css +0 -111
  210. package/template/packs/editorial-monograph/document/design.md +0 -279
  211. package/template/packs/editorial-monograph/document/index.tsx +0 -97
  212. package/template/packs/editorial-monograph/document/media/README.md +0 -13
  213. package/template/packs/editorial-monograph/document/openpress.config.mjs +0 -26
  214. package/template/packs/editorial-monograph/document/theme/README.md +0 -11
  215. package/template/packs/editorial-monograph/document/theme/base/page-contract.css +0 -505
  216. package/template/packs/editorial-monograph/document/theme/base/print.css +0 -93
  217. package/template/packs/editorial-monograph/document/theme/base/typography.css +0 -336
  218. package/template/packs/editorial-monograph/document/theme/fonts.css +0 -3
  219. package/template/packs/editorial-monograph/document/theme/page-surfaces/back-cover.css +0 -43
  220. package/template/packs/editorial-monograph/document/theme/page-surfaces/chapter-opener.css +0 -205
  221. package/template/packs/editorial-monograph/document/theme/page-surfaces/cover.css +0 -147
  222. package/template/packs/editorial-monograph/document/theme/page-surfaces/toc.css +0 -149
  223. package/template/packs/editorial-monograph/document/theme/patterns/_chart-frame.css +0 -49
  224. package/template/packs/editorial-monograph/document/theme/patterns/figure-grid.css +0 -68
  225. package/template/packs/editorial-monograph/document/theme/patterns/table-utilities.css +0 -66
  226. package/template/packs/editorial-monograph/document/theme/shell/reader-controls.css +0 -761
  227. package/template/packs/editorial-monograph/document/theme/tokens.css +0 -80
  228. package/template/packs/editorial-monograph/openpress.config.mjs +0 -5
  229. /package/template/core/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
  230. /package/template/core/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
  231. /package/template/core/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
  232. /package/template/core/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
  233. /package/template/core/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
  234. /package/template/core/src/openpress/{projectSources.ts → workbench/project/projectSourceModel.ts} +0 -0
@@ -0,0 +1,525 @@
1
+ import { memo, useState, type CSSProperties } from "react";
2
+ import { Component as ComponentIcon, Images, Palette, type LucideIcon } from "lucide-react";
3
+ import type { BookmarkItem, BookmarkSubItem, MediaAssetItem } from "../../document-model";
4
+ import { projectSourceDirectoryPath, PROJECT_SOURCES } from "./projectSourceModel";
5
+ import type { BlockSource } from "../../document-model";
6
+ import type { DisplayPage } from "../../reader";
7
+ import { Panel } from "../panels/Panel";
8
+ import { ProjectPreviewDialog } from "./ProjectPreviewDialog";
9
+ import {
10
+ createProjectObjectEntityId,
11
+ type ProjectMentionItem,
12
+ type ProjectPanelPreview,
13
+ } from "./projectPreviewTypes";
14
+
15
+ export { createProjectObjectEntityId } from "./projectPreviewTypes";
16
+ export type { ProjectMentionItem, ProjectPanelPreview } from "./projectPreviewTypes";
17
+
18
+ export const PROJECT_VISUAL_SYSTEM_KEY = "visual-system";
19
+ export const PROJECT_IMAGE_GALLERY_KEY = "image-gallery";
20
+ export const PROJECT_COMPONENT_LIBRARY_KEY = "component-library";
21
+
22
+ export type ProjectComponentUsage = {
23
+ count: number;
24
+ pageIndexes: number[];
25
+ html: string;
26
+ previews: ProjectComponentPreview[];
27
+ };
28
+
29
+ export type ProjectComponentPreview = {
30
+ name: string;
31
+ html: string;
32
+ pageIndex: number;
33
+ };
34
+
35
+ export function createProjectComponentUsages(pages: DisplayPage[]): Map<string, ProjectComponentUsage> {
36
+ const usages = new Map<string, ProjectComponentUsage>();
37
+ pages.forEach((page) => {
38
+ const html = String(page.html ?? "");
39
+ const pageIndex = page.pageNumber - 1;
40
+ for (const block of extractRenderedComponentBlocks(html)) {
41
+ const current = usages.get(block.name) ?? { count: 0, pageIndexes: [], html: block.html, previews: [] };
42
+ current.count += 1;
43
+ if (!current.html) current.html = block.html;
44
+ if (!current.pageIndexes.includes(pageIndex)) current.pageIndexes.push(pageIndex);
45
+ current.previews.push({ name: block.name, html: block.html, pageIndex });
46
+ usages.set(block.name, current);
47
+ }
48
+ });
49
+ return usages;
50
+ }
51
+
52
+ export function createProjectMentionItems(
53
+ mediaAssets: MediaAssetItem[],
54
+ componentUsages: Map<string, ProjectComponentUsage>,
55
+ bookmarks: BookmarkItem[] = [],
56
+ ): ProjectMentionItem[] {
57
+ const referenceItems = createBookmarkMentionItems(bookmarks);
58
+
59
+ const mediaItems: ProjectMentionItem[] = mediaAssets.map((item) => ({
60
+ trigger: "@",
61
+ value: mediaMention(item.fileName),
62
+ label: item.fileName,
63
+ meta: item.usageCount > 0 ? `media · P${String(item.pageIndex + 1).padStart(2, "0")}` : "media · unused",
64
+ kind: "media",
65
+ }));
66
+
67
+ const componentItems: ProjectMentionItem[] = Array.from(componentUsages.entries())
68
+ .sort(([a], [b]) => a.localeCompare(b, "zh-Hant"))
69
+ .map(([name, usage]) => ({
70
+ trigger: "@",
71
+ value: componentMention(name),
72
+ label: name,
73
+ meta: `component · ${usage.count}`,
74
+ kind: "component",
75
+ }));
76
+
77
+ return [...PROJECT_SKILL_MENTIONS, ...referenceItems, ...mediaItems, ...componentItems];
78
+ }
79
+
80
+ function ProjectEntryPanelImpl({
81
+ mediaAssets,
82
+ componentUsages,
83
+ mentionItems,
84
+ currentSource,
85
+ onCommentSubmitted,
86
+ }: {
87
+ mediaAssets: MediaAssetItem[];
88
+ componentUsages: Map<string, ProjectComponentUsage>;
89
+ mentionItems: ProjectMentionItem[];
90
+ currentSource: BlockSource | undefined;
91
+ onCommentSubmitted?: () => void | Promise<void>;
92
+ }) {
93
+ const [preview, setPreview] = useState<ProjectPanelPreview | null>(null);
94
+ const componentItems = Array.from(componentUsages.entries())
95
+ .sort(([a], [b]) => a.localeCompare(b, "zh-Hant"))
96
+ .slice(0, 8);
97
+ const mediaItems = mediaAssets
98
+ .slice(0, 10)
99
+ .map((item) => ({
100
+ fileName: item.fileName,
101
+ src: item.src,
102
+ mention: mediaMention(item.fileName),
103
+ meta: item.usageCount > 0 ? `P${String(item.pageIndex + 1).padStart(2, "0")}` : "unused",
104
+ }));
105
+
106
+ return (
107
+ <Panel className="openpress-project-panel openpress-panel--compact" aria-label="Project tools">
108
+ <Panel.Section className="openpress-project-tool-block" aria-label="媒體素材">
109
+ <Panel.SectionTitle>Media</Panel.SectionTitle>
110
+ {mediaItems.length > 0 ? (
111
+ <div className="openpress-project-asset-list">
112
+ {mediaItems.map((item) => (
113
+ <button
114
+ type="button"
115
+ className="openpress-project-asset"
116
+ data-openpress-object-id={createProjectObjectEntityId("media", item.fileName)}
117
+ key={`${item.src}-${item.fileName}`}
118
+ onClick={() => setPreview({ kind: "media", title: item.fileName, src: item.src })}
119
+ >
120
+ <span className="openpress-project-asset-thumb">
121
+ <img src={item.src} alt="" loading="lazy" />
122
+ </span>
123
+ <span className="openpress-project-asset-body">
124
+ <strong>{item.fileName}</strong>
125
+ </span>
126
+ </button>
127
+ ))}
128
+ </div>
129
+ ) : (
130
+ <Panel.Empty>尚無圖片</Panel.Empty>
131
+ )}
132
+ </Panel.Section>
133
+
134
+ <Panel.Section className="openpress-project-tool-block" aria-label="Components">
135
+ <Panel.SectionTitle>Components</Panel.SectionTitle>
136
+ {componentItems.length > 0 ? (
137
+ <div className="openpress-project-component-mention-list">
138
+ {componentItems.map(([name, usage]) => (
139
+ <button
140
+ type="button"
141
+ data-openpress-object-id={createProjectObjectEntityId("component", name)}
142
+ key={name}
143
+ onClick={() => setPreview({ kind: "component", title: name, html: usage.html })}
144
+ >
145
+ <ComponentIcon aria-hidden="true" />
146
+ <span>
147
+ <strong>{name}</strong>
148
+ </span>
149
+ </button>
150
+ ))}
151
+ </div>
152
+ ) : (
153
+ <Panel.Empty>尚無 component</Panel.Empty>
154
+ )}
155
+ </Panel.Section>
156
+ {preview ? (
157
+ <ProjectPreviewDialog
158
+ key={`${preview.kind}-${preview.title}`}
159
+ preview={preview}
160
+ onClose={() => setPreview(null)}
161
+ />
162
+ ) : null}
163
+ </Panel>
164
+ );
165
+ }
166
+
167
+ export const ProjectEntryPanel = memo(ProjectEntryPanelImpl);
168
+ ProjectEntryPanel.displayName = "ProjectEntryPanel";
169
+
170
+ export function ProjectPreviewPanel({
171
+ mediaAssets,
172
+ componentUsages,
173
+ selectedKey,
174
+ }: {
175
+ mediaAssets: MediaAssetItem[];
176
+ componentUsages: Map<string, ProjectComponentUsage>;
177
+ selectedKey: string | null;
178
+ }) {
179
+ if (!selectedKey || selectedKey === PROJECT_VISUAL_SYSTEM_KEY) {
180
+ return <ProjectVisualSystem />;
181
+ }
182
+
183
+ if (selectedKey === PROJECT_IMAGE_GALLERY_KEY) {
184
+ return <ProjectImageGallery mediaAssets={mediaAssets} />;
185
+ }
186
+
187
+ if (selectedKey === PROJECT_COMPONENT_LIBRARY_KEY) {
188
+ return <ProjectComponentLibrary usages={componentUsages} />;
189
+ }
190
+
191
+ return <ProjectVisualSystem />;
192
+ }
193
+
194
+ function ProjectPanelButton({
195
+ icon: Icon,
196
+ label,
197
+ meta,
198
+ active,
199
+ onClick,
200
+ }: {
201
+ icon: LucideIcon;
202
+ label: string;
203
+ meta: string;
204
+ active: boolean;
205
+ onClick: () => void;
206
+ }) {
207
+ return (
208
+ <button
209
+ type="button"
210
+ className={`bookmark-item bookmark-h2 openpress-project-entry${active ? " is-active" : ""}`}
211
+ aria-pressed={active}
212
+ onClick={onClick}
213
+ >
214
+ <span className="bookmark-index openpress-project-entry-icon"><Icon aria-hidden="true" /></span>
215
+ <span className="bookmark-title">
216
+ <span>{label}</span>
217
+ <small>{meta}</small>
218
+ </span>
219
+ </button>
220
+ );
221
+ }
222
+
223
+ function ProjectVisualSystem() {
224
+ return (
225
+ <section className="openpress-project-preview-panel" aria-label="專案">
226
+ <article className="openpress-project-visual-system" aria-label="Visual System">
227
+ <ProjectSectionHeader title="Visual System" minimal />
228
+
229
+ <div className="openpress-project-visual-grid">
230
+ <section className="openpress-project-visual-card openpress-project-visual-card--typography" aria-label="Typography">
231
+ <header>
232
+ <span>Typography</span>
233
+ <strong>閱讀層級</strong>
234
+ </header>
235
+ <div className="openpress-project-type-specimen">
236
+ <p className="openpress-project-type-kicker">Course Notes</p>
237
+ <h2>Data Structures</h2>
238
+ <h3>Linked List 與 Tree Traversal</h3>
239
+ <h4>Pointer / Node / Recursive Thinking</h4>
240
+ <p>以 C/C++ 實作資料結構,整理概念、表示法與操作流程。</p>
241
+ <code>struct Node *next = head;</code>
242
+ </div>
243
+ </section>
244
+
245
+ <section className="openpress-project-visual-card" aria-label="Color Palette">
246
+ <header>
247
+ <span>Palette</span>
248
+ <strong>色票配置</strong>
249
+ </header>
250
+ <div className="openpress-project-swatch-grid">
251
+ {PROJECT_COLOR_SWATCHES.map((item) => (
252
+ <div className="openpress-project-swatch" key={item.token}>
253
+ <span
254
+ className="openpress-project-swatch-chip"
255
+ style={{ "--openpress-project-swatch": `var(${item.token})` } as CSSProperties}
256
+ aria-hidden="true"
257
+ />
258
+ <strong>{item.label}</strong>
259
+ <code>{item.token.replace("--openpress-", "")}</code>
260
+ </div>
261
+ ))}
262
+ </div>
263
+ </section>
264
+
265
+ <section className="openpress-project-visual-card openpress-project-visual-card--surfaces" aria-label="Surfaces">
266
+ <header>
267
+ <span>Surfaces</span>
268
+ <strong>區塊背景</strong>
269
+ </header>
270
+ <div className="openpress-project-surface-preview">
271
+ <div className="openpress-project-surface-paper">
272
+ <span>Page paper</span>
273
+ </div>
274
+ <div className="openpress-project-surface-block">
275
+ <span>Figure / Code block</span>
276
+ </div>
277
+ </div>
278
+ </section>
279
+ </div>
280
+ </article>
281
+ </section>
282
+ );
283
+ }
284
+
285
+ function ProjectComponentLibrary({
286
+ usages,
287
+ }: {
288
+ usages: Map<string, ProjectComponentUsage>;
289
+ }) {
290
+ const previewItems = createComponentPreviewItems(usages);
291
+ return (
292
+ <section className="openpress-project-preview-panel" aria-label="專案">
293
+ <article className="openpress-project-component-viewer" aria-label={PROJECT_SOURCES.components.label}>
294
+ <ProjectSectionHeader
295
+ title="Rendered Components"
296
+ description="文件目前實際渲染出的 component、圖表與示意圖狀態。"
297
+ stats={[
298
+ ["Kinds", String(usages.size)],
299
+ ["Renders", String(previewItems.length)],
300
+ ]}
301
+ />
302
+ {previewItems.length > 0 ? (
303
+ <div className="openpress-project-component-list" aria-label="rendered content block list">
304
+ {previewItems.map((item) => (
305
+ <figure className="openpress-project-component-preview-row" key={`${item.name}-${item.index}`}>
306
+ <figcaption>
307
+ <span>{item.name}</span>
308
+ <small>P{String(item.preview.pageIndex + 1).padStart(2, "0")}</small>
309
+ </figcaption>
310
+ <div
311
+ className="openpress-project-component-preview"
312
+ dangerouslySetInnerHTML={{ __html: item.preview.html }}
313
+ />
314
+ </figure>
315
+ ))}
316
+ </div>
317
+ ) : (
318
+ <p className="openpress-project-empty">目前文件尚未渲染任何內容區塊。</p>
319
+ )}
320
+ </article>
321
+ </section>
322
+ );
323
+ }
324
+
325
+ function ProjectImageGallery({
326
+ mediaAssets,
327
+ }: {
328
+ mediaAssets: MediaAssetItem[];
329
+ }) {
330
+ const usedCount = mediaAssets.filter((item) => item.usageCount > 0).length;
331
+ const unreferencedAssets = mediaAssets.filter((item) => item.usageCount === 0);
332
+ const referencedAssets = mediaAssets.filter((item) => item.usageCount > 0);
333
+ return (
334
+ <section className="openpress-project-preview-panel" aria-label="專案">
335
+ <article className="openpress-project-gallery-viewer" aria-label="Image Gallery">
336
+ <ProjectSectionHeader
337
+ title="Media Library"
338
+ description={projectSourceDirectoryPath("media")}
339
+ stats={[
340
+ ["Files", String(mediaAssets.length)],
341
+ ["Used", String(usedCount)],
342
+ ]}
343
+ />
344
+ {mediaAssets.length > 0 ? (
345
+ <div className="openpress-project-media-sections" aria-label="media gallery">
346
+ <ProjectMediaSection title="未引用" assets={unreferencedAssets} />
347
+ <ProjectMediaSection title="已引用" assets={referencedAssets} />
348
+ </div>
349
+ ) : (
350
+ <p className="openpress-project-empty">{projectSourceDirectoryPath("media")} 尚未有可預覽素材。</p>
351
+ )}
352
+ </article>
353
+ </section>
354
+ );
355
+ }
356
+
357
+ function ProjectSectionHeader({
358
+ title,
359
+ description,
360
+ stats,
361
+ minimal = false,
362
+ }: {
363
+ title: string;
364
+ description?: string;
365
+ stats?: Array<[string, string]>;
366
+ minimal?: boolean;
367
+ }) {
368
+ return (
369
+ <header className={`openpress-project-section-header${minimal ? " openpress-project-section-header--minimal" : ""}`}>
370
+ <div>
371
+ <h2>{title}</h2>
372
+ {description ? <span>{description}</span> : null}
373
+ </div>
374
+ {!minimal && stats?.length ? (
375
+ <dl>
376
+ {stats.map(([label, value]) => (
377
+ <div key={label}>
378
+ <dt>{label}</dt>
379
+ <dd>{value}</dd>
380
+ </div>
381
+ ))}
382
+ </dl>
383
+ ) : null}
384
+ </header>
385
+ );
386
+ }
387
+
388
+ function ProjectMediaSection({
389
+ title,
390
+ assets,
391
+ }: {
392
+ title: string;
393
+ assets: MediaAssetItem[];
394
+ }) {
395
+ return (
396
+ <section className="openpress-project-media-section" aria-label={title}>
397
+ <header className="openpress-project-media-section-header">
398
+ <h3>{title}</h3>
399
+ </header>
400
+ {assets.length > 0 ? (
401
+ <div className="openpress-project-media-gallery">
402
+ {assets.map((item) => (
403
+ <figure className="openpress-project-media-card" data-unused={item.usageCount === 0 ? "true" : "false"} key={item.id}>
404
+ <img src={item.src} alt="" loading="lazy" />
405
+ <figcaption>
406
+ <strong>{item.fileName}</strong>
407
+ </figcaption>
408
+ </figure>
409
+ ))}
410
+ </div>
411
+ ) : (
412
+ <p className="openpress-project-media-section-empty">沒有{title}圖片。</p>
413
+ )}
414
+ </section>
415
+ );
416
+ }
417
+
418
+ function createComponentPreviewItems(usages: Map<string, ProjectComponentUsage>) {
419
+ return Array.from(usages.entries())
420
+ .flatMap(([name, usage]) => usage.previews.map((preview, index) => ({ name, preview, index })))
421
+ .filter((item) => Boolean(item.preview.html))
422
+ .sort((a, b) => {
423
+ const pageDelta = a.preview.pageIndex - b.preview.pageIndex;
424
+ return pageDelta || a.name.localeCompare(b.name, "zh-Hant") || a.index - b.index;
425
+ });
426
+ }
427
+
428
+ function extractRenderedComponentBlocks(html: string) {
429
+ const blocks: Array<{ name: string; html: string }> = [];
430
+ const openTagPattern = /<(figure|section|article|div)\b[^>]*data-openpress-component="([^"]+)"[^>]*>/g;
431
+ for (const match of html.matchAll(openTagPattern)) {
432
+ const tagName = match[1];
433
+ const componentName = match[2];
434
+ const start = match.index ?? 0;
435
+ const end = findClosingTagEnd(html, tagName, start + match[0].length);
436
+ blocks.push({
437
+ name: componentName,
438
+ html: end > start ? html.slice(start, end) : match[0],
439
+ });
440
+ }
441
+ return blocks;
442
+ }
443
+
444
+ function findClosingTagEnd(html: string, tagName: string, fromIndex: number) {
445
+ const tagPattern = new RegExp(`</?${tagName}\\b[^>]*>`, "gi");
446
+ tagPattern.lastIndex = fromIndex;
447
+ let depth = 1;
448
+ let match: RegExpExecArray | null;
449
+ while ((match = tagPattern.exec(html))) {
450
+ if (match[0].startsWith("</")) {
451
+ depth -= 1;
452
+ } else if (!match[0].endsWith("/>")) {
453
+ depth += 1;
454
+ }
455
+ if (depth === 0) return tagPattern.lastIndex;
456
+ }
457
+ return -1;
458
+ }
459
+
460
+ function mediaMention(fileName: string) {
461
+ return `@media/${fileName}`;
462
+ }
463
+
464
+ function componentMention(name: string) {
465
+ return `@component/${name}`;
466
+ }
467
+
468
+ function createBookmarkMentionItems(bookmarks: BookmarkItem[]): ProjectMentionItem[] {
469
+ return bookmarks
470
+ .filter((item) => item.label !== "00")
471
+ .flatMap((chapter) => [
472
+ bookmarkMentionItem("chapter", chapter),
473
+ ...chapter.subs.map((section) => bookmarkMentionItem("section", section)),
474
+ ]);
475
+ }
476
+
477
+ function bookmarkMentionItem(kind: "chapter" | "section", item: BookmarkItem | BookmarkSubItem): ProjectMentionItem {
478
+ const label = item.label ? `${item.label} ` : "";
479
+ return {
480
+ trigger: "@",
481
+ value: `@${kind}/${bookmarkMentionSlug(item)}`,
482
+ label: `${label}${item.title}`,
483
+ meta: `${kind === "chapter" ? "chapter" : "section"} · P${String(item.pageIndex + 1).padStart(2, "0")}`,
484
+ kind,
485
+ };
486
+ }
487
+
488
+ function bookmarkMentionSlug(item: BookmarkItem | BookmarkSubItem) {
489
+ const parts = [item.label, item.title]
490
+ .filter(Boolean)
491
+ .map((part) => mentionSlugPart(String(part)));
492
+ return parts.filter(Boolean).join("-") || item.id;
493
+ }
494
+
495
+ function mentionSlugPart(value: string) {
496
+ return value
497
+ .trim()
498
+ .replace(/\s+/g, "-")
499
+ .replace(/[^\p{L}\p{N}._-]+/gu, "-")
500
+ .replace(/-+/g, "-")
501
+ .replace(/^-|-$/g, "");
502
+ }
503
+
504
+
505
+ const PROJECT_SKILL_MENTIONS: ProjectMentionItem[] = [
506
+ { trigger: "/", value: "/insert-image", label: "insert-image", meta: "skill", kind: "skill" },
507
+ { trigger: "/", value: "/redraw-figure", label: "redraw-figure", meta: "skill", kind: "skill" },
508
+ { trigger: "/", value: "/rewrite-section", label: "rewrite-section", meta: "skill", kind: "skill" },
509
+ { trigger: "/", value: "/apply-comments", label: "apply-comments", meta: "skill", kind: "skill" },
510
+ { trigger: "/", value: "/apply-style", label: "apply-style", meta: "skill", kind: "skill" },
511
+ { trigger: "/", value: "/fix-code", label: "fix-code", meta: "skill", kind: "skill" },
512
+ ];
513
+
514
+ const PROJECT_COLOR_SWATCHES = [
515
+ { label: "Document", token: "--openpress-color-document" },
516
+ { label: "Paper", token: "--openpress-color-paper" },
517
+ { label: "Ink", token: "--openpress-color-ink" },
518
+ { label: "Body", token: "--openpress-color-body" },
519
+ { label: "Muted", token: "--openpress-color-muted" },
520
+ { label: "Subtle", token: "--openpress-color-subtle" },
521
+ { label: "Line", token: "--openpress-color-line" },
522
+ { label: "Block", token: "--openpress-color-block" },
523
+ { label: "Info", token: "--openpress-color-info" },
524
+ { label: "Green", token: "--openpress-color-green" },
525
+ ];
@@ -0,0 +1,35 @@
1
+ import { useId } from "react";
2
+ import { WorkbenchDialog } from "../dialog";
3
+ import type { ProjectPanelPreview } from "./projectPreviewTypes";
4
+ import { createProjectObjectEntityId } from "./projectPreviewTypes";
5
+
6
+ export interface ProjectPreviewDialogProps {
7
+ preview: ProjectPanelPreview;
8
+ onClose: () => void;
9
+ }
10
+
11
+ export function ProjectPreviewDialog({ preview, onClose }: ProjectPreviewDialogProps) {
12
+ const titleId = useId();
13
+
14
+ return (
15
+ <WorkbenchDialog
16
+ titleId={titleId}
17
+ title={preview.title}
18
+ className="openpress-project-preview-dialog"
19
+ closeLabel="關閉預覽"
20
+ onClose={onClose}
21
+ >
22
+ <div
23
+ className="openpress-project-preview-dialog__body"
24
+ data-preview-kind={preview.kind}
25
+ data-openpress-object-id={createProjectObjectEntityId(preview.kind, preview.title)}
26
+ >
27
+ {preview.kind === "media" ? (
28
+ <img src={preview.src} alt="" />
29
+ ) : (
30
+ <div dangerouslySetInnerHTML={{ __html: preview.html }} />
31
+ )}
32
+ </div>
33
+ </WorkbenchDialog>
34
+ );
35
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./ProjectEntryPanel";
2
+ export * from "./projectSourceModel";
@@ -0,0 +1,11 @@
1
+ import type { ComposerMentionItem } from "../mentions";
2
+
3
+ export type ProjectMentionItem = ComposerMentionItem;
4
+
5
+ export type ProjectPanelPreview =
6
+ | { kind: "media"; title: string; src: string }
7
+ | { kind: "component"; title: string; html: string };
8
+
9
+ export function createProjectObjectEntityId(kind: "media" | "component", name: string) {
10
+ return [kind, encodeURIComponent(name)].join(":");
11
+ }