@puredesktop/create-app 2.1.9 → 2.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-puredesktop-app.mjs +70 -338
- package/package.json +3 -2
- package/template/README.md +3 -3
- package/template/docs/README.md +9 -8
- package/template/docs/howtos/persisting-app-state.md +242 -242
- package/template/docs/howtos/using-document-editor.md +440 -0
- package/template/docs/plugin-manifest.md +1 -1
- package/template/docs/ui-and-components.md +84 -79
- package/template/gitignore.tpl +6 -1
- package/template/package.json +1 -1
- package/template/plugin.json +1 -1
- package/template/scripts/validate-puredesktop-app.mjs +46 -46
- package/template/src/App.tsx +8 -8
- package/template/src/components/AppShell.tsx +11 -11
- package/template/src/components/StarterWorkspace.tsx +154 -154
- package/template/src/constants.ts +2 -2
- package/template/src/lib/starterWorkspace.ts +29 -29
- package/template/vite.config.js +13 -13
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
# Using The Document Editor
|
|
2
|
+
|
|
3
|
+
Use the platform document editor when the app needs a rich writing surface:
|
|
4
|
+
notes, long-form documents, reports, books, review workflows, markdown-backed
|
|
5
|
+
content, or any product surface where formatting matters.
|
|
6
|
+
|
|
7
|
+
The editor is controlled by the app. The platform provides the TipTap editor,
|
|
8
|
+
toolbar, comments, slash commands, document features, and conversion helpers.
|
|
9
|
+
The bridge provides the shell persistence mechanisms. Your app owns the typed
|
|
10
|
+
editor surface or session code that decides what to load, when to save, which
|
|
11
|
+
bridge helper to call, and how persisted data maps back into product state.
|
|
12
|
+
|
|
13
|
+
The editor does not persist content by itself.
|
|
14
|
+
|
|
15
|
+
## Imports
|
|
16
|
+
|
|
17
|
+
Generated apps import the editor from the UI bridge package:
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import {
|
|
21
|
+
DocumentEditor,
|
|
22
|
+
type DocumentEditorHandle,
|
|
23
|
+
type EditorViewMode,
|
|
24
|
+
EditorViewModeToggle,
|
|
25
|
+
DocumentToolbarIcon,
|
|
26
|
+
ToolbarActionButton,
|
|
27
|
+
ToolbarActionDivider,
|
|
28
|
+
ToolbarActionStrip,
|
|
29
|
+
ToolbarInlineToolButton,
|
|
30
|
+
useEditorExtensions,
|
|
31
|
+
type SlashCommandItem,
|
|
32
|
+
} from '@puredesktop/puredesktop-ui-bridge/editor'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Minimal Editor
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { useMemo, useRef, useState } from 'react'
|
|
39
|
+
import {
|
|
40
|
+
DocumentEditor,
|
|
41
|
+
type DocumentEditorHandle,
|
|
42
|
+
type SlashCommandItem,
|
|
43
|
+
useEditorExtensions,
|
|
44
|
+
} from '@puredesktop/puredesktop-ui-bridge/editor'
|
|
45
|
+
|
|
46
|
+
export function NotesEditor(): React.ReactElement {
|
|
47
|
+
const editorRef = useRef<DocumentEditorHandle>(null)
|
|
48
|
+
const [content, setContent] = useState('<h1>Untitled</h1><p></p>')
|
|
49
|
+
const extensions = useEditorExtensions({
|
|
50
|
+
placeholder: 'Start writing...',
|
|
51
|
+
features: {
|
|
52
|
+
comments: true,
|
|
53
|
+
autoReviewPrompts: true,
|
|
54
|
+
tables: true,
|
|
55
|
+
taskList: true,
|
|
56
|
+
math: true,
|
|
57
|
+
mermaid: true,
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const slashCommands = useMemo<SlashCommandItem[]>(
|
|
62
|
+
() => [
|
|
63
|
+
{
|
|
64
|
+
id: 'insert-summary',
|
|
65
|
+
title: 'Summary block',
|
|
66
|
+
description: 'Add a heading and starter paragraph.',
|
|
67
|
+
section: 'App',
|
|
68
|
+
run: ({ editor, range }) => {
|
|
69
|
+
editor
|
|
70
|
+
.chain()
|
|
71
|
+
.focus()
|
|
72
|
+
.deleteRange(range)
|
|
73
|
+
.insertContent('<h2>Summary</h2><p></p>')
|
|
74
|
+
.run()
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
[],
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<DocumentEditor
|
|
83
|
+
ref={editorRef}
|
|
84
|
+
value={content}
|
|
85
|
+
extensions={extensions}
|
|
86
|
+
inputFormat="html"
|
|
87
|
+
outputFormat="html"
|
|
88
|
+
enableComments
|
|
89
|
+
slashCommands={slashCommands}
|
|
90
|
+
onChange={setContent}
|
|
91
|
+
/>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`onChange` receives the serialized document in the selected output format. Save
|
|
97
|
+
that value through the app's existing document/page/session save path. Simple
|
|
98
|
+
apps can debounce and call their session update command from the editor surface.
|
|
99
|
+
Larger document apps can delegate to their document/session modules. Do not make
|
|
100
|
+
the editor own product persistence.
|
|
101
|
+
|
|
102
|
+
## Capability Map
|
|
103
|
+
|
|
104
|
+
The editor package includes these capability groups:
|
|
105
|
+
|
|
106
|
+
| Capability | Exports | Use when |
|
|
107
|
+
| --- | --- | --- |
|
|
108
|
+
| Document surface | `DocumentEditor`, `DocumentEditorHandle`, `DocumentEditorProps` | Rendering and controlling a rich document editor. |
|
|
109
|
+
| Extension setup | `useEditorExtensions`, `buildExtensions`, `EditorFeatures` | Choosing document features for the product surface. |
|
|
110
|
+
| Content conversion | `resolveEditorContent`, `toHtml`, `toMd`, `EditorInputFormat` | Loading markdown or HTML and saving in the app's chosen format. |
|
|
111
|
+
| Toolbar chrome | `DocumentToolbarIcon`, `ToolbarActionStrip`, `ToolbarActionButton`, `ToolbarActionIconButton`, `ToolbarInlineToolButton`, `ToolbarActionDivider` | Adding app controls into the shared editor toolbar. |
|
|
112
|
+
| View mode | `EditorViewModeToggle`, `EditorViewMode` | Switching between full editor chrome and a quiet page view. |
|
|
113
|
+
| Slash commands | `SlashCommands`, `createDefaultSlashCommands`, `SlashCommandItem` | Adding app-specific insert/actions to the slash menu. |
|
|
114
|
+
| Comments and review marks | `CommentMark`, comment types/status helpers, `parseCommentReplies`, `serializeCommentReplies` | Anchored review comments and editorial annotations. |
|
|
115
|
+
| Auto review | `runAutoReview`, `buildAutoReviewPrompt`, `buildAutoReviewPromptBundle`, `collectAutoReviewParagraphs`, `collectScopedAutoReviewParagraphs`, `validateAutoReviewFindings` | App-provided review workflows that annotate document text. |
|
|
116
|
+
| Rich document nodes | `Figure`, `Footnote`, `IndexMarker`, `MermaidBlock`, `SmartTypography`, math insertion helpers | Product surfaces that need figures, notes, diagrams, smart typography, or editable math. |
|
|
117
|
+
| Assets | `CollectionImage`, `createCollectionImagePasteExtension`, `insertCollectionImage`, `insertCollectionImageFromBinaryResult` | Persisted pasted or selected images that need app-managed storage. |
|
|
118
|
+
| Diff view | `DocumentDiffView`, `buildAlignedLineDiff` | Showing text/document differences in app UI. |
|
|
119
|
+
|
|
120
|
+
## Feature Flags
|
|
121
|
+
|
|
122
|
+
`useEditorExtensions` builds the shared extension set. Enable only the features
|
|
123
|
+
the product needs:
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
const extensions = useEditorExtensions({
|
|
127
|
+
placeholder: 'Write the report...',
|
|
128
|
+
features: {
|
|
129
|
+
tables: true,
|
|
130
|
+
taskList: true,
|
|
131
|
+
highlight: true,
|
|
132
|
+
math: true,
|
|
133
|
+
figures: true,
|
|
134
|
+
mermaid: true,
|
|
135
|
+
footnotes: true,
|
|
136
|
+
smartTypography: true,
|
|
137
|
+
comments: true,
|
|
138
|
+
autoReviewPrompts: true,
|
|
139
|
+
indexMarkers: false,
|
|
140
|
+
documentAssetIdentity: true,
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Comments require both `features.comments` and `enableComments` on
|
|
146
|
+
`DocumentEditor`.
|
|
147
|
+
|
|
148
|
+
## Editor Handle
|
|
149
|
+
|
|
150
|
+
Use a ref for explicit editor actions:
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
const editorRef = useRef<DocumentEditorHandle>(null)
|
|
154
|
+
|
|
155
|
+
editorRef.current?.insertHtml('<p>Inserted from the app.</p>')
|
|
156
|
+
editorRef.current?.insertImage({
|
|
157
|
+
src: imageUrl,
|
|
158
|
+
alt: 'Sketch',
|
|
159
|
+
caption: 'Draft sketch',
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const tiptapEditor = editorRef.current?.getEditor()
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The handle also supports opening, updating, applying, removing, and clearing
|
|
166
|
+
comment marks, plus `runAutoReview` when the app provides a review provider.
|
|
167
|
+
|
|
168
|
+
## View Modes
|
|
169
|
+
|
|
170
|
+
Use `boxed` for a full editing surface with toolbar chrome. Use `page` for a
|
|
171
|
+
quiet reading or print-like surface.
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
const [viewMode, setViewMode] = useState<EditorViewMode>('boxed')
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<>
|
|
178
|
+
<EditorViewModeToggle value={viewMode} onChange={setViewMode} />
|
|
179
|
+
<DocumentEditor
|
|
180
|
+
value={content}
|
|
181
|
+
extensions={extensions}
|
|
182
|
+
viewMode={viewMode}
|
|
183
|
+
showToolbar={viewMode !== 'page'}
|
|
184
|
+
showSelectionComments={viewMode !== 'page'}
|
|
185
|
+
onChange={setContent}
|
|
186
|
+
/>
|
|
187
|
+
</>
|
|
188
|
+
)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Toolbar Slots
|
|
192
|
+
|
|
193
|
+
Keep product-specific controls outside the editor implementation by using the
|
|
194
|
+
toolbar slots:
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
<DocumentEditor
|
|
198
|
+
value={content}
|
|
199
|
+
extensions={extensions}
|
|
200
|
+
toolbarInlineTools={
|
|
201
|
+
<ToolbarActionStrip>
|
|
202
|
+
<ToolbarInlineToolButton
|
|
203
|
+
aria-label="Insert section"
|
|
204
|
+
title="Insert section"
|
|
205
|
+
onClick={() => editorRef.current?.insertHtml('<h2>New section</h2>')}
|
|
206
|
+
>
|
|
207
|
+
<DocumentToolbarIcon name="insert-menu" />
|
|
208
|
+
</ToolbarInlineToolButton>
|
|
209
|
+
</ToolbarActionStrip>
|
|
210
|
+
}
|
|
211
|
+
toolbarActions={
|
|
212
|
+
<ToolbarActionStrip>
|
|
213
|
+
<ToolbarActionDivider />
|
|
214
|
+
<ToolbarActionButton onClick={saveDocument}>
|
|
215
|
+
Save
|
|
216
|
+
</ToolbarActionButton>
|
|
217
|
+
</ToolbarActionStrip>
|
|
218
|
+
}
|
|
219
|
+
onChange={setContent}
|
|
220
|
+
/>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Use platform toolbar components for editor chrome, and keep app-specific logic
|
|
224
|
+
in app hooks or `src/lib`.
|
|
225
|
+
|
|
226
|
+
## Assets
|
|
227
|
+
|
|
228
|
+
There are two image paths.
|
|
229
|
+
|
|
230
|
+
Use `editorRef.current?.insertImage(...)` only when the app already has a stable
|
|
231
|
+
image URL:
|
|
232
|
+
|
|
233
|
+
```tsx
|
|
234
|
+
editorRef.current?.insertImage({
|
|
235
|
+
src: imageUrl,
|
|
236
|
+
alt: 'Screenshot',
|
|
237
|
+
caption: 'Imported screenshot',
|
|
238
|
+
})
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Use collection image handling when the user can paste, drop, or choose image
|
|
242
|
+
files and the app must keep those files with the document. In that flow:
|
|
243
|
+
|
|
244
|
+
1. The editor detects pasted/dropped image bytes.
|
|
245
|
+
2. The app writes those bytes through bridge filesystem helpers.
|
|
246
|
+
3. The app inserts an image node into the editor.
|
|
247
|
+
4. The document stores a relative asset path so it can be reopened later.
|
|
248
|
+
|
|
249
|
+
Wire it like this:
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
import { useMemo, useRef } from 'react'
|
|
253
|
+
import {
|
|
254
|
+
CollectionImage,
|
|
255
|
+
createCollectionImagePasteExtension,
|
|
256
|
+
DocumentEditor,
|
|
257
|
+
type DocumentEditorHandle,
|
|
258
|
+
useEditorExtensions,
|
|
259
|
+
} from '@puredesktop/puredesktop-ui-bridge/editor'
|
|
260
|
+
import { createCollectionImagePasteHandler } from '@puredesktop/puredesktop-ui-bridge/bridge/collectionImagePaste'
|
|
261
|
+
import { DEFAULT_COLLECTION_FIGURES_DIR } from '@puredesktop/puredesktop-ui-bridge/bridge/collectionAssets'
|
|
262
|
+
import {
|
|
263
|
+
readPlatformFileBinary,
|
|
264
|
+
writePlatformFileBinary,
|
|
265
|
+
} from '../bridge/platformBridge'
|
|
266
|
+
|
|
267
|
+
function base64ToBytes(base64: string): Uint8Array {
|
|
268
|
+
const binary = atob(base64)
|
|
269
|
+
return Uint8Array.from(binary, char => char.charCodeAt(0))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function bytesToBase64(bytes: Uint8Array): string {
|
|
273
|
+
let binary = ''
|
|
274
|
+
const chunk = 0x8000
|
|
275
|
+
for (let index = 0; index < bytes.length; index += chunk) {
|
|
276
|
+
binary += String.fromCharCode(...bytes.subarray(index, index + chunk))
|
|
277
|
+
}
|
|
278
|
+
return btoa(binary)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function readImageBytes(absolutePath: string): Promise<Uint8Array> {
|
|
282
|
+
const result = await readPlatformFileBinary(absolutePath)
|
|
283
|
+
return base64ToBytes(result.base64)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function writeImageBytes(
|
|
287
|
+
absolutePath: string,
|
|
288
|
+
bytes: Uint8Array,
|
|
289
|
+
): Promise<void> {
|
|
290
|
+
await writePlatformFileBinary(absolutePath, bytesToBase64(bytes))
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function DocumentWithImages({
|
|
294
|
+
documentFolder,
|
|
295
|
+
content,
|
|
296
|
+
onChange,
|
|
297
|
+
}: {
|
|
298
|
+
documentFolder: string
|
|
299
|
+
content: string
|
|
300
|
+
onChange: (content: string) => void
|
|
301
|
+
}): React.ReactElement {
|
|
302
|
+
const editorRef = useRef<DocumentEditorHandle>(null)
|
|
303
|
+
const baseExtensions = useEditorExtensions({
|
|
304
|
+
placeholder: 'Start writing...',
|
|
305
|
+
features: {
|
|
306
|
+
figures: true,
|
|
307
|
+
},
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
const handlePastedImage = useMemo(
|
|
311
|
+
() =>
|
|
312
|
+
createCollectionImagePasteHandler({
|
|
313
|
+
collectionPath: documentFolder,
|
|
314
|
+
subdir: DEFAULT_COLLECTION_FIGURES_DIR,
|
|
315
|
+
getEditor: () => editorRef.current?.getEditor(),
|
|
316
|
+
writeBinary: writeImageBytes,
|
|
317
|
+
}),
|
|
318
|
+
[documentFolder],
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
const extensions = useMemo(() => {
|
|
322
|
+
const withoutStockImage = baseExtensions.filter(
|
|
323
|
+
extension => extension.name !== 'image',
|
|
324
|
+
)
|
|
325
|
+
return [
|
|
326
|
+
...withoutStockImage,
|
|
327
|
+
CollectionImage,
|
|
328
|
+
createCollectionImagePasteExtension({
|
|
329
|
+
handleImage: handlePastedImage,
|
|
330
|
+
readBinary: readImageBytes,
|
|
331
|
+
}),
|
|
332
|
+
]
|
|
333
|
+
}, [baseExtensions, handlePastedImage])
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<DocumentEditor
|
|
337
|
+
ref={editorRef}
|
|
338
|
+
value={content}
|
|
339
|
+
extensions={extensions}
|
|
340
|
+
onChange={onChange}
|
|
341
|
+
/>
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
`CollectionImage` replaces the stock image node because collection images need
|
|
347
|
+
two things at once: a data URL for immediate display in the editor and a
|
|
348
|
+
relative asset path for saved document HTML.
|
|
349
|
+
|
|
350
|
+
`createCollectionImagePasteExtension` is the TipTap paste/drop hook. It does not
|
|
351
|
+
save anything by itself. It reads image bytes and passes them to `handleImage`.
|
|
352
|
+
|
|
353
|
+
`createCollectionImagePasteHandler` is the bridge-aware handler. It builds a
|
|
354
|
+
relative path under the document folder, writes the bytes with `writeBinary`,
|
|
355
|
+
then inserts the image into the current editor.
|
|
356
|
+
|
|
357
|
+
Keep the byte/base64 adapter functions at the app bridge boundary if multiple
|
|
358
|
+
components need images. Component code should call domain-named helpers, not
|
|
359
|
+
repeat raw bridge conversion details.
|
|
360
|
+
|
|
361
|
+
Use `insertCollectionImageFromBinaryResult` when the app already loaded an
|
|
362
|
+
asset as `{ mimeType, base64 }` and needs to insert it into the editor, for
|
|
363
|
+
example from an asset picker.
|
|
364
|
+
|
|
365
|
+
## Auto Review
|
|
366
|
+
|
|
367
|
+
Auto review means: collect paragraphs from the current document, send those
|
|
368
|
+
paragraphs to an app-provided review model/tool, validate the returned findings
|
|
369
|
+
against the original paragraph text, then apply accepted findings as editor
|
|
370
|
+
comments.
|
|
371
|
+
|
|
372
|
+
The editor owns paragraph collection, finding validation, and comment
|
|
373
|
+
application. The app owns the review provider because the app decides which
|
|
374
|
+
agent, model, network call, prompt, or bridge helper performs the review.
|
|
375
|
+
|
|
376
|
+
Use auto review for document products that need editorial annotations, for
|
|
377
|
+
example copy editing, fact checking, source review, logic review, claim review,
|
|
378
|
+
or voice review. Do not use it for normal spellcheck or for apps that only need
|
|
379
|
+
a plain text box.
|
|
380
|
+
|
|
381
|
+
The provider contract is:
|
|
382
|
+
|
|
383
|
+
```tsx
|
|
384
|
+
import {
|
|
385
|
+
buildAutoReviewPrompt,
|
|
386
|
+
parseAutoReviewFindingResponse,
|
|
387
|
+
type AutoReviewFindingsProvider,
|
|
388
|
+
} from '@puredesktop/puredesktop-ui-bridge/editor'
|
|
389
|
+
|
|
390
|
+
const findingsProvider: AutoReviewFindingsProvider = async ({
|
|
391
|
+
paragraphs,
|
|
392
|
+
options,
|
|
393
|
+
}) => {
|
|
394
|
+
const prompt = buildAutoReviewPrompt('copyedit', paragraphs)
|
|
395
|
+
|
|
396
|
+
// Call the app's chosen review path here:
|
|
397
|
+
// - an app-scoped shell agent session;
|
|
398
|
+
// - a permitted network endpoint;
|
|
399
|
+
// - an app-owned bridge helper.
|
|
400
|
+
const modelResponse = await runDocumentReview(prompt, options)
|
|
401
|
+
|
|
402
|
+
return parseAutoReviewFindingResponse(modelResponse)
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
`modelResponse` must be JSON that describes findings. Each finding points at an
|
|
407
|
+
exact span inside one collected paragraph:
|
|
408
|
+
|
|
409
|
+
```json
|
|
410
|
+
{
|
|
411
|
+
"findings": [
|
|
412
|
+
{
|
|
413
|
+
"paragraph_id": "p-1",
|
|
414
|
+
"exact_text": "the exact words in the paragraph",
|
|
415
|
+
"annotation_type": "copyedit",
|
|
416
|
+
"suggested_note": "Short note shown to the user.",
|
|
417
|
+
"severity": "minor",
|
|
418
|
+
"confidence": 0.8
|
|
419
|
+
}
|
|
420
|
+
]
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Then run the review from the editor handle:
|
|
425
|
+
|
|
426
|
+
```tsx
|
|
427
|
+
const result = await editorRef.current?.runAutoReview({
|
|
428
|
+
process: 'copyedit',
|
|
429
|
+
enabledTypes: ['copyedit'],
|
|
430
|
+
findingsProvider,
|
|
431
|
+
maxFindings: 12,
|
|
432
|
+
minConfidence: 0.55,
|
|
433
|
+
})
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
If `findingsProvider` is missing, `runAutoReview` returns an
|
|
437
|
+
`unavailableReason` and does not annotate the document. If a finding's
|
|
438
|
+
`exact_text` cannot be found in its paragraph, the editor skips it. The model
|
|
439
|
+
does not edit the document directly; it only returns findings that the editor
|
|
440
|
+
validates and applies as comments.
|
|
@@ -10,7 +10,7 @@ agent tools.
|
|
|
10
10
|
"schemaVersion": 1,
|
|
11
11
|
"id": "{{PLUGIN_ID}}",
|
|
12
12
|
"name": "{{APP_TITLE}}",
|
|
13
|
-
"permissions": ["network", "settings", "filesystem", "agents"],
|
|
13
|
+
"permissions": ["network", "settings", "filesystem", "agents"],
|
|
14
14
|
"entrypoint": {
|
|
15
15
|
"kind": "dev-url",
|
|
16
16
|
"url": "http://localhost:{{APP_PORT}}"
|
|
@@ -1,84 +1,89 @@
|
|
|
1
|
-
# UI And Components
|
|
2
|
-
|
|
3
|
-
The generated app starts with a small starter workspace so the iframe has a
|
|
4
|
-
useful screen while the first builder task runs. Treat the starter as temporary
|
|
5
|
-
scaffold, not product UI.
|
|
6
|
-
|
|
7
|
-
`src/App.tsx` owns bridge readiness and `AppFrame`. `src/components/AppShell.tsx`
|
|
8
|
-
is the thin composition root after boot succeeds. Do not turn either file into
|
|
9
|
-
the product implementation.
|
|
10
|
-
|
|
11
|
-
## AppFrame
|
|
12
|
-
|
|
13
|
-
Every render path in `src/App.tsx` uses `AppFrame`. This gives the iframe the
|
|
14
|
-
PureDesktop reset, theme CSS variables, and shell-compatible surface styling.
|
|
15
|
-
|
|
16
|
-
## App Structure
|
|
17
|
-
|
|
18
|
-
Use the first real product task to replace the starter workspace with app-owned
|
|
19
|
-
surfaces. Keep responsibilities split:
|
|
20
|
-
|
|
21
|
-
- `src/App.tsx`: bridge readiness, boot loading, error states, and `AppFrame`.
|
|
22
|
-
- `src/components/AppShell.tsx`: thin composition root that wires boot data into
|
|
23
|
-
product surfaces.
|
|
24
|
-
- `src/components/*`: product screens, panels, lists, editors, dialogs, and
|
|
25
|
-
controls.
|
|
26
|
-
- `src/hooks/*`: reusable workflow state, effects, synchronization, and bridge
|
|
27
|
-
orchestration.
|
|
28
|
-
- `src/lib/*`: domain types, reducers, rules, transforms, fixtures, and tests.
|
|
29
|
-
|
|
30
|
-
Start with controlled, simple modules:
|
|
31
|
-
|
|
32
|
-
```text
|
|
33
|
-
src/
|
|
34
|
-
components/
|
|
35
|
-
AppShell.tsx
|
|
36
|
-
StarterWorkspace.tsx # replace when the first product surface exists
|
|
37
|
-
ProjectBoard.tsx
|
|
38
|
-
ProjectDetail.tsx
|
|
39
|
-
hooks/
|
|
40
|
-
useProjects.ts
|
|
41
|
-
lib/
|
|
42
|
-
projects.ts
|
|
43
|
-
projects.test.ts
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Keep state local only while it is display-only. When state drives multiple
|
|
47
|
-
components, persistence, agent tools, bridge calls, or domain rules, move it into
|
|
48
|
-
an app hook or `src/lib` module before adding more screens.
|
|
49
|
-
|
|
50
|
-
## Platform Components
|
|
51
|
-
|
|
52
|
-
Import shared UI by exact exported package paths before creating custom
|
|
53
|
-
primitives. Use existing app folders before creating new ones.
|
|
54
|
-
|
|
55
|
-
Useful starting points:
|
|
56
|
-
|
|
57
|
-
- `AppFrame` from `@puredesktop/puredesktop-ui-bridge/components/common/containers/AppFrame`
|
|
58
|
-
- `EmptyState` from `@puredesktop/puredesktop-ui-bridge/components/common/feedback/EmptyState`
|
|
1
|
+
# UI And Components
|
|
2
|
+
|
|
3
|
+
The generated app starts with a small starter workspace so the iframe has a
|
|
4
|
+
useful screen while the first builder task runs. Treat the starter as temporary
|
|
5
|
+
scaffold, not product UI.
|
|
6
|
+
|
|
7
|
+
`src/App.tsx` owns bridge readiness and `AppFrame`. `src/components/AppShell.tsx`
|
|
8
|
+
is the thin composition root after boot succeeds. Do not turn either file into
|
|
9
|
+
the product implementation.
|
|
10
|
+
|
|
11
|
+
## AppFrame
|
|
12
|
+
|
|
13
|
+
Every render path in `src/App.tsx` uses `AppFrame`. This gives the iframe the
|
|
14
|
+
PureDesktop reset, theme CSS variables, and shell-compatible surface styling.
|
|
15
|
+
|
|
16
|
+
## App Structure
|
|
17
|
+
|
|
18
|
+
Use the first real product task to replace the starter workspace with app-owned
|
|
19
|
+
surfaces. Keep responsibilities split:
|
|
20
|
+
|
|
21
|
+
- `src/App.tsx`: bridge readiness, boot loading, error states, and `AppFrame`.
|
|
22
|
+
- `src/components/AppShell.tsx`: thin composition root that wires boot data into
|
|
23
|
+
product surfaces.
|
|
24
|
+
- `src/components/*`: product screens, panels, lists, editors, dialogs, and
|
|
25
|
+
controls.
|
|
26
|
+
- `src/hooks/*`: reusable workflow state, effects, synchronization, and bridge
|
|
27
|
+
orchestration.
|
|
28
|
+
- `src/lib/*`: domain types, reducers, rules, transforms, fixtures, and tests.
|
|
29
|
+
|
|
30
|
+
Start with controlled, simple modules:
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
src/
|
|
34
|
+
components/
|
|
35
|
+
AppShell.tsx
|
|
36
|
+
StarterWorkspace.tsx # replace when the first product surface exists
|
|
37
|
+
ProjectBoard.tsx
|
|
38
|
+
ProjectDetail.tsx
|
|
39
|
+
hooks/
|
|
40
|
+
useProjects.ts
|
|
41
|
+
lib/
|
|
42
|
+
projects.ts
|
|
43
|
+
projects.test.ts
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Keep state local only while it is display-only. When state drives multiple
|
|
47
|
+
components, persistence, agent tools, bridge calls, or domain rules, move it into
|
|
48
|
+
an app hook or `src/lib` module before adding more screens.
|
|
49
|
+
|
|
50
|
+
## Platform Components
|
|
51
|
+
|
|
52
|
+
Import shared UI by exact exported package paths before creating custom
|
|
53
|
+
primitives. Use existing app folders before creating new ones.
|
|
54
|
+
|
|
55
|
+
Useful starting points:
|
|
56
|
+
|
|
57
|
+
- `AppFrame` from `@puredesktop/puredesktop-ui-bridge/components/common/containers/AppFrame`
|
|
58
|
+
- `EmptyState` from `@puredesktop/puredesktop-ui-bridge/components/common/feedback/EmptyState`
|
|
59
59
|
- `Heading` from `@puredesktop/puredesktop-ui-bridge/components/common/typography/Heading`
|
|
60
60
|
- `Text` from `@puredesktop/puredesktop-ui-bridge/components/common/typography/Text`
|
|
61
61
|
- `Button` from `@puredesktop/puredesktop-ui-bridge/components/common/buttons/Button`
|
|
62
|
+
- `DocumentEditor` from `@puredesktop/puredesktop-ui-bridge/editor`
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
```ts
|
|
68
|
-
const Root = styled.div`
|
|
69
|
-
display: flex;
|
|
70
|
-
flex-direction: column;
|
|
71
|
-
gap: var(--platform-spacing-md);
|
|
72
|
-
color: var(--platform-colors-text);
|
|
73
|
-
background: var(--platform-colors-bg);
|
|
74
|
-
`
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
Use app-local CSS variables only for app-specific meaning, for example
|
|
78
|
-
`--{{APP_SLUG}}-accent`.
|
|
64
|
+
For rich writing surfaces, review workflows, markdown/HTML documents, and
|
|
65
|
+
document-specific toolbar controls, see
|
|
66
|
+
[Using the document editor](./howtos/using-document-editor.md).
|
|
79
67
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
68
|
+
## Styling
|
|
69
|
+
|
|
70
|
+
Use styled-components and platform CSS variables:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const Root = styled.div`
|
|
74
|
+
display: flex;
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
gap: var(--platform-spacing-md);
|
|
77
|
+
color: var(--platform-colors-text);
|
|
78
|
+
background: var(--platform-colors-bg);
|
|
79
|
+
`
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Use app-local CSS variables only for app-specific meaning, for example
|
|
83
|
+
`--{{APP_SLUG}}-accent`.
|
|
84
|
+
|
|
85
|
+
See [Theme CSS variables](./theme-vars.md) for the generated list of available
|
|
86
|
+
platform variables by domain.
|
|
87
|
+
|
|
88
|
+
Avoid inline styles except for tiny dynamic values that cannot be represented as
|
|
89
|
+
props on a styled component.
|
package/template/gitignore.tpl
CHANGED
package/template/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"puredesktop:check": "node scripts/validate-puredesktop-app.mjs"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@puredesktop/puredesktop-ui-bridge": "2.1.
|
|
13
|
+
"@puredesktop/puredesktop-ui-bridge": "2.1.9",
|
|
14
14
|
"react": "^19.1.0",
|
|
15
15
|
"react-dom": "^19.1.0",
|
|
16
16
|
"styled-components": "^6.1.18"
|
package/template/plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"id": "{{PLUGIN_ID}}",
|
|
4
4
|
"name": "{{APP_TITLE}}",
|
|
5
|
-
"permissions": ["network", "settings", "filesystem", "agents"],
|
|
5
|
+
"permissions": ["network", "settings", "filesystem", "agents"],
|
|
6
6
|
"entrypoint": {
|
|
7
7
|
"kind": "dev-url",
|
|
8
8
|
"url": "http://localhost:{{APP_PORT}}"
|