@open-press/core 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 (144) hide show
  1. package/README.md +6 -3
  2. package/engine/cli.mjs +8 -8
  3. package/engine/commands/_shared.mjs +37 -15
  4. package/engine/commands/dev.mjs +2 -2
  5. package/engine/commands/image.mjs +29 -0
  6. package/engine/commands/skills-sync.mjs +71 -0
  7. package/engine/commands/typecheck.mjs +63 -1
  8. package/engine/commands/upgrade.mjs +3 -3
  9. package/engine/document-export.mjs +1 -1
  10. package/engine/output/chrome-pdf.mjs +110 -3
  11. package/engine/output/static-server.mjs +87 -9
  12. package/engine/react/comment-endpoint.mjs +13 -39
  13. package/engine/react/comment-marker.mjs +43 -19
  14. package/engine/react/document-entry.mjs +46 -28
  15. package/engine/react/document-export.mjs +328 -164
  16. package/engine/react/http-json.mjs +24 -0
  17. package/engine/react/mdx-compile.mjs +126 -3
  18. package/engine/react/measurement-css.mjs +114 -1
  19. package/engine/react/object-entities.mjs +204 -0
  20. package/engine/react/pagination/allocator.mjs +48 -3
  21. package/engine/react/pagination.mjs +1 -1
  22. package/engine/react/pipeline/allocate.mjs +41 -72
  23. package/engine/react/pipeline/frame-measurement.mjs +6 -0
  24. package/engine/react/press-tree-inspection.mjs +172 -0
  25. package/engine/react/project-asset-endpoint.mjs +6 -24
  26. package/engine/react/source-edit-endpoint.d.mts +10 -0
  27. package/engine/react/source-edit-endpoint.mjs +75 -0
  28. package/engine/react/sources/mdx-resolver.mjs +13 -15
  29. package/engine/react/style-discovery.mjs +23 -8
  30. package/engine/runtime/config.d.mts +8 -0
  31. package/engine/runtime/config.mjs +57 -60
  32. package/engine/runtime/file-utils.mjs +9 -1
  33. package/engine/runtime/file-walk.mjs +22 -0
  34. package/engine/runtime/inspection.mjs +1 -20
  35. package/engine/runtime/page-geometry.mjs +131 -0
  36. package/engine/runtime/path-utils.mjs +20 -0
  37. package/engine/runtime/source-text-tools.d.mts +102 -0
  38. package/engine/runtime/source-text-tools.mjs +551 -16
  39. package/engine/runtime/source-workspace.mjs +16 -34
  40. package/engine/runtime/validation.mjs +19 -10
  41. package/package.json +3 -5
  42. package/src/openpress/app/OpenPressApp.tsx +296 -0
  43. package/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +20 -9
  44. package/src/openpress/app/WorkspaceGalleryPage.tsx +219 -0
  45. package/src/openpress/app/index.ts +2 -0
  46. package/src/openpress/core/Frame.tsx +26 -15
  47. package/src/openpress/core/FrameContext.tsx +10 -3
  48. package/src/openpress/core/MdxArea.tsx +11 -12
  49. package/src/openpress/core/Press.tsx +25 -4
  50. package/src/openpress/core/Workspace.tsx +36 -0
  51. package/src/openpress/core/cn.ts +4 -0
  52. package/src/openpress/core/index.tsx +11 -3
  53. package/src/openpress/core/primitives.tsx +74 -6
  54. package/src/openpress/core/types.ts +94 -41
  55. package/src/openpress/core/useSource.ts +1 -1
  56. package/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
  57. package/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
  58. package/src/openpress/{types.ts → document-model/documentTypes.ts} +51 -0
  59. package/src/openpress/document-model/index.ts +7 -0
  60. package/src/openpress/document-model/objectEntityModel.ts +55 -0
  61. package/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
  62. package/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
  63. package/src/openpress/document-model/workspaceManifestModel.ts +57 -0
  64. package/src/openpress/manuscript/index.tsx +49 -7
  65. package/src/openpress/mdx/index.ts +15 -7
  66. package/src/openpress/reader/PageThumbnailsPanel.tsx +168 -0
  67. package/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
  68. package/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
  69. package/src/openpress/reader/index.ts +11 -0
  70. package/src/openpress/reader/pageViewportScaleModel.ts +73 -0
  71. package/src/openpress/reader/readerTypes.ts +4 -0
  72. package/src/openpress/reader/usePageViewportScale.ts +119 -0
  73. package/src/openpress/reader/usePanelState.ts +56 -0
  74. package/src/openpress/reader/useReaderHashSync.ts +61 -0
  75. package/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
  76. package/src/openpress/reader/useReaderRuntime.ts +146 -0
  77. package/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
  78. package/src/openpress/shared/Panel.tsx +77 -0
  79. package/src/openpress/shared/index.ts +4 -0
  80. package/src/openpress/shared/numberUtils.ts +3 -0
  81. package/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
  82. package/src/openpress/workbench/Workbench.tsx +506 -0
  83. package/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
  84. package/src/openpress/workbench/actions/ExportImageControl.tsx +96 -0
  85. package/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
  86. package/src/openpress/workbench/actions/SearchControl.tsx +345 -0
  87. package/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
  88. package/src/openpress/workbench/actions/index.ts +6 -0
  89. package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
  90. package/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
  91. package/src/openpress/workbench/dialog/index.ts +1 -0
  92. package/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
  93. package/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
  94. package/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
  95. package/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
  96. package/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
  97. package/src/openpress/workbench/document/index.ts +10 -0
  98. package/src/openpress/workbench/index.ts +2 -0
  99. package/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
  100. package/src/openpress/workbench/inspector/index.ts +5 -0
  101. package/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
  102. package/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
  103. package/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
  104. package/src/openpress/workbench/inspector/useInspectorComments.ts +254 -0
  105. package/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
  106. package/src/openpress/workbench/mentions/index.ts +2 -0
  107. package/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
  108. package/src/openpress/workbench/panels/Panel.tsx +1 -0
  109. package/src/openpress/workbench/panels/PendingCommentsPanel.tsx +80 -0
  110. package/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
  111. package/src/openpress/workbench/panels/index.ts +3 -0
  112. package/src/openpress/workbench/project/ProjectEntryPanel.tsx +525 -0
  113. package/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
  114. package/src/openpress/workbench/project/index.ts +2 -0
  115. package/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
  116. package/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
  117. package/src/openpress/workbench/shell/index.ts +1 -0
  118. package/src/openpress/workbench/workbenchFormatters.ts +120 -0
  119. package/src/openpress/workbench/workbenchTypes.ts +35 -0
  120. package/src/styles/openpress/print-route.css +0 -2
  121. package/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
  122. package/src/styles/openpress/public-viewer.css +25 -320
  123. package/src/styles/openpress/reader-runtime.css +252 -55
  124. package/src/styles/openpress/responsive.css +145 -270
  125. package/src/styles/openpress/workbench-panels.css +327 -178
  126. package/src/styles/openpress/workbench.css +986 -451
  127. package/src/styles/openpress/workspace-gallery.css +300 -0
  128. package/src/styles/openpress.css +2 -1
  129. package/tsconfig.json +1 -1
  130. package/vite.config.ts +50 -0
  131. package/engine/commands/init.mjs +0 -24
  132. package/engine/init.mjs +0 -90
  133. package/src/openpress/App.tsx +0 -127
  134. package/src/openpress/inspector.ts +0 -282
  135. package/src/openpress/projectWorkspace.tsx +0 -919
  136. package/src/openpress/readerRuntime.ts +0 -230
  137. package/src/openpress/workbench.tsx +0 -1265
  138. package/src/openpress/workbenchTypes.ts +0 -4
  139. /package/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
  140. /package/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
  141. /package/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
  142. /package/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
  143. /package/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
  144. /package/src/openpress/{projectSources.ts → workbench/project/projectSourceModel.ts} +0 -0
@@ -0,0 +1,300 @@
1
+ /* Workspace gallery — the reader's landing page for multi-Press
2
+ workspaces. Shows a card per <Press> child; clicking enters that
3
+ document's reader. Single-Press workspaces skip the gallery and
4
+ load the document directly, so this CSS is dormant until users
5
+ add a second <Press> to their <Workspace>.
6
+
7
+ Layout intent: Figma-style file grid — uniform card width, fixed
8
+ thumbnail aspect ratio (4:3), filename + meta below. Each card
9
+ loads its first page asynchronously and renders it scaled-down
10
+ inside the thumbnail slot. */
11
+
12
+ .openpress-workspace-gallery {
13
+ --workspace-bg: #10110f;
14
+ --workspace-bg-soft: #171813;
15
+ --workspace-ink: #f4f1e8;
16
+ --workspace-muted: rgba(244, 241, 232, 0.52);
17
+ --workspace-line: rgba(244, 241, 232, 0.12);
18
+ --workspace-card: #f7f5ee;
19
+ --workspace-card-ink: #141411;
20
+ --workspace-card-muted: #65635d;
21
+ --workspace-card-line: rgba(20, 20, 17, 0.1);
22
+ --workspace-card-stage: #e8e5dc;
23
+ display: grid;
24
+ gap: 2rem;
25
+ min-height: 100vh;
26
+ max-width: none;
27
+ margin: 0;
28
+ padding: 3.6rem clamp(2rem, 4vw, 4.5rem) 6rem;
29
+ font-family: var(--openpress-font-body, system-ui, sans-serif);
30
+ color: var(--workspace-ink);
31
+ background:
32
+ linear-gradient(180deg, var(--workspace-bg-soft), var(--workspace-bg) 42rem),
33
+ var(--workspace-bg);
34
+ }
35
+
36
+ .openpress-workspace-gallery__header {
37
+ display: grid;
38
+ grid-template-columns: minmax(0, 1fr) auto;
39
+ align-items: end;
40
+ justify-content: space-between;
41
+ gap: 2.5rem;
42
+ padding: 0 0 1.45rem;
43
+ border-bottom: 1px solid var(--workspace-line);
44
+ }
45
+
46
+ .openpress-workspace-gallery__headline {
47
+ display: grid;
48
+ gap: 0.75rem;
49
+ }
50
+
51
+ .openpress-workspace-gallery__eyebrow {
52
+ margin: 0;
53
+ color: var(--workspace-muted);
54
+ font-family: var(--openpress-font-mono, ui-monospace, monospace);
55
+ font-size: 0.68rem;
56
+ font-weight: 600;
57
+ letter-spacing: 0.16em;
58
+ text-transform: uppercase;
59
+ }
60
+
61
+ .openpress-workspace-gallery__header h1 {
62
+ margin: 0;
63
+ font-family: var(--openpress-font-display, var(--openpress-font-body, system-ui));
64
+ font-size: clamp(2.6rem, 5.4vw, 5.2rem);
65
+ font-weight: 720;
66
+ line-height: 0.94;
67
+ letter-spacing: -0.035em;
68
+ color: var(--workspace-ink);
69
+ }
70
+
71
+ .openpress-workspace-gallery__count {
72
+ margin: 0;
73
+ display: grid;
74
+ justify-items: end;
75
+ gap: 0.25rem;
76
+ min-width: 4.5rem;
77
+ color: var(--workspace-ink);
78
+ font-family: var(--openpress-font-mono, ui-monospace, monospace);
79
+ line-height: 1;
80
+ }
81
+
82
+ .openpress-workspace-gallery__count span {
83
+ color: var(--workspace-ink);
84
+ font-size: 2rem;
85
+ font-weight: 500;
86
+ letter-spacing: -0.04em;
87
+ }
88
+
89
+ .openpress-workspace-gallery__count small {
90
+ color: var(--workspace-muted);
91
+ font-size: 0.62rem;
92
+ font-weight: 600;
93
+ letter-spacing: 0.14em;
94
+ text-transform: uppercase;
95
+ }
96
+
97
+ .openpress-workspace-gallery__grid {
98
+ list-style: none;
99
+ margin: 0;
100
+ padding: 0;
101
+ display: grid;
102
+ align-items: start;
103
+ /* Uniform card size — Figma-style. Outer thumb is fixed 4:3, the
104
+ inner page letterboxes to its own geometry. */
105
+ grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
106
+ gap: 1.5rem;
107
+ }
108
+
109
+ .openpress-workspace-gallery__item {
110
+ display: flex;
111
+ }
112
+
113
+ .openpress-workspace-gallery__card {
114
+ appearance: none;
115
+ display: grid;
116
+ grid-template-rows: auto minmax(6.75rem, auto);
117
+ align-self: start;
118
+ width: 100%;
119
+ padding: 0;
120
+ border: 1px solid rgba(255, 255, 255, 0.08);
121
+ border-radius: 8px;
122
+ background: var(--workspace-card);
123
+ color: var(--workspace-card-ink);
124
+ text-align: left;
125
+ cursor: pointer;
126
+ overflow: hidden;
127
+ transition:
128
+ transform 160ms ease,
129
+ box-shadow 160ms ease,
130
+ border-color 160ms ease;
131
+ }
132
+
133
+ .openpress-workspace-gallery__card:hover,
134
+ .openpress-workspace-gallery__card:focus-visible {
135
+ border-color: rgba(255, 255, 255, 0.28);
136
+ transform: translateY(-2px);
137
+ box-shadow: 0 18px 44px rgba(0, 0, 0, 0.34);
138
+ outline: none;
139
+ }
140
+
141
+ .openpress-workspace-gallery__thumb {
142
+ position: relative;
143
+ display: block;
144
+ width: 100%;
145
+ /* Uniform 4:3 outer slot across every card. The page itself
146
+ letterboxes inside via centered scale, so each Press shows at its
147
+ own true aspect against the gradient background. */
148
+ aspect-ratio: 4 / 3;
149
+ background:
150
+ linear-gradient(
151
+ 135deg,
152
+ color-mix(in srgb, var(--workspace-card-ink) 5%, var(--workspace-card-stage)),
153
+ var(--workspace-card-stage)
154
+ );
155
+ border-bottom: 1px solid var(--workspace-card-line);
156
+ overflow: hidden;
157
+ }
158
+
159
+ .openpress-workspace-gallery__thumb::before {
160
+ content: "";
161
+ position: absolute;
162
+ inset: 0;
163
+ pointer-events: none;
164
+ background-image:
165
+ linear-gradient(rgba(20, 20, 17, 0.05) 1px, transparent 1px),
166
+ linear-gradient(90deg, rgba(20, 20, 17, 0.05) 1px, transparent 1px);
167
+ background-size: 24px 24px;
168
+ opacity: 0.5;
169
+ }
170
+
171
+ .openpress-workspace-gallery__thumb-stage {
172
+ position: absolute;
173
+ inset: clamp(0.85rem, 6%, 1.45rem);
174
+ display: grid;
175
+ place-items: center;
176
+ }
177
+
178
+ .openpress-workspace-gallery__thumb-frame {
179
+ position: relative;
180
+ box-shadow:
181
+ 0 18px 36px rgba(20, 20, 17, 0.18),
182
+ 0 0 0 1px rgba(20, 20, 17, 0.08);
183
+ }
184
+
185
+ .openpress-workspace-gallery__thumb-stage .openpress-public-page {
186
+ display: block;
187
+ pointer-events: none;
188
+ user-select: none;
189
+ }
190
+
191
+ .openpress-workspace-gallery__thumb-placeholder {
192
+ position: absolute;
193
+ inset: clamp(0.85rem, 6%, 1.45rem);
194
+ display: grid;
195
+ place-items: center;
196
+ }
197
+
198
+ .openpress-workspace-gallery__thumb-skel {
199
+ display: block;
200
+ width: 70%;
201
+ height: 70%;
202
+ background:
203
+ repeating-linear-gradient(
204
+ 135deg,
205
+ rgba(20, 20, 17, 0.04) 0 6px,
206
+ transparent 6px 14px
207
+ ),
208
+ #fff;
209
+ border: 1px solid var(--workspace-card-line);
210
+ border-radius: 3px;
211
+ box-shadow: 0 14px 28px rgba(20, 20, 17, 0.14);
212
+ }
213
+
214
+ .openpress-workspace-gallery__thumb-placeholder[data-state="loading"] .openpress-workspace-gallery__thumb-skel {
215
+ animation: openpress-gallery-skel-pulse 1.4s ease-in-out infinite;
216
+ }
217
+
218
+ @keyframes openpress-gallery-skel-pulse {
219
+ 0%, 100% { opacity: 1; }
220
+ 50% { opacity: 0.55; }
221
+ }
222
+
223
+ .openpress-workspace-gallery__body {
224
+ display: grid;
225
+ align-content: space-between;
226
+ gap: 1.2rem;
227
+ min-height: 6.75rem;
228
+ padding: 1.1rem 1.22rem 1.15rem;
229
+ background: var(--workspace-card);
230
+ }
231
+
232
+ .openpress-workspace-gallery__title {
233
+ display: block;
234
+ color: var(--workspace-card-ink);
235
+ font-size: 1rem;
236
+ font-weight: 700;
237
+ line-height: 1.2;
238
+ white-space: nowrap;
239
+ overflow: hidden;
240
+ text-overflow: ellipsis;
241
+ }
242
+
243
+ .openpress-workspace-gallery__meta {
244
+ display: flex;
245
+ align-items: center;
246
+ justify-content: space-between;
247
+ gap: 0.7rem;
248
+ flex-wrap: wrap;
249
+ color: var(--workspace-card-muted);
250
+ font-family: var(--openpress-font-mono, ui-monospace, monospace);
251
+ font-size: 0.66rem;
252
+ letter-spacing: 0.03em;
253
+ }
254
+
255
+ .openpress-workspace-gallery__slug {
256
+ max-width: 13rem;
257
+ color: color-mix(in srgb, var(--workspace-card-ink) 72%, transparent);
258
+ font-weight: 500;
259
+ overflow: hidden;
260
+ text-overflow: ellipsis;
261
+ text-transform: uppercase;
262
+ white-space: nowrap;
263
+ }
264
+
265
+ .openpress-workspace-gallery__dot {
266
+ color: color-mix(in srgb, var(--workspace-card-muted) 55%, transparent);
267
+ }
268
+
269
+ .openpress-workspace-gallery__pages,
270
+ .openpress-workspace-gallery__geom {
271
+ color: var(--workspace-card-muted);
272
+ }
273
+
274
+ .openpress-workspace-gallery__geom {
275
+ display: inline-flex;
276
+ align-items: center;
277
+ min-height: 1.35rem;
278
+ padding: 0 0.48rem;
279
+ border: 1px solid var(--workspace-card-line);
280
+ border-radius: 4px;
281
+ background: rgba(255, 255, 255, 0.36);
282
+ color: color-mix(in srgb, var(--workspace-card-ink) 76%, transparent);
283
+ font-size: 0.62rem;
284
+ white-space: nowrap;
285
+ }
286
+
287
+ @media (max-width: 720px) {
288
+ .openpress-workspace-gallery {
289
+ padding: 2.25rem 1rem 4rem;
290
+ }
291
+
292
+ .openpress-workspace-gallery__header {
293
+ display: grid;
294
+ align-items: start;
295
+ }
296
+
297
+ .openpress-workspace-gallery__grid {
298
+ grid-template-columns: 1fr;
299
+ }
300
+ }
@@ -4,9 +4,10 @@
4
4
  @import url("/openpress/components.css");
5
5
 
6
6
  @import "./openpress/app-shell.css";
7
+ @import "./openpress/workspace-gallery.css";
7
8
  @import "./openpress/workbench.css";
8
9
  @import "./openpress/workbench-panels.css";
9
- @import "./openpress/project-workspace.css";
10
+ @import "./openpress/project-preview-panel.css";
10
11
  @import "./openpress/media-workspace.css";
11
12
  @import "./openpress/reader-runtime.css";
12
13
  @import "./openpress/public-viewer.css";
package/tsconfig.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "forceConsistentCasingInFileNames": true,
12
12
  "module": "ESNext",
13
13
  "moduleResolution": "Bundler",
14
- "types": ["node", "vite/client"],
14
+ "types": ["node"],
15
15
  "resolveJsonModule": true,
16
16
  "isolatedModules": true,
17
17
  "noEmit": true,
package/vite.config.ts CHANGED
@@ -6,8 +6,10 @@ import type { IncomingMessage, ServerResponse } from "node:http";
6
6
  import { defineConfig } from "vite";
7
7
  import react from "@vitejs/plugin-react";
8
8
  import { loadConfig, publicPdfHref } from "./engine/runtime/config.mjs";
9
+ import { searchSourceText } from "./engine/runtime/source-text-tools.mjs";
9
10
  import { handleCommentRequest } from "./engine/react/comment-endpoint.mjs";
10
11
  import { handleProjectAssetRequest } from "./engine/react/project-asset-endpoint.mjs";
12
+ import { handleSourceEditRequest } from "./engine/react/source-edit-endpoint.mjs";
11
13
 
12
14
  const frameworkRoot = fileURLToPath(new URL("./", import.meta.url));
13
15
  const workspaceRoot = process.env.OPENPRESS_WORKSPACE_ROOT
@@ -53,6 +55,7 @@ const workspaceDefines = {
53
55
 
54
56
  export default defineConfig({
55
57
  base: "./",
58
+ cacheDir: path.join(workspaceRoot, ".openpress", "vite-client"),
56
59
  plugins: [openpressLocalDeployPlugin(), react()],
57
60
  define: workspaceDefines,
58
61
  resolve: {
@@ -68,6 +71,17 @@ export default defineConfig({
68
71
  ...workspaceAliases,
69
72
  },
70
73
  },
74
+ optimizeDeps: {
75
+ include: [
76
+ "@mdx-js/react",
77
+ "lucide-react",
78
+ "react",
79
+ "react-dom",
80
+ "react-dom/client",
81
+ "react/jsx-dev-runtime",
82
+ "react/jsx-runtime",
83
+ ],
84
+ },
71
85
  build: {
72
86
  outDir: outputDir,
73
87
  emptyOutDir: true,
@@ -108,6 +122,12 @@ function openpressLocalDeployPlugin() {
108
122
  server.middlewares.use("/__openpress/status", (req, res) => {
109
123
  void handleLocalStatusRequest(req, res);
110
124
  });
125
+ server.middlewares.use("/__openpress/search", (req, res) => {
126
+ void handleLocalSearchRequest(req, res);
127
+ });
128
+ server.middlewares.use("/__openpress/source-edit", (req, res) => {
129
+ void handleSourceEditRequest(req, res, { root: workspaceRoot });
130
+ });
111
131
  server.middlewares.use("/__openpress/deploy", (req, res) => {
112
132
  void handleLocalDeployRequest(req, res);
113
133
  });
@@ -267,6 +287,32 @@ async function handleLocalStatusRequest(req: IncomingMessage, res: ServerRespons
267
287
  });
268
288
  }
269
289
 
290
+ async function handleLocalSearchRequest(req: IncomingMessage, res: ServerResponse) {
291
+ if (req.method !== "GET") {
292
+ writeJson(res, 405, { ok: false, message: "Search endpoint requires GET." });
293
+ return;
294
+ }
295
+
296
+ const requestUrl = new URL(req.url ?? "/", "http://localhost");
297
+ const query = (requestUrl.searchParams.get("q") ?? "").trim();
298
+ if (!query) {
299
+ writeJson(res, 400, { ok: false, message: "Search query is required." });
300
+ return;
301
+ }
302
+
303
+ try {
304
+ const report = await searchSourceText({
305
+ config: openpressConfig,
306
+ query,
307
+ scope: searchScopeFrom(requestUrl.searchParams),
308
+ caseSensitive: requestUrl.searchParams.get("caseSensitive") === "true",
309
+ });
310
+ writeJson(res, 200, { ok: true, ...report });
311
+ } catch (error) {
312
+ writeJson(res, 500, { ok: false, message: error instanceof Error ? error.message : String(error) });
313
+ }
314
+ }
315
+
270
316
  async function handleLocalDeployRequest(req: IncomingMessage, res: ServerResponse) {
271
317
  if (req.method !== "POST") {
272
318
  writeJson(res, 405, { ok: false, message: "Deploy endpoint requires POST." });
@@ -368,6 +414,10 @@ function localDeploySetupMessage() {
368
414
  return `Deployment adapter \`${openpressConfig.deploy.adapter}\` is not configured.`;
369
415
  }
370
416
 
417
+ function searchScopeFrom(searchParams: URLSearchParams) {
418
+ return searchParams.get("scope") === "all" ? "all" : "content";
419
+ }
420
+
371
421
  async function fileExists(filePath: string) {
372
422
  try {
373
423
  await fs.access(filePath);
@@ -1,24 +0,0 @@
1
- import { initWorkspace, listStylePackSkills } from "../init.mjs";
2
- import { formatDisplayPath, parseInitOptions } from "./_shared.mjs";
3
-
4
- export const needsWorkspace = false;
5
-
6
- export async function run({ argv }) {
7
- const options = parseInitOptions(argv);
8
- if (!options.target) {
9
- console.error("openpress init: target path is required");
10
- console.error("Usage: openpress init <target> [--skill <name>] [--force]");
11
- const available = await listStylePackSkills();
12
- if (available.length) console.error(`Style packs available: ${available.join(", ")}`);
13
- return 1;
14
- }
15
- const result = await initWorkspace(options);
16
- const displayPath = formatDisplayPath(result.targetPath);
17
- console.log(`OpenPress init: created ${displayPath} from style pack "${result.skill}".`);
18
- console.log("Next steps:");
19
- console.log(` cd ${displayPath}`);
20
- console.log(" # 填入 openpress.config.mjs 的 title / subtitle / organization");
21
- console.log(" # 改 document/index.tsx 與 document/chapters/**/*.mdx 為實際內容");
22
- console.log(" node engine/cli.mjs validate");
23
- return 0;
24
- }
package/engine/init.mjs DELETED
@@ -1,90 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
-
5
- const SELF_DIR = path.dirname(fileURLToPath(import.meta.url));
6
- const ENGINE_ROOT = path.resolve(SELF_DIR, "..");
7
- const SKILLS_DIR = path.join(ENGINE_ROOT, "skills");
8
-
9
- const DEFAULT_SKILL = "editorial-monograph";
10
-
11
- export async function initWorkspace({ target, skill = DEFAULT_SKILL, force = false }) {
12
- if (!target) throw new Error("openpress init: target path is required");
13
- const targetPath = path.resolve(target);
14
-
15
- const starterPath = path.join(SKILLS_DIR, skill, "starter");
16
- try {
17
- const stat = await fs.stat(starterPath);
18
- if (!stat.isDirectory()) {
19
- throw new Error(`openpress init: skill "${skill}" has no starter/ directory at ${starterPath}`);
20
- }
21
- } catch (error) {
22
- if (error?.code === "ENOENT") {
23
- const available = await listStylePackSkills();
24
- throw new Error(
25
- `openpress init: skill "${skill}" not found or has no starter. ` +
26
- `Available style packs: ${available.join(", ") || "(none)"}`,
27
- );
28
- }
29
- throw error;
30
- }
31
-
32
- if (!force) {
33
- try {
34
- const stat = await fs.stat(targetPath);
35
- if (stat.isDirectory()) {
36
- const entries = await fs.readdir(targetPath);
37
- if (entries.length > 0) {
38
- throw new Error(`openpress init: target ${targetPath} exists and is not empty. Pass --force to overwrite.`);
39
- }
40
- } else {
41
- throw new Error(`openpress init: target ${targetPath} exists and is not a directory.`);
42
- }
43
- } catch (error) {
44
- if (error?.code !== "ENOENT") throw error;
45
- }
46
- }
47
-
48
- await fs.mkdir(targetPath, { recursive: true });
49
- await copyDirectory(starterPath, targetPath);
50
-
51
- return { targetPath, skill };
52
- }
53
-
54
- export async function listStylePackSkills() {
55
- try {
56
- const entries = await fs.readdir(SKILLS_DIR, { withFileTypes: true });
57
- const names = [];
58
- for (const entry of entries) {
59
- if (!entry.isDirectory()) continue;
60
- const starter = path.join(SKILLS_DIR, entry.name, "starter");
61
- try {
62
- const stat = await fs.stat(starter);
63
- if (stat.isDirectory()) names.push(entry.name);
64
- } catch {
65
- // skill without starter/ is not a style pack — skip
66
- }
67
- }
68
- return names.sort();
69
- } catch (error) {
70
- if (error?.code === "ENOENT") return [];
71
- throw error;
72
- }
73
- }
74
-
75
- async function copyDirectory(source, destination) {
76
- await fs.mkdir(destination, { recursive: true });
77
- for (const entry of await fs.readdir(source, { withFileTypes: true })) {
78
- if (entry.name === ".DS_Store") continue;
79
- const sourcePath = path.join(source, entry.name);
80
- const destPath = path.join(destination, entry.name);
81
- if (entry.isDirectory()) {
82
- await copyDirectory(sourcePath, destPath);
83
- } else if (entry.isFile()) {
84
- await fs.copyFile(sourcePath, destPath);
85
- } else if (entry.isSymbolicLink()) {
86
- const link = await fs.readlink(sourcePath);
87
- await fs.symlink(link, destPath);
88
- }
89
- }
90
- }
@@ -1,127 +0,0 @@
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
- }