@puredesktop/create-app 2.1.7 → 2.1.8
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 +4 -3
- package/package.json +1 -1
- package/template/README.md +3 -1
- package/template/docs/README.md +8 -3
- package/template/docs/howtos/adding-agent-tool.md +106 -0
- package/template/docs/howtos/persisting-app-state.md +242 -0
- package/template/docs/plugin-manifest.md +1 -1
- package/template/docs/theme-vars.md +138 -138
- package/template/docs/tool-handlers.md +3 -0
- package/template/docs/ui-and-components.md +75 -51
- package/template/package.json +1 -1
- package/template/plugin.json +1 -1
- package/template/scripts/validate-puredesktop-app.mjs +46 -8
- package/template/src/App.tsx +5 -4
- package/template/src/components/AppShell.tsx +11 -33
- package/template/src/components/StarterWorkspace.tsx +154 -0
- package/template/src/constants.ts +2 -1
- package/template/src/lib/starterWorkspace.ts +29 -0
- package/template/vite.config.js +13 -10
|
@@ -394,7 +394,8 @@ Before shipping:
|
|
|
394
394
|
|
|
395
395
|
Register in PureDesktop (File -> Register App...) with:
|
|
396
396
|
entrypoint: http://localhost:${options.port}
|
|
397
|
-
permissions: network, settings, agents
|
|
397
|
+
permissions: network, settings, filesystem, agents
|
|
398
398
|
|
|
399
|
-
Build
|
|
400
|
-
|
|
399
|
+
Build product surfaces in src/components, workflow hooks in src/hooks, and domain logic in src/lib.
|
|
400
|
+
Keep src/App.tsx and src/components/AppShell.tsx thin.
|
|
401
|
+
`)
|
package/package.json
CHANGED
package/template/README.md
CHANGED
|
@@ -36,7 +36,9 @@ and declared app agent tools.
|
|
|
36
36
|
- `src/App.tsx`: bridge and boot gates wrapped in `AppFrame`.
|
|
37
37
|
- `src/bridge/platformBridge.ts`: local app bridge exports.
|
|
38
38
|
- `src/hooks/useAppBoot.ts`: boot data loading after bridge readiness.
|
|
39
|
-
- `src/components/AppShell.tsx`:
|
|
39
|
+
- `src/components/AppShell.tsx`: thin app-owned composition root.
|
|
40
|
+
- `src/components/StarterWorkspace.tsx`: temporary starter screen to replace
|
|
41
|
+
when the first product surface exists.
|
|
40
42
|
- `docs/`: builder-facing app, bridge, agent, and tool-handler docs.
|
|
41
43
|
|
|
42
44
|
## Builder Docs
|
package/template/docs/README.md
CHANGED
|
@@ -14,8 +14,9 @@ Read these in order when building or repairing an app:
|
|
|
14
14
|
3. [Bridge helpers](./bridge/README.md)
|
|
15
15
|
4. [App agent](./agent.md)
|
|
16
16
|
5. [Tool handlers](./tool-handlers.md)
|
|
17
|
-
6. [
|
|
18
|
-
7. [
|
|
17
|
+
6. [Adding an app agent tool](./howtos/adding-agent-tool.md)
|
|
18
|
+
7. [UI and components](./ui-and-components.md)
|
|
19
|
+
8. [Theme CSS variables](./theme-vars.md)
|
|
19
20
|
|
|
20
21
|
Key files in this scaffold:
|
|
21
22
|
|
|
@@ -25,7 +26,11 @@ Key files in this scaffold:
|
|
|
25
26
|
- `src/App.tsx`: bridge readiness, boot loading, and `AppFrame` wrapper.
|
|
26
27
|
- `src/bridge/platformBridge.ts`: local app bridge surface.
|
|
27
28
|
- `src/hooks/useAppBoot.ts`: app boot data loading after the bridge is ready.
|
|
28
|
-
- `src/components/AppShell.tsx`:
|
|
29
|
+
- `src/components/AppShell.tsx`: thin app-owned composition root.
|
|
30
|
+
- `src/components/StarterWorkspace.tsx`: temporary starter screen to replace
|
|
31
|
+
when the first product surface exists.
|
|
32
|
+
- `src/lib/starterWorkspace.ts`: starter data/helper example showing where
|
|
33
|
+
reusable non-rendering logic belongs.
|
|
29
34
|
- `scripts/validate-puredesktop-app.mjs`: registration and tool scaffold check.
|
|
30
35
|
|
|
31
36
|
Use `npm run puredesktop:check` after `npm run build` before registration.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Adding An App Agent Tool
|
|
2
|
+
|
|
3
|
+
App agent tools are a three-file contract. A tool is not available until the
|
|
4
|
+
manifest, registration catalog, and runtime handler all contain the same name.
|
|
5
|
+
|
|
6
|
+
## Edit Sequence
|
|
7
|
+
|
|
8
|
+
1. Add the tool declaration to `plugin.json` under `app.agents.tools`.
|
|
9
|
+
Include a clear `description`, an `inputSchema`, and `requiresApproval`.
|
|
10
|
+
Set `requiresApproval: true` for writes that mutate app data, files,
|
|
11
|
+
external services, or broad app state.
|
|
12
|
+
|
|
13
|
+
2. Add the same tool name to `src/agents/catalog.ts`.
|
|
14
|
+
`APP_AGENT_TOOL_NAMES` is the list registered with the shell at runtime. Keep
|
|
15
|
+
it in the same order as `plugin.json` `app.agents.tools`.
|
|
16
|
+
|
|
17
|
+
3. Add the same tool name to `src/agents/handlers/index.ts`.
|
|
18
|
+
Implement the handler with app-owned state or domain helpers. Return compact
|
|
19
|
+
JSON for reads and a short result for writes. Return
|
|
20
|
+
`agentToolErrorContent(message)` for invalid input the assistant can correct.
|
|
21
|
+
|
|
22
|
+
4. Update `agents.md` when the app assistant needs to know when or how to use
|
|
23
|
+
the tool.
|
|
24
|
+
|
|
25
|
+
5. Run checks:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm run typecheck
|
|
29
|
+
npm run build
|
|
30
|
+
npm run puredesktop:check
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
During dev mode, `npm run typecheck` is enough to catch TypeScript errors, but
|
|
34
|
+
`npm run puredesktop:check` only proves the final package after `npm run build`
|
|
35
|
+
refreshes `dist`.
|
|
36
|
+
|
|
37
|
+
## Existing Tool Scaffold
|
|
38
|
+
|
|
39
|
+
For apps that already have tools, these files already exist:
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
src/
|
|
43
|
+
agents/
|
|
44
|
+
catalog.ts
|
|
45
|
+
handlers/
|
|
46
|
+
index.ts
|
|
47
|
+
hooks/
|
|
48
|
+
useAppAgentTools.ts
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Only edit `plugin.json`, `src/agents/catalog.ts`,
|
|
52
|
+
`src/agents/handlers/index.ts`, and optionally `agents.md`.
|
|
53
|
+
|
|
54
|
+
## First Tool In An App
|
|
55
|
+
|
|
56
|
+
If `src/agents/` does not exist, create the scaffold:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
// src/agents/catalog.ts
|
|
60
|
+
export const APP_AGENT_TOOL_NAMES = ['listItems'] as const
|
|
61
|
+
|
|
62
|
+
export const APP_AGENT_LOG_LABEL = '{{APP_SLUG}}'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
// src/agents/handlers/index.ts
|
|
67
|
+
import { agentToolErrorContent } from '@puredesktop/puredesktop-ui-bridge/bridge/agentToolHelpers'
|
|
68
|
+
import type { AgentToolHandler } from '@puredesktop/puredesktop-ui-bridge/bridge/react/usePlatformAgentTools'
|
|
69
|
+
|
|
70
|
+
const listItems: AgentToolHandler = async () => {
|
|
71
|
+
return agentToolErrorContent('listItems is not implemented yet.')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const appAgentHandlers = {
|
|
75
|
+
listItems,
|
|
76
|
+
} satisfies Record<string, AgentToolHandler>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// src/hooks/useAppAgentTools.ts
|
|
81
|
+
import { usePlatformAgentTools } from '@puredesktop/puredesktop-ui-bridge/bridge/react/usePlatformAgentTools'
|
|
82
|
+
import { APP_AGENT_LOG_LABEL, APP_AGENT_TOOL_NAMES } from '../agents/catalog'
|
|
83
|
+
import { appAgentHandlers } from '../agents/handlers'
|
|
84
|
+
|
|
85
|
+
export function useAppAgentTools(ready: boolean): void {
|
|
86
|
+
usePlatformAgentTools({
|
|
87
|
+
ready,
|
|
88
|
+
tools: APP_AGENT_TOOL_NAMES,
|
|
89
|
+
logLabel: APP_AGENT_LOG_LABEL,
|
|
90
|
+
handlers: appAgentHandlers,
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Then wire the hook in `src/App.tsx` immediately after `usePlatformBridge()`:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const { error: bridgeError, ready } = usePlatformBridge()
|
|
99
|
+
useAppAgentTools(ready)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Common Failure
|
|
103
|
+
|
|
104
|
+
Do not update only `plugin.json` and the handler. If `APP_AGENT_TOOL_NAMES`
|
|
105
|
+
does not include the new tool, the iframe never registers it with the shell. The
|
|
106
|
+
assistant will see a declared tool that the running app cannot handle.
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# Persisting App State
|
|
2
|
+
|
|
3
|
+
PureDesktop apps persist non-document state through UI bridge helpers. This
|
|
4
|
+
state is app-owned: the app defines the TypeScript type, default value, parser,
|
|
5
|
+
and update commands.
|
|
6
|
+
|
|
7
|
+
## State Surfaces
|
|
8
|
+
|
|
9
|
+
| State | Helper | Permission |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| Small app preferences | `usePlatformAppSettings` | `settings` |
|
|
12
|
+
| Internal JSON records | `usePlatformJsonStore` | `filesystem` |
|
|
13
|
+
|
|
14
|
+
Use document lifecycle APIs for user-visible documents, drafts, recents, rename,
|
|
15
|
+
duplicate, and open/save workflows.
|
|
16
|
+
|
|
17
|
+
## Generated App Wiring
|
|
18
|
+
|
|
19
|
+
The generated app already has the bridge readiness boundary:
|
|
20
|
+
|
|
21
|
+
- `src/App.tsx` calls `usePlatformBridge()` and receives `ready`.
|
|
22
|
+
- `src/App.tsx` calls `useAppBoot(ready || standaloneDev)`.
|
|
23
|
+
- `src/hooks/useAppBoot.ts` loads startup settings through the app bridge.
|
|
24
|
+
- `src/components/AppShell.tsx` receives boot data as props.
|
|
25
|
+
|
|
26
|
+
Use that structure instead of adding bridge calls inside components.
|
|
27
|
+
|
|
28
|
+
When state is read once at startup, `useAppBoot` can load it and pass it down.
|
|
29
|
+
When state can change while the app is open, create a dedicated hook in
|
|
30
|
+
`src/hooks/` that calls the platform helper and exposes typed commands.
|
|
31
|
+
|
|
32
|
+
## Add Editable App Settings
|
|
33
|
+
|
|
34
|
+
1. Create or reuse the domain folder under `src/lib/<domain>/`.
|
|
35
|
+
2. Define the settings type, default value, and parser in that domain folder.
|
|
36
|
+
3. Create `src/hooks/use<Domain>Settings.ts`.
|
|
37
|
+
4. Inside that hook, call `usePlatformAppSettings`.
|
|
38
|
+
5. Pass `APP_SLUG` and the bridge `ready` value to the hook.
|
|
39
|
+
6. Return typed settings plus app-specific command functions.
|
|
40
|
+
7. Components call those command functions; they do not build settings patches.
|
|
41
|
+
|
|
42
|
+
Hook shape:
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
use real domain settings type
|
|
46
|
+
call usePlatformAppSettings with APP_SLUG, ready, default settings, parser
|
|
47
|
+
return settings, loading, error
|
|
48
|
+
return field-specific commands that call patchSettings
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Field-specific commands own the settings patch. Components call the command; they
|
|
52
|
+
do not assemble the patch object.
|
|
53
|
+
|
|
54
|
+
## Add An Internal JSON Store
|
|
55
|
+
|
|
56
|
+
1. Add `filesystem` to `plugin.json`.
|
|
57
|
+
2. Create or reuse the domain folder under `src/lib/<domain>/`.
|
|
58
|
+
3. Define the store type, empty value, parser, and pure update helpers in that
|
|
59
|
+
domain folder.
|
|
60
|
+
4. Create `src/hooks/use<Domain>Store.ts`.
|
|
61
|
+
5. Inside that hook, call `usePlatformJsonStore`.
|
|
62
|
+
6. Use a file name that includes `APP_SLUG` and the real persisted record name.
|
|
63
|
+
7. Return typed store state plus app-specific command functions.
|
|
64
|
+
8. Components call those command functions; they do not edit raw JSON objects.
|
|
65
|
+
|
|
66
|
+
Hook shape:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
use real domain store type
|
|
70
|
+
call usePlatformJsonStore with APP_SLUG, ready, file name, empty store, parser
|
|
71
|
+
return store, loading, saving, error
|
|
72
|
+
return domain commands that call updateValue with pure update functions
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Domain commands own the store update. Components call the command; they do not
|
|
76
|
+
edit raw JSON objects. Store update functions must return new store objects.
|
|
77
|
+
|
|
78
|
+
## App Settings
|
|
79
|
+
|
|
80
|
+
Import:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { usePlatformAppSettings } from '@puredesktop/puredesktop-ui-bridge/bridge/react/usePlatformAppSettings'
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Use for small app preferences such as current mode, sorting, filtering, and
|
|
87
|
+
lightweight options.
|
|
88
|
+
|
|
89
|
+
Options:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
{
|
|
93
|
+
appSlug: string | null
|
|
94
|
+
enabled?: boolean
|
|
95
|
+
initialSettings?: TSettings
|
|
96
|
+
parse?: (value: Record<string, unknown>) => TSettings
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
{
|
|
104
|
+
settings: TSettings
|
|
105
|
+
loading: boolean
|
|
106
|
+
error: Error | null
|
|
107
|
+
patchSettings: (patch: Record<string, unknown>) => Promise<TSettings>
|
|
108
|
+
reload: () => void
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Behavior:
|
|
113
|
+
|
|
114
|
+
- loads through `settings.app.get`;
|
|
115
|
+
- saves through `settings.app.update`;
|
|
116
|
+
- `patchSettings` shallow-merges the patch in shell preferences;
|
|
117
|
+
- the shell stores app settings under the current app slug;
|
|
118
|
+
- the app parser converts persisted `Record<string, unknown>` into the app's
|
|
119
|
+
typed settings object.
|
|
120
|
+
|
|
121
|
+
App ownership:
|
|
122
|
+
|
|
123
|
+
- keep the settings type in app code;
|
|
124
|
+
- keep defaults and parsing in app code;
|
|
125
|
+
- expose typed settings and typed commands to components;
|
|
126
|
+
- use app settings for preferences, not large record collections.
|
|
127
|
+
|
|
128
|
+
## JSON Store
|
|
129
|
+
|
|
130
|
+
Import:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { usePlatformJsonStore } from '@puredesktop/puredesktop-ui-bridge/bridge/react/usePlatformJsonStore'
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Use for internal app records that are larger than settings and are not
|
|
137
|
+
user-visible documents.
|
|
138
|
+
|
|
139
|
+
Options:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
{
|
|
143
|
+
appSlug: string | null
|
|
144
|
+
fileName: `${string}.json` | null
|
|
145
|
+
initialValue: TValue
|
|
146
|
+
enabled?: boolean
|
|
147
|
+
parse?: (value: unknown) => TValue
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
{
|
|
155
|
+
value: TValue
|
|
156
|
+
path: string | null
|
|
157
|
+
loading: boolean
|
|
158
|
+
saving: boolean
|
|
159
|
+
error: Error | null
|
|
160
|
+
saveValue: (next: TValue) => Promise<TValue>
|
|
161
|
+
updateValue: (updater: (current: TValue) => TValue) => Promise<TValue>
|
|
162
|
+
reload: () => void
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Behavior:
|
|
167
|
+
|
|
168
|
+
- loads through `storage.readJson`;
|
|
169
|
+
- saves through `storage.writeJson`;
|
|
170
|
+
- missing files load as `initialValue`;
|
|
171
|
+
- saved values replace the full JSON value;
|
|
172
|
+
- `updateValue` queues writes and applies each updater to the latest in-memory
|
|
173
|
+
value;
|
|
174
|
+
- `fileName` must be a single `.json` file name;
|
|
175
|
+
- the JSON file is stored in the shell-managed appdata folder.
|
|
176
|
+
|
|
177
|
+
App ownership:
|
|
178
|
+
|
|
179
|
+
- include `filesystem` in `plugin.json`;
|
|
180
|
+
- keep the store type in app code;
|
|
181
|
+
- keep defaults, parsing, and pure update functions in app code;
|
|
182
|
+
- use an app-prefixed file name with the real persisted record name;
|
|
183
|
+
- expose typed state and typed commands to components;
|
|
184
|
+
- never mutate the current store object in place.
|
|
185
|
+
|
|
186
|
+
## File Shape
|
|
187
|
+
|
|
188
|
+
Use one `src/lib/<domain>/` folder per persisted domain. Do not put types,
|
|
189
|
+
defaults, parsing, pure updates, platform hooks, and UI in one file.
|
|
190
|
+
|
|
191
|
+
```text
|
|
192
|
+
src/
|
|
193
|
+
hooks/
|
|
194
|
+
use<Domain>Settings.ts
|
|
195
|
+
use<Domain>Store.ts
|
|
196
|
+
lib/
|
|
197
|
+
<domain>/
|
|
198
|
+
types.ts
|
|
199
|
+
defaults.ts
|
|
200
|
+
parse.ts
|
|
201
|
+
updates.ts
|
|
202
|
+
index.ts
|
|
203
|
+
components/
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Responsibilities:
|
|
207
|
+
|
|
208
|
+
- `types.ts` exports the persisted domain types.
|
|
209
|
+
- `defaults.ts` exports default settings or empty store values.
|
|
210
|
+
- `parse.ts` converts unknown persisted values into valid domain state.
|
|
211
|
+
- `updates.ts` contains pure functions that return new state objects.
|
|
212
|
+
- `index.ts` re-exports the domain pieces used outside the folder.
|
|
213
|
+
- `src/hooks/use<Domain>Settings.ts` calls `usePlatformAppSettings`.
|
|
214
|
+
- `src/hooks/use<Domain>Store.ts` calls `usePlatformJsonStore`.
|
|
215
|
+
- components import the app-owned hooks and render state.
|
|
216
|
+
|
|
217
|
+
Split a file when it starts owning more than one responsibility. The hook should
|
|
218
|
+
not define the domain model. Components should not parse persisted data. Pure
|
|
219
|
+
update functions should not call platform helpers.
|
|
220
|
+
|
|
221
|
+
## Verification
|
|
222
|
+
|
|
223
|
+
Run:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
npm run typecheck
|
|
227
|
+
npm run build
|
|
228
|
+
npm run puredesktop:check
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Source checks:
|
|
232
|
+
|
|
233
|
+
- app settings state has a domain settings type, default value, parser, and
|
|
234
|
+
hook;
|
|
235
|
+
- JSON store state has a domain store type, empty value, parser, pure update
|
|
236
|
+
helpers, and hook;
|
|
237
|
+
- components import the app-owned hooks instead of platform persistence helpers;
|
|
238
|
+
- `usePlatformJsonStore` is only imported from app-owned hooks;
|
|
239
|
+
- apps using `usePlatformJsonStore` include `filesystem` in `plugin.json`;
|
|
240
|
+
- JSON store file names include `APP_SLUG` and the real persisted record name;
|
|
241
|
+
- JSON store update functions return new objects instead of mutating existing
|
|
242
|
+
state.
|
|
@@ -10,7 +10,7 @@ agent tools.
|
|
|
10
10
|
"schemaVersion": 1,
|
|
11
11
|
"id": "{{PLUGIN_ID}}",
|
|
12
12
|
"name": "{{APP_TITLE}}",
|
|
13
|
-
"permissions": ["network", "settings", "agents"],
|
|
13
|
+
"permissions": ["network", "settings", "filesystem", "agents"],
|
|
14
14
|
"entrypoint": {
|
|
15
15
|
"kind": "dev-url",
|
|
16
16
|
"url": "http://localhost:{{APP_PORT}}"
|
|
@@ -1,138 +1,138 @@
|
|
|
1
|
-
# Theme CSS Variables
|
|
2
|
-
|
|
3
|
-
This file is generated from `packages/ui/src/theme/tokens.ts`.
|
|
4
|
-
|
|
5
|
-
PureDesktop injects these variables through `AppFrame` and the shell bridge
|
|
6
|
-
theme payload. Use them in app styling instead of hardcoded platform colors,
|
|
7
|
-
spacing, typography, radii, shadows, transitions, or app stacking values.
|
|
8
|
-
|
|
9
|
-
This static reference is generated by PureDesktop maintainers before the
|
|
10
|
-
create-app package is released.
|
|
11
|
-
|
|
12
|
-
| Variable | Domain | Light value | Dark value |
|
|
13
|
-
| --- | --- | --- | --- |
|
|
14
|
-
| `--platform-colors-accent` | colors | `#1b1b1e` | `#e5e5e9` |
|
|
15
|
-
| `--platform-colors-accent-hover` | colors | `#1b1b1e` | `#eaeaee` |
|
|
16
|
-
| `--platform-colors-accent-muted` | colors | `#eaeaee` | `#313135` |
|
|
17
|
-
| `--platform-colors-app-viewport` | colors | `#fbfbfc` | `#202024` |
|
|
18
|
-
| `--platform-colors-bg` | colors | `#fbfbfc` | `#202024` |
|
|
19
|
-
| `--platform-colors-border` | colors | `#e5e5e9` | `#313135` |
|
|
20
|
-
| `--platform-colors-border-strong` | colors | `#dadade` | `#43434a` |
|
|
21
|
-
| `--platform-colors-chrome-tab-strip` | colors | `#ececef` | `#1b1b1e` |
|
|
22
|
-
| `--platform-colors-chrome-titlebar` | colors | `#fbfbfc` | `#202024` |
|
|
23
|
-
| `--platform-colors-danger` | colors | `#b23a2e` | `#e0796e` |
|
|
24
|
-
| `--platform-colors-divider` | colors | `#e5e5e9` | `#313135` |
|
|
25
|
-
| `--platform-colors-elevated` | colors | `#ffffff` | `#26262a` |
|
|
26
|
-
| `--platform-colors-focus` | colors | `#1b1b1e` | `#e5e5e9` |
|
|
27
|
-
| `--platform-colors-info` | colors | `#3b6fb0` | `#6fa0e0` |
|
|
28
|
-
| `--platform-colors-selection` | colors | `#eaeaee` | `#313135` |
|
|
29
|
-
| `--platform-colors-semantic-blue` | colors | `#3b6fb0` | `#6fa0e0` |
|
|
30
|
-
| `--platform-colors-semantic-blue-border` | colors | `#b9cbe4` | `#365373` |
|
|
31
|
-
| `--platform-colors-semantic-blue-muted` | colors | `#e8f0fb` | `#1c2a3c` |
|
|
32
|
-
| `--platform-colors-semantic-blue-text` | colors | `#244f87` | `#a8c8f0` |
|
|
33
|
-
| `--platform-colors-semantic-green` | colors | `#1f8a55` | `#57b988` |
|
|
34
|
-
| `--platform-colors-semantic-green-border` | colors | `#1f8a55` | `#32684d` |
|
|
35
|
-
| `--platform-colors-semantic-green-muted` | colors | `#eef7f1` | `#18342a` |
|
|
36
|
-
| `--platform-colors-semantic-green-text` | colors | `#16683f` | `#8fe0b6` |
|
|
37
|
-
| `--platform-colors-semantic-orange` | colors | `#c98a2b` | `#e0b257` |
|
|
38
|
-
| `--platform-colors-semantic-orange-border` | colors | `#e3c98f` | `#6d5b2f` |
|
|
39
|
-
| `--platform-colors-semantic-orange-muted` | colors | `#f3e5c8` | `#3a3322` |
|
|
40
|
-
| `--platform-colors-semantic-orange-text` | colors | `#7a5114` | `#f0ce8a` |
|
|
41
|
-
| `--platform-colors-semantic-red` | colors | `#b23a2e` | `#e0796e` |
|
|
42
|
-
| `--platform-colors-semantic-red-border` | colors | `#dfb7b1` | `#753f38` |
|
|
43
|
-
| `--platform-colors-semantic-red-muted` | colors | `#f4dfdc` | `#3a211d` |
|
|
44
|
-
| `--platform-colors-semantic-red-text` | colors | `#8a2d24` | `#f0a89e` |
|
|
45
|
-
| `--platform-colors-success` | colors | `#1f8a55` | `#57b988` |
|
|
46
|
-
| `--platform-colors-surface` | colors | `#f8f8fa` | `#26262a` |
|
|
47
|
-
| `--platform-colors-surface-active` | colors | `#eaeaee` | `#161619` |
|
|
48
|
-
| `--platform-colors-surface-hover` | colors | `#eaeaee` | `#2c2c30` |
|
|
49
|
-
| `--platform-colors-text` | colors | `#1b1b1e` | `#ececef` |
|
|
50
|
-
| `--platform-colors-text-danger` | colors | `#b23a2e` | `#e0796e` |
|
|
51
|
-
| `--platform-colors-text-disabled` | colors | `#9a9aa0` | `#6e6e74` |
|
|
52
|
-
| `--platform-colors-text-inverse` | colors | `#ffffff` | `#1b1b1e` |
|
|
53
|
-
| `--platform-colors-text-secondary` | colors | `#3a3a3e` | `#c9c9cf` |
|
|
54
|
-
| `--platform-colors-warning` | colors | `#c98a2b` | `#e0b257` |
|
|
55
|
-
| `--platform-radius-full` | radius | `999px` | `999px` |
|
|
56
|
-
| `--platform-radius-lg` | radius | `0px` | `0px` |
|
|
57
|
-
| `--platform-radius-md` | radius | `0px` | `0px` |
|
|
58
|
-
| `--platform-radius-sm` | radius | `0px` | `0px` |
|
|
59
|
-
| `--platform-shadow-lg` | shadow | `0 18px 48px rgb(18 18 22 / 0.13)` | `0 18px 44px rgba(0, 0, 0, 0.38)` |
|
|
60
|
-
| `--platform-shadow-md` | shadow | `0 8px 24px rgb(18 18 22 / 0.08)` | `0 8px 24px rgba(0, 0, 0, 0.3)` |
|
|
61
|
-
| `--platform-shadow-sm` | shadow | `none` | `none` |
|
|
62
|
-
| `--platform-spacing-lg` | spacing | `16px` | `16px` |
|
|
63
|
-
| `--platform-spacing-md` | spacing | `12px` | `12px` |
|
|
64
|
-
| `--platform-spacing-sm` | spacing | `8px` | `8px` |
|
|
65
|
-
| `--platform-spacing-xl` | spacing | `24px` | `24px` |
|
|
66
|
-
| `--platform-spacing-xs` | spacing | `4px` | `4px` |
|
|
67
|
-
| `--platform-spacing-xxl` | spacing | `32px` | `32px` |
|
|
68
|
-
| `--platform-transition-base` | transition | `200ms ease` | `200ms ease` |
|
|
69
|
-
| `--platform-transition-fast` | transition | `120ms ease` | `120ms ease` |
|
|
70
|
-
| `--platform-transition-slow` | transition | `320ms ease` | `320ms ease` |
|
|
71
|
-
| `--platform-typography-font-family` | typography | `"Archivo", system-ui, -apple-system, "Segoe UI", sans-serif` | `"Archivo", system-ui, -apple-system, "Segoe UI", sans-serif` |
|
|
72
|
-
| `--platform-typography-font-family-content` | typography | `'Source Serif 4', Georgia, serif` | `'Source Serif 4', Georgia, serif` |
|
|
73
|
-
| `--platform-typography-font-family-mono` | typography | `'JetBrains Mono', ui-monospace, monospace` | `'JetBrains Mono', ui-monospace, monospace` |
|
|
74
|
-
| `--platform-typography-font-size-base` | typography | `14.5px` | `14.5px` |
|
|
75
|
-
| `--platform-typography-font-size-lg` | typography | `17px` | `17px` |
|
|
76
|
-
| `--platform-typography-font-size-sm` | typography | `13px` | `13px` |
|
|
77
|
-
| `--platform-typography-font-size-xl` | typography | `24px` | `24px` |
|
|
78
|
-
| `--platform-typography-font-size-xs` | typography | `11px` | `11px` |
|
|
79
|
-
| `--platform-typography-font-weight-bold` | typography | `600` | `600` |
|
|
80
|
-
| `--platform-typography-font-weight-medium` | typography | `500` | `500` |
|
|
81
|
-
| `--platform-typography-font-weight-normal` | typography | `400` | `400` |
|
|
82
|
-
| `--platform-typography-line-height-base` | typography | `1.6` | `1.6` |
|
|
83
|
-
| `--platform-typography-line-height-tight` | typography | `1.25` | `1.25` |
|
|
84
|
-
| `--platform-z-index-zi-app-base` | zIndex | `0` | `0` |
|
|
85
|
-
| `--platform-z-index-zi-app-chrome` | zIndex | `20` | `20` |
|
|
86
|
-
| `--platform-z-index-zi-app-menus` | zIndex | `100` | `100` |
|
|
87
|
-
| `--platform-z-index-zi-app-modal` | zIndex | `300` | `300` |
|
|
88
|
-
| `--platform-z-index-zi-app-sidebar` | zIndex | `10` | `10` |
|
|
89
|
-
| `--platform-z-index-zi-app-toast` | zIndex | `200` | `200` |
|
|
90
|
-
| `--platform-z-index-zi-app-viewport-toolbar` | zIndex | `25` | `25` |
|
|
91
|
-
|
|
92
|
-
## App-Owned Colors
|
|
93
|
-
|
|
94
|
-
Use platform variables for the base app shell, text, surfaces, borders,
|
|
95
|
-
spacing, typography, radius, shadows, transitions, and z-index.
|
|
96
|
-
|
|
97
|
-
When the app domain needs colors beyond the platform set, define app-prefixed
|
|
98
|
-
variables and keep them synchronized with the shell theme through
|
|
99
|
-
`data-platform-theme`.
|
|
100
|
-
|
|
101
|
-
Good use cases for app-owned colors:
|
|
102
|
-
|
|
103
|
-
- chart series;
|
|
104
|
-
- calendar categories;
|
|
105
|
-
- tags and labels;
|
|
106
|
-
- syntax or document annotations;
|
|
107
|
-
- domain-specific status colors not covered by platform semantic colors.
|
|
108
|
-
|
|
109
|
-
Pattern:
|
|
110
|
-
|
|
111
|
-
```ts
|
|
112
|
-
const Root = styled.div`
|
|
113
|
-
--myapp-chart-blue: #3b73d9;
|
|
114
|
-
--myapp-chart-blue-muted: color-mix(
|
|
115
|
-
in srgb,
|
|
116
|
-
var(--myapp-chart-blue) 12%,
|
|
117
|
-
var(--platform-colors-surface)
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
[data-platform-theme='dark'] & {
|
|
121
|
-
--myapp-chart-blue: #8ab4ff;
|
|
122
|
-
--myapp-chart-blue-muted: color-mix(
|
|
123
|
-
in srgb,
|
|
124
|
-
var(--myapp-chart-blue) 18%,
|
|
125
|
-
var(--platform-colors-surface)
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
`
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
Rules for app-owned colors:
|
|
132
|
-
|
|
133
|
-
- prefix app variables with the app slug or a short app namespace;
|
|
134
|
-
- provide dark-mode values inside `[data-platform-theme='dark'] &`;
|
|
135
|
-
- derive muted fills, hovers, selections, and outlines with `color-mix()` and
|
|
136
|
-
platform surface, text, border, or background variables;
|
|
137
|
-
- use `--platform-colors-accent` for primary app identity and actions;
|
|
138
|
-
- reserve app-owned colors for domain meaning.
|
|
1
|
+
# Theme CSS Variables
|
|
2
|
+
|
|
3
|
+
This file is generated from `packages/ui/src/theme/tokens.ts`.
|
|
4
|
+
|
|
5
|
+
PureDesktop injects these variables through `AppFrame` and the shell bridge
|
|
6
|
+
theme payload. Use them in app styling instead of hardcoded platform colors,
|
|
7
|
+
spacing, typography, radii, shadows, transitions, or app stacking values.
|
|
8
|
+
|
|
9
|
+
This static reference is generated by PureDesktop maintainers before the
|
|
10
|
+
create-app package is released.
|
|
11
|
+
|
|
12
|
+
| Variable | Domain | Light value | Dark value |
|
|
13
|
+
| --- | --- | --- | --- |
|
|
14
|
+
| `--platform-colors-accent` | colors | `#1b1b1e` | `#e5e5e9` |
|
|
15
|
+
| `--platform-colors-accent-hover` | colors | `#1b1b1e` | `#eaeaee` |
|
|
16
|
+
| `--platform-colors-accent-muted` | colors | `#eaeaee` | `#313135` |
|
|
17
|
+
| `--platform-colors-app-viewport` | colors | `#fbfbfc` | `#202024` |
|
|
18
|
+
| `--platform-colors-bg` | colors | `#fbfbfc` | `#202024` |
|
|
19
|
+
| `--platform-colors-border` | colors | `#e5e5e9` | `#313135` |
|
|
20
|
+
| `--platform-colors-border-strong` | colors | `#dadade` | `#43434a` |
|
|
21
|
+
| `--platform-colors-chrome-tab-strip` | colors | `#ececef` | `#1b1b1e` |
|
|
22
|
+
| `--platform-colors-chrome-titlebar` | colors | `#fbfbfc` | `#202024` |
|
|
23
|
+
| `--platform-colors-danger` | colors | `#b23a2e` | `#e0796e` |
|
|
24
|
+
| `--platform-colors-divider` | colors | `#e5e5e9` | `#313135` |
|
|
25
|
+
| `--platform-colors-elevated` | colors | `#ffffff` | `#26262a` |
|
|
26
|
+
| `--platform-colors-focus` | colors | `#1b1b1e` | `#e5e5e9` |
|
|
27
|
+
| `--platform-colors-info` | colors | `#3b6fb0` | `#6fa0e0` |
|
|
28
|
+
| `--platform-colors-selection` | colors | `#eaeaee` | `#313135` |
|
|
29
|
+
| `--platform-colors-semantic-blue` | colors | `#3b6fb0` | `#6fa0e0` |
|
|
30
|
+
| `--platform-colors-semantic-blue-border` | colors | `#b9cbe4` | `#365373` |
|
|
31
|
+
| `--platform-colors-semantic-blue-muted` | colors | `#e8f0fb` | `#1c2a3c` |
|
|
32
|
+
| `--platform-colors-semantic-blue-text` | colors | `#244f87` | `#a8c8f0` |
|
|
33
|
+
| `--platform-colors-semantic-green` | colors | `#1f8a55` | `#57b988` |
|
|
34
|
+
| `--platform-colors-semantic-green-border` | colors | `#1f8a55` | `#32684d` |
|
|
35
|
+
| `--platform-colors-semantic-green-muted` | colors | `#eef7f1` | `#18342a` |
|
|
36
|
+
| `--platform-colors-semantic-green-text` | colors | `#16683f` | `#8fe0b6` |
|
|
37
|
+
| `--platform-colors-semantic-orange` | colors | `#c98a2b` | `#e0b257` |
|
|
38
|
+
| `--platform-colors-semantic-orange-border` | colors | `#e3c98f` | `#6d5b2f` |
|
|
39
|
+
| `--platform-colors-semantic-orange-muted` | colors | `#f3e5c8` | `#3a3322` |
|
|
40
|
+
| `--platform-colors-semantic-orange-text` | colors | `#7a5114` | `#f0ce8a` |
|
|
41
|
+
| `--platform-colors-semantic-red` | colors | `#b23a2e` | `#e0796e` |
|
|
42
|
+
| `--platform-colors-semantic-red-border` | colors | `#dfb7b1` | `#753f38` |
|
|
43
|
+
| `--platform-colors-semantic-red-muted` | colors | `#f4dfdc` | `#3a211d` |
|
|
44
|
+
| `--platform-colors-semantic-red-text` | colors | `#8a2d24` | `#f0a89e` |
|
|
45
|
+
| `--platform-colors-success` | colors | `#1f8a55` | `#57b988` |
|
|
46
|
+
| `--platform-colors-surface` | colors | `#f8f8fa` | `#26262a` |
|
|
47
|
+
| `--platform-colors-surface-active` | colors | `#eaeaee` | `#161619` |
|
|
48
|
+
| `--platform-colors-surface-hover` | colors | `#eaeaee` | `#2c2c30` |
|
|
49
|
+
| `--platform-colors-text` | colors | `#1b1b1e` | `#ececef` |
|
|
50
|
+
| `--platform-colors-text-danger` | colors | `#b23a2e` | `#e0796e` |
|
|
51
|
+
| `--platform-colors-text-disabled` | colors | `#9a9aa0` | `#6e6e74` |
|
|
52
|
+
| `--platform-colors-text-inverse` | colors | `#ffffff` | `#1b1b1e` |
|
|
53
|
+
| `--platform-colors-text-secondary` | colors | `#3a3a3e` | `#c9c9cf` |
|
|
54
|
+
| `--platform-colors-warning` | colors | `#c98a2b` | `#e0b257` |
|
|
55
|
+
| `--platform-radius-full` | radius | `999px` | `999px` |
|
|
56
|
+
| `--platform-radius-lg` | radius | `0px` | `0px` |
|
|
57
|
+
| `--platform-radius-md` | radius | `0px` | `0px` |
|
|
58
|
+
| `--platform-radius-sm` | radius | `0px` | `0px` |
|
|
59
|
+
| `--platform-shadow-lg` | shadow | `0 18px 48px rgb(18 18 22 / 0.13)` | `0 18px 44px rgba(0, 0, 0, 0.38)` |
|
|
60
|
+
| `--platform-shadow-md` | shadow | `0 8px 24px rgb(18 18 22 / 0.08)` | `0 8px 24px rgba(0, 0, 0, 0.3)` |
|
|
61
|
+
| `--platform-shadow-sm` | shadow | `none` | `none` |
|
|
62
|
+
| `--platform-spacing-lg` | spacing | `16px` | `16px` |
|
|
63
|
+
| `--platform-spacing-md` | spacing | `12px` | `12px` |
|
|
64
|
+
| `--platform-spacing-sm` | spacing | `8px` | `8px` |
|
|
65
|
+
| `--platform-spacing-xl` | spacing | `24px` | `24px` |
|
|
66
|
+
| `--platform-spacing-xs` | spacing | `4px` | `4px` |
|
|
67
|
+
| `--platform-spacing-xxl` | spacing | `32px` | `32px` |
|
|
68
|
+
| `--platform-transition-base` | transition | `200ms ease` | `200ms ease` |
|
|
69
|
+
| `--platform-transition-fast` | transition | `120ms ease` | `120ms ease` |
|
|
70
|
+
| `--platform-transition-slow` | transition | `320ms ease` | `320ms ease` |
|
|
71
|
+
| `--platform-typography-font-family` | typography | `"Archivo", system-ui, -apple-system, "Segoe UI", sans-serif` | `"Archivo", system-ui, -apple-system, "Segoe UI", sans-serif` |
|
|
72
|
+
| `--platform-typography-font-family-content` | typography | `'Source Serif 4', Georgia, serif` | `'Source Serif 4', Georgia, serif` |
|
|
73
|
+
| `--platform-typography-font-family-mono` | typography | `'JetBrains Mono', ui-monospace, monospace` | `'JetBrains Mono', ui-monospace, monospace` |
|
|
74
|
+
| `--platform-typography-font-size-base` | typography | `14.5px` | `14.5px` |
|
|
75
|
+
| `--platform-typography-font-size-lg` | typography | `17px` | `17px` |
|
|
76
|
+
| `--platform-typography-font-size-sm` | typography | `13px` | `13px` |
|
|
77
|
+
| `--platform-typography-font-size-xl` | typography | `24px` | `24px` |
|
|
78
|
+
| `--platform-typography-font-size-xs` | typography | `11px` | `11px` |
|
|
79
|
+
| `--platform-typography-font-weight-bold` | typography | `600` | `600` |
|
|
80
|
+
| `--platform-typography-font-weight-medium` | typography | `500` | `500` |
|
|
81
|
+
| `--platform-typography-font-weight-normal` | typography | `400` | `400` |
|
|
82
|
+
| `--platform-typography-line-height-base` | typography | `1.6` | `1.6` |
|
|
83
|
+
| `--platform-typography-line-height-tight` | typography | `1.25` | `1.25` |
|
|
84
|
+
| `--platform-z-index-zi-app-base` | zIndex | `0` | `0` |
|
|
85
|
+
| `--platform-z-index-zi-app-chrome` | zIndex | `20` | `20` |
|
|
86
|
+
| `--platform-z-index-zi-app-menus` | zIndex | `100` | `100` |
|
|
87
|
+
| `--platform-z-index-zi-app-modal` | zIndex | `300` | `300` |
|
|
88
|
+
| `--platform-z-index-zi-app-sidebar` | zIndex | `10` | `10` |
|
|
89
|
+
| `--platform-z-index-zi-app-toast` | zIndex | `200` | `200` |
|
|
90
|
+
| `--platform-z-index-zi-app-viewport-toolbar` | zIndex | `25` | `25` |
|
|
91
|
+
|
|
92
|
+
## App-Owned Colors
|
|
93
|
+
|
|
94
|
+
Use platform variables for the base app shell, text, surfaces, borders,
|
|
95
|
+
spacing, typography, radius, shadows, transitions, and z-index.
|
|
96
|
+
|
|
97
|
+
When the app domain needs colors beyond the platform set, define app-prefixed
|
|
98
|
+
variables and keep them synchronized with the shell theme through
|
|
99
|
+
`data-platform-theme`.
|
|
100
|
+
|
|
101
|
+
Good use cases for app-owned colors:
|
|
102
|
+
|
|
103
|
+
- chart series;
|
|
104
|
+
- calendar categories;
|
|
105
|
+
- tags and labels;
|
|
106
|
+
- syntax or document annotations;
|
|
107
|
+
- domain-specific status colors not covered by platform semantic colors.
|
|
108
|
+
|
|
109
|
+
Pattern:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const Root = styled.div`
|
|
113
|
+
--myapp-chart-blue: #3b73d9;
|
|
114
|
+
--myapp-chart-blue-muted: color-mix(
|
|
115
|
+
in srgb,
|
|
116
|
+
var(--myapp-chart-blue) 12%,
|
|
117
|
+
var(--platform-colors-surface)
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
[data-platform-theme='dark'] & {
|
|
121
|
+
--myapp-chart-blue: #8ab4ff;
|
|
122
|
+
--myapp-chart-blue-muted: color-mix(
|
|
123
|
+
in srgb,
|
|
124
|
+
var(--myapp-chart-blue) 18%,
|
|
125
|
+
var(--platform-colors-surface)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
`
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Rules for app-owned colors:
|
|
132
|
+
|
|
133
|
+
- prefix app variables with the app slug or a short app namespace;
|
|
134
|
+
- provide dark-mode values inside `[data-platform-theme='dark'] &`;
|
|
135
|
+
- derive muted fills, hovers, selections, and outlines with `color-mix()` and
|
|
136
|
+
platform surface, text, border, or background variables;
|
|
137
|
+
- use `--platform-colors-accent` for primary app identity and actions;
|
|
138
|
+
- reserve app-owned colors for domain meaning.
|
|
@@ -7,6 +7,9 @@ App agent tools have two halves:
|
|
|
7
7
|
|
|
8
8
|
The names must match exactly.
|
|
9
9
|
|
|
10
|
+
For the exact edit checklist when adding a new tool, read
|
|
11
|
+
[Adding an app agent tool](./adding-agent-tool.md).
|
|
12
|
+
|
|
10
13
|
## Generated Structure
|
|
11
14
|
|
|
12
15
|
When the manifest includes `app.agents.tools`, the generator creates this
|
|
@@ -1,35 +1,56 @@
|
|
|
1
|
-
# UI And Components
|
|
2
|
-
|
|
3
|
-
The generated app starts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
+
|
|
31
52
|
Import shared UI by exact exported package paths before creating custom
|
|
32
|
-
primitives.
|
|
53
|
+
primitives. Use existing app folders before creating new ones.
|
|
33
54
|
|
|
34
55
|
Useful starting points:
|
|
35
56
|
|
|
@@ -38,23 +59,26 @@ Useful starting points:
|
|
|
38
59
|
- `Heading` from `@puredesktop/puredesktop-ui-bridge/components/common/typography/Heading`
|
|
39
60
|
- `Text` from `@puredesktop/puredesktop-ui-bridge/components/common/typography/Text`
|
|
40
61
|
- `Button` from `@puredesktop/puredesktop-ui-bridge/components/common/buttons/Button`
|
|
41
|
-
|
|
42
|
-
## Styling
|
|
43
|
-
|
|
44
|
-
Use styled-components and platform CSS variables:
|
|
45
|
-
|
|
46
|
-
```ts
|
|
47
|
-
const Root = styled.div`
|
|
48
|
-
display: flex;
|
|
49
|
-
flex-direction: column;
|
|
50
|
-
gap: var(--platform-spacing-md);
|
|
51
|
-
color: var(--platform-colors-text);
|
|
52
|
-
background: var(--platform-colors-bg);
|
|
53
|
-
`
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Use app-local CSS variables only for app-specific meaning, for example
|
|
57
|
-
`--{{APP_SLUG}}-accent`.
|
|
58
|
-
|
|
59
|
-
See [Theme CSS variables](./theme-vars.md) for the generated list of available
|
|
60
|
-
platform variables by domain.
|
|
62
|
+
|
|
63
|
+
## Styling
|
|
64
|
+
|
|
65
|
+
Use styled-components and platform CSS variables:
|
|
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`.
|
|
79
|
+
|
|
80
|
+
See [Theme CSS variables](./theme-vars.md) for the generated list of available
|
|
81
|
+
platform variables by domain.
|
|
82
|
+
|
|
83
|
+
Avoid inline styles except for tiny dynamic values that cannot be represented as
|
|
84
|
+
props on a styled component.
|
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.8",
|
|
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", "agents"],
|
|
5
|
+
"permissions": ["network", "settings", "filesystem", "agents"],
|
|
6
6
|
"entrypoint": {
|
|
7
7
|
"kind": "dev-url",
|
|
8
8
|
"url": "http://localhost:{{APP_PORT}}"
|
|
@@ -20,9 +20,10 @@ if (packageJson) {
|
|
|
20
20
|
validatePackageJson(packageJson)
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
if (manifest) {
|
|
24
|
-
validateManifest(manifest)
|
|
25
|
-
|
|
23
|
+
if (manifest) {
|
|
24
|
+
validateManifest(manifest)
|
|
25
|
+
await validateBridgePermissionUsage(manifest)
|
|
26
|
+
}
|
|
26
27
|
|
|
27
28
|
if (await isDirectory(distPath)) {
|
|
28
29
|
info.push('dist folder is present for static registration.')
|
|
@@ -135,7 +136,7 @@ function validateManifest(manifest) {
|
|
|
135
136
|
if (errors.length === 0) info.push('plugin.json is valid.')
|
|
136
137
|
}
|
|
137
138
|
|
|
138
|
-
function validateEntrypoint(entrypoint, permissions) {
|
|
139
|
+
function validateEntrypoint(entrypoint, permissions) {
|
|
139
140
|
if (!isPlainObject(entrypoint)) {
|
|
140
141
|
errors.push('entrypoint is required.')
|
|
141
142
|
return
|
|
@@ -169,10 +170,47 @@ function validateEntrypoint(entrypoint, permissions) {
|
|
|
169
170
|
}
|
|
170
171
|
return
|
|
171
172
|
}
|
|
172
|
-
errors.push(`Unsupported entrypoint kind: ${String(entrypoint.kind ?? '') || '(missing)'}.`)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async function
|
|
173
|
+
errors.push(`Unsupported entrypoint kind: ${String(entrypoint.kind ?? '') || '(missing)'}.`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function validateBridgePermissionUsage(manifest) {
|
|
177
|
+
const permissions = Array.isArray(manifest.permissions) ? manifest.permissions : []
|
|
178
|
+
const sourceText = await readProjectText(join(ROOT, 'src'))
|
|
179
|
+
const requirements = [
|
|
180
|
+
{
|
|
181
|
+
permission: 'filesystem',
|
|
182
|
+
hints: [
|
|
183
|
+
'/bridge/storage',
|
|
184
|
+
'/bridge/fs',
|
|
185
|
+
'/bridge/dialog',
|
|
186
|
+
'/bridge/react/usePlatformJsonStore',
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
permission: 'settings',
|
|
191
|
+
hints: [
|
|
192
|
+
'/bridge/appSettings',
|
|
193
|
+
'/bridge/preferences',
|
|
194
|
+
'/bridge/react/usePlatformAppSettings',
|
|
195
|
+
'/bridge/react/usePlatformPreferences',
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
permission: 'network',
|
|
200
|
+
hints: ['/bridge/network', '/bridge/vision'],
|
|
201
|
+
},
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
for (const requirement of requirements) {
|
|
205
|
+
if (permissions.includes(requirement.permission)) continue
|
|
206
|
+
if (!requirement.hints.some(hint => sourceText.includes(hint))) continue
|
|
207
|
+
errors.push(
|
|
208
|
+
`Source imports ${requirement.permission}-gated bridge helpers, so plugin.json must include the ${requirement.permission} permission.`,
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function validateAgentTools(manifest, distPath) {
|
|
176
214
|
const tools = manifest.app?.agents?.tools ?? []
|
|
177
215
|
if (!Array.isArray(tools) || tools.length === 0) {
|
|
178
216
|
info.push('No app agent tools declared.')
|
package/template/src/App.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { AppFrame } from '@puredesktop/puredesktop-ui-bridge/components/common/containers/AppFrame'
|
|
2
2
|
import { EmptyState } from '@puredesktop/puredesktop-ui-bridge/components/common/feedback/EmptyState'
|
|
3
3
|
import { usePlatformBridge } from '@puredesktop/puredesktop-ui-bridge/bridge/react/usePlatformBridge'
|
|
4
|
-
import { AppShell } from './components/AppShell'
|
|
5
|
-
import { isStandaloneDevMode } from './bridge/platformBridge'
|
|
6
|
-
import {
|
|
4
|
+
import { AppShell } from './components/AppShell'
|
|
5
|
+
import { isStandaloneDevMode } from './bridge/platformBridge'
|
|
6
|
+
import { APP_TITLE } from './constants'
|
|
7
|
+
import { useAppBoot } from './hooks/useAppBoot'
|
|
7
8
|
|
|
8
9
|
export function App(): React.ReactElement {
|
|
9
10
|
const { error: bridgeError, ready } = usePlatformBridge()
|
|
@@ -28,7 +29,7 @@ export function App(): React.ReactElement {
|
|
|
28
29
|
<AppFrame>
|
|
29
30
|
<EmptyState
|
|
30
31
|
tone={bootError ? 'error' : 'neutral'}
|
|
31
|
-
title={bootError ? 'Boot failed' :
|
|
32
|
+
title={bootError ? 'Boot failed' : APP_TITLE}
|
|
32
33
|
message={
|
|
33
34
|
bootError
|
|
34
35
|
? bootError.message
|
|
@@ -1,33 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
`
|
|
13
|
-
|
|
14
|
-
export interface AppShellProps {
|
|
15
|
-
settings: AppSettings
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function AppShell({ settings }: AppShellProps): React.ReactElement {
|
|
19
|
-
return (
|
|
20
|
-
<Root>
|
|
21
|
-
<Heading level={1}>{{APP_TITLE}}</Heading>
|
|
22
|
-
<Text>
|
|
23
|
-
Start building in <code>src/components/AppShell.tsx</code>. Bridge boot
|
|
24
|
-
is wired — AppFrame, settings load, and standalone browser dev all work.
|
|
25
|
-
</Text>
|
|
26
|
-
{Object.keys(settings).length > 0 ? (
|
|
27
|
-
<Text size="sm" tone="secondary">
|
|
28
|
-
Loaded {Object.keys(settings).length} setting key(s).
|
|
29
|
-
</Text>
|
|
30
|
-
) : null}
|
|
31
|
-
</Root>
|
|
32
|
-
)
|
|
33
|
-
}
|
|
1
|
+
import type { AppSettings } from '../types'
|
|
2
|
+
import { APP_TITLE } from '../constants'
|
|
3
|
+
import { StarterWorkspace } from './StarterWorkspace'
|
|
4
|
+
|
|
5
|
+
export interface AppShellProps {
|
|
6
|
+
settings: AppSettings
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function AppShell({ settings }: AppShellProps): React.ReactElement {
|
|
10
|
+
return <StarterWorkspace appName={APP_TITLE} settings={settings} />
|
|
11
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import styled from 'styled-components'
|
|
2
|
+
import { Heading } from '@puredesktop/puredesktop-ui-bridge/components/common/typography/Heading'
|
|
3
|
+
import { Text } from '@puredesktop/puredesktop-ui-bridge/components/common/typography/Text'
|
|
4
|
+
import {
|
|
5
|
+
readConfiguredSettingCount,
|
|
6
|
+
starterMilestones,
|
|
7
|
+
} from '../lib/starterWorkspace'
|
|
8
|
+
import type { AppSettings } from '../types'
|
|
9
|
+
|
|
10
|
+
const Root = styled.div`
|
|
11
|
+
display: flex;
|
|
12
|
+
min-height: 100%;
|
|
13
|
+
width: 100%;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
padding: var(--platform-spacing-xxl);
|
|
17
|
+
color: var(--platform-colors-text);
|
|
18
|
+
background:
|
|
19
|
+
linear-gradient(
|
|
20
|
+
135deg,
|
|
21
|
+
color-mix(in srgb, var(--platform-colors-accent) 6%, transparent),
|
|
22
|
+
transparent 42%
|
|
23
|
+
),
|
|
24
|
+
var(--platform-colors-bg);
|
|
25
|
+
`
|
|
26
|
+
|
|
27
|
+
const Panel = styled.section`
|
|
28
|
+
display: grid;
|
|
29
|
+
width: min(760px, 100%);
|
|
30
|
+
gap: var(--platform-spacing-xl);
|
|
31
|
+
`
|
|
32
|
+
|
|
33
|
+
const Header = styled.header`
|
|
34
|
+
display: grid;
|
|
35
|
+
gap: var(--platform-spacing-sm);
|
|
36
|
+
`
|
|
37
|
+
|
|
38
|
+
const MilestoneGrid = styled.div`
|
|
39
|
+
display: grid;
|
|
40
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
41
|
+
gap: var(--platform-spacing-md);
|
|
42
|
+
|
|
43
|
+
@media (max-width: 720px) {
|
|
44
|
+
grid-template-columns: 1fr;
|
|
45
|
+
}
|
|
46
|
+
`
|
|
47
|
+
|
|
48
|
+
const Milestone = styled.article`
|
|
49
|
+
display: grid;
|
|
50
|
+
gap: var(--platform-spacing-sm);
|
|
51
|
+
min-width: 0;
|
|
52
|
+
padding: var(--platform-spacing-lg);
|
|
53
|
+
border: 1px solid var(--platform-colors-border);
|
|
54
|
+
background: var(--platform-colors-surface);
|
|
55
|
+
box-shadow: var(--platform-shadow-sm);
|
|
56
|
+
`
|
|
57
|
+
|
|
58
|
+
const PulseTrack = styled.div`
|
|
59
|
+
position: relative;
|
|
60
|
+
overflow: hidden;
|
|
61
|
+
height: 3px;
|
|
62
|
+
background: var(--platform-colors-border);
|
|
63
|
+
`
|
|
64
|
+
|
|
65
|
+
const Pulse = styled.div`
|
|
66
|
+
position: absolute;
|
|
67
|
+
inset: 0 auto 0 0;
|
|
68
|
+
width: 44%;
|
|
69
|
+
background: var(--platform-colors-accent);
|
|
70
|
+
animation: starter-pulse 1600ms ease-in-out infinite;
|
|
71
|
+
|
|
72
|
+
@keyframes starter-pulse {
|
|
73
|
+
0% {
|
|
74
|
+
transform: translateX(-100%);
|
|
75
|
+
opacity: 0.35;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
45%,
|
|
79
|
+
60% {
|
|
80
|
+
opacity: 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
100% {
|
|
84
|
+
transform: translateX(230%);
|
|
85
|
+
opacity: 0.35;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
`
|
|
89
|
+
|
|
90
|
+
const MetaRow = styled.div`
|
|
91
|
+
display: flex;
|
|
92
|
+
flex-wrap: wrap;
|
|
93
|
+
gap: var(--platform-spacing-sm);
|
|
94
|
+
`
|
|
95
|
+
|
|
96
|
+
const MetaPill = styled.span`
|
|
97
|
+
padding: 4px 8px;
|
|
98
|
+
border: 1px solid var(--platform-colors-border);
|
|
99
|
+
background: var(--platform-colors-surface);
|
|
100
|
+
color: var(--platform-colors-text-secondary);
|
|
101
|
+
font-size: var(--platform-typography-font-size-xs);
|
|
102
|
+
font-weight: var(--platform-typography-font-weight-medium);
|
|
103
|
+
`
|
|
104
|
+
|
|
105
|
+
interface StarterWorkspaceProps {
|
|
106
|
+
appName: string
|
|
107
|
+
settings: AppSettings
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function StarterWorkspace({
|
|
111
|
+
appName,
|
|
112
|
+
settings,
|
|
113
|
+
}: StarterWorkspaceProps): React.ReactElement {
|
|
114
|
+
const settingCount = readConfiguredSettingCount(settings)
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<Root>
|
|
118
|
+
<Panel aria-label={`${appName} starter workspace`}>
|
|
119
|
+
<Header>
|
|
120
|
+
<Text size="sm" tone="secondary">
|
|
121
|
+
PureDesktop app workspace
|
|
122
|
+
</Text>
|
|
123
|
+
<Heading level={1}>{appName}</Heading>
|
|
124
|
+
<Text tone="secondary">
|
|
125
|
+
This workspace is ready. The first build step will replace this
|
|
126
|
+
starter screen with the product experience.
|
|
127
|
+
</Text>
|
|
128
|
+
</Header>
|
|
129
|
+
|
|
130
|
+
<MilestoneGrid>
|
|
131
|
+
{starterMilestones.map(milestone => (
|
|
132
|
+
<Milestone key={milestone.id}>
|
|
133
|
+
<Text size="sm" weight="medium">
|
|
134
|
+
{milestone.title}
|
|
135
|
+
</Text>
|
|
136
|
+
<Text size="sm" tone="secondary">
|
|
137
|
+
{milestone.detail}
|
|
138
|
+
</Text>
|
|
139
|
+
<PulseTrack aria-hidden="true">
|
|
140
|
+
<Pulse />
|
|
141
|
+
</PulseTrack>
|
|
142
|
+
</Milestone>
|
|
143
|
+
))}
|
|
144
|
+
</MilestoneGrid>
|
|
145
|
+
|
|
146
|
+
<MetaRow>
|
|
147
|
+
<MetaPill>{settingCount} setting key(s)</MetaPill>
|
|
148
|
+
<MetaPill>Theme active</MetaPill>
|
|
149
|
+
<MetaPill>Shell connected</MetaPill>
|
|
150
|
+
</MetaRow>
|
|
151
|
+
</Panel>
|
|
152
|
+
</Root>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export const APP_SLUG = '{{APP_SLUG}}'
|
|
1
|
+
export const APP_SLUG = '{{APP_SLUG}}'
|
|
2
|
+
export const APP_TITLE = '{{APP_TITLE}}'
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AppSettings } from '../types'
|
|
2
|
+
|
|
3
|
+
export interface StarterMilestone {
|
|
4
|
+
detail: string
|
|
5
|
+
id: string
|
|
6
|
+
title: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const starterMilestones: StarterMilestone[] = [
|
|
10
|
+
{
|
|
11
|
+
detail: 'The starter screen is temporary and ready to be replaced by product UI.',
|
|
12
|
+
id: 'boot',
|
|
13
|
+
title: 'Workspace ready',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
detail: 'PureDesktop styling is loaded so early screens match the shell.',
|
|
17
|
+
id: 'composition',
|
|
18
|
+
title: 'Theme active',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
detail: 'Settings and shell capabilities are available through the app bridge.',
|
|
22
|
+
id: 'domain',
|
|
23
|
+
title: 'Shell connected',
|
|
24
|
+
},
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
export function readConfiguredSettingCount(settings: AppSettings): number {
|
|
28
|
+
return Object.keys(settings).length
|
|
29
|
+
}
|
package/template/vite.config.js
CHANGED
|
@@ -16,13 +16,16 @@ function devServerFromManifest(metaUrl) {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export default defineConfig({
|
|
20
|
-
plugins: [
|
|
21
|
-
react({
|
|
22
|
-
plugins: [
|
|
23
|
-
['@swc/plugin-styled-components', { displayName: true, fileName: true }],
|
|
24
|
-
],
|
|
25
|
-
}),
|
|
26
|
-
],
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
export default defineConfig({
|
|
20
|
+
plugins: [
|
|
21
|
+
react({
|
|
22
|
+
plugins: [
|
|
23
|
+
['@swc/plugin-styled-components', { displayName: true, fileName: true }],
|
|
24
|
+
],
|
|
25
|
+
}),
|
|
26
|
+
],
|
|
27
|
+
optimizeDeps: {
|
|
28
|
+
include: ['react-dom'],
|
|
29
|
+
},
|
|
30
|
+
server: devServerFromManifest(import.meta.url),
|
|
31
|
+
})
|