@stonecrop/desktop 0.10.1 → 0.10.3
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/README.md +184 -7
- package/dist/desktop.d.ts +49 -0
- package/dist/desktop.js +1615 -1622
- package/dist/desktop.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/types/index.d.ts +45 -0
- package/dist/src/types/index.d.ts.map +1 -1
- package/package.json +15 -6
- package/src/components/Desktop.vue +157 -251
- package/src/index.ts +1 -0
- package/src/types/index.ts +49 -0
package/README.md
CHANGED
|
@@ -1,11 +1,188 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @stonecrop/desktop
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A three-view UI shell for Stonecrop applications. Renders a doctype list → records list → record form layout driven entirely by the host application's Registry and HST state. Desktop owns no data lifecycle — it emits events and the host app decides what to do.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- Command Palette with search
|
|
5
|
+
## Features
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
- **Three-view layout**: doctypes → records → record form, navigated by route or adapter
|
|
8
|
+
- **ActionSet toolbar**: FSM transitions become action buttons/dropdowns automatically from the doctype workflow
|
|
9
|
+
- **CommandPalette**: `Ctrl+K` / `Cmd+K` search across doctypes and records
|
|
10
|
+
- **SheetNav**: tabbed navigation between open records
|
|
11
|
+
- **Event-driven**: all significant interactions emit typed events for the host to respond to
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @stonecrop/desktop
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Desktop requires `@stonecrop/stonecrop` to be installed and the `StonecropPlugin` mounted before use:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { createApp } from 'vue'
|
|
23
|
+
import Stonecrop from '@stonecrop/stonecrop'
|
|
24
|
+
import { registry } from './registry'
|
|
25
|
+
|
|
26
|
+
createApp(App).use(Stonecrop, { registry }).mount('#app')
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Basic Usage
|
|
30
|
+
|
|
31
|
+
```vue
|
|
32
|
+
<script setup lang="ts">
|
|
33
|
+
import { Desktop } from '@stonecrop/desktop'
|
|
34
|
+
import type { ActionEventPayload } from '@stonecrop/desktop'
|
|
35
|
+
import { useStonecrop } from '@stonecrop/stonecrop'
|
|
36
|
+
|
|
37
|
+
const { stonecrop } = useStonecrop()
|
|
38
|
+
|
|
39
|
+
async function handleAction(payload: ActionEventPayload) {
|
|
40
|
+
// Call your server, trigger FSM transitions, update HST...
|
|
41
|
+
const node = stonecrop.value?.getRecordById(payload.doctype, payload.recordId)
|
|
42
|
+
await node?.triggerTransition(payload.name, { fsmContext: payload.data })
|
|
43
|
+
}
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<template>
|
|
47
|
+
<Desktop
|
|
48
|
+
:available-doctypes="['plan', 'recipe', 'resource']"
|
|
49
|
+
@action="handleAction"
|
|
50
|
+
/>
|
|
51
|
+
</template>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Props
|
|
55
|
+
|
|
56
|
+
| Prop | Type | Default | Description |
|
|
57
|
+
|------|------|---------|-------------|
|
|
58
|
+
| `availableDoctypes` | `string[]` | `[]` | Doctype slugs to display in the doctypes list |
|
|
59
|
+
| `routeAdapter` | `RouteAdapter` | — | Custom routing layer (required for Nuxt/custom hosts) |
|
|
60
|
+
| `confirmFn` | `(msg: string) => boolean \| Promise<boolean>` | `window.confirm` | Replacement for the native browser confirm dialog |
|
|
61
|
+
|
|
62
|
+
## Emitted Events
|
|
63
|
+
|
|
64
|
+
| Event | Payload | When |
|
|
65
|
+
|-------|---------|------|
|
|
66
|
+
| `action` | `ActionEventPayload` | User triggers an FSM transition or DELETE |
|
|
67
|
+
| `navigate` | `NavigationTarget` | Desktop wants to change views |
|
|
68
|
+
| `record:open` | `RecordOpenEventPayload` | User opens a specific record |
|
|
69
|
+
|
|
70
|
+
### `action` payload
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
type ActionEventPayload = {
|
|
74
|
+
name: string // FSM transition name e.g. 'SUBMIT', 'APPROVE', 'DELETE'
|
|
75
|
+
doctype: string
|
|
76
|
+
recordId: string
|
|
77
|
+
data: Record<string, any> // Form field snapshot at trigger time
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Desktop reads the available transitions for the current record directly from the doctype workflow (`DoctypeMeta.getAvailableTransitions`) using `Stonecrop.getRecordState` to resolve the current FSM state (reads the `status` field, falls back to `workflow.initial`). **Desktop never calls `triggerTransition` itself** — that is the host application's responsibility.
|
|
82
|
+
|
|
83
|
+
### `navigate` payload
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
type NavigationTarget = {
|
|
87
|
+
view: 'doctypes' | 'records' | 'record'
|
|
88
|
+
doctype?: string
|
|
89
|
+
recordId?: string
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Fired on every internal navigation. If a `routeAdapter` is provided it is also called. If not, Desktop falls back to `registry.router` (Vue Router) with paths `'/'`, `'/:doctype'`, `'/:doctype/:recordId'`.
|
|
94
|
+
|
|
95
|
+
## Router Adapter
|
|
96
|
+
|
|
97
|
+
For Nuxt apps (or any host with custom route conventions), supply a `routeAdapter` instead of relying on the registry's Vue Router:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { useRoute, useRouter } from '#app'
|
|
101
|
+
import type { RouteAdapter, NavigationTarget } from '@stonecrop/desktop'
|
|
102
|
+
|
|
103
|
+
function useFabRouteAdapter(): RouteAdapter {
|
|
104
|
+
const route = useRoute()
|
|
105
|
+
const router = useRouter()
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
getCurrentDoctype: () => route.meta.slug as string ?? '',
|
|
109
|
+
getCurrentRecordId: () => route.params.id as string ?? '',
|
|
110
|
+
getCurrentView: () => {
|
|
111
|
+
if (!route.meta.slug) return 'doctypes'
|
|
112
|
+
if (!route.params.id) return 'records'
|
|
113
|
+
return 'record'
|
|
114
|
+
},
|
|
115
|
+
navigate: (target: NavigationTarget) => {
|
|
116
|
+
if (target.view === 'doctypes') return router.push('/')
|
|
117
|
+
if (target.view === 'records') return router.push(`/${target.doctype}`)
|
|
118
|
+
return router.push(`/${target.doctype}/${target.recordId}`)
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```vue
|
|
125
|
+
<Desktop :route-adapter="useFabRouteAdapter()" @action="handleAction" />
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Handling `action` Events
|
|
129
|
+
|
|
130
|
+
The complete host-side pattern for handling an action in a Nuxt/fab context:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import type { ActionEventPayload } from '@stonecrop/desktop'
|
|
134
|
+
import { useStonecrop } from '@stonecrop/stonecrop'
|
|
135
|
+
|
|
136
|
+
const { stonecrop } = useStonecrop()
|
|
137
|
+
|
|
138
|
+
async function handleAction(payload: ActionEventPayload) {
|
|
139
|
+
if (!stonecrop.value) return
|
|
140
|
+
|
|
141
|
+
// 1. Optionally persist field changes to HST before the transition
|
|
142
|
+
const store = stonecrop.value.getStore()
|
|
143
|
+
for (const [field, value] of Object.entries(payload.data)) {
|
|
144
|
+
const path = `${payload.doctype}.${payload.recordId}.${field}`
|
|
145
|
+
if (store.has(path) && store.get(path) !== value) {
|
|
146
|
+
store.set(path, value)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 2. Call the server (StonecropClient, $fetch, tRPC — whatever your stack uses)
|
|
151
|
+
const result = await client.runAction(payload.doctype, payload.name, {
|
|
152
|
+
id: payload.recordId,
|
|
153
|
+
data: payload.data,
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// 3. Sync the server response back into HST
|
|
157
|
+
if (result.success && result.data) {
|
|
158
|
+
stonecrop.value.addRecord(payload.doctype, payload.recordId, result.data)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Provide / Inject
|
|
164
|
+
|
|
165
|
+
Desktop provides a `desktopMethods` object that child components (slot content) can inject:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { inject } from 'vue'
|
|
169
|
+
|
|
170
|
+
const { navigateToDoctype, openRecord, createNewRecord, handleDelete, emitAction } =
|
|
171
|
+
inject('desktopMethods')!
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`emitAction(name, data?)` is a convenience wrapper for emitting an `action` event from deeply nested slot content without passing refs down manually.
|
|
175
|
+
|
|
176
|
+
## Components
|
|
177
|
+
|
|
178
|
+
### ActionSet
|
|
179
|
+
|
|
180
|
+
Renders a toolbar from an `ActionElements[]` array. Used internally by Desktop; can be used standalone.
|
|
181
|
+
|
|
182
|
+
### CommandPalette
|
|
183
|
+
|
|
184
|
+
Full-text search over registered doctypes and records. Activated with `Ctrl+K`.
|
|
185
|
+
|
|
186
|
+
### SheetNav
|
|
187
|
+
|
|
188
|
+
Tab strip for navigating between open records.
|
package/dist/desktop.d.ts
CHANGED
|
@@ -10,6 +10,19 @@ import SheetNav from './components/SheetNav.vue';
|
|
|
10
10
|
*/
|
|
11
11
|
export declare type ActionElements = ButtonElement | DropdownElement;
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Payload emitted with the 'action' event when the user triggers an FSM transition
|
|
15
|
+
* @public
|
|
16
|
+
*/
|
|
17
|
+
export declare type ActionEventPayload = {
|
|
18
|
+
/** The FSM transition name (e.g. 'SAVE', 'SUBMIT', 'APPROVE') */
|
|
19
|
+
name: string;
|
|
20
|
+
doctype: string;
|
|
21
|
+
recordId: string;
|
|
22
|
+
/** Snapshot of the form data at the time the action was triggered */
|
|
23
|
+
data: Record<string, any>;
|
|
24
|
+
};
|
|
25
|
+
|
|
13
26
|
export { ActionSet }
|
|
14
27
|
|
|
15
28
|
/**
|
|
@@ -52,6 +65,42 @@ export declare type ElementAction = BaseElement & {
|
|
|
52
65
|
action?: () => void;
|
|
53
66
|
};
|
|
54
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Navigation target passed to RouteAdapter.navigate and emitted with the 'navigate' event
|
|
70
|
+
* @public
|
|
71
|
+
*/
|
|
72
|
+
export declare type NavigationTarget = {
|
|
73
|
+
view: 'doctypes' | 'records' | 'record';
|
|
74
|
+
doctype?: string;
|
|
75
|
+
recordId?: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Payload emitted with the 'record:open' event
|
|
80
|
+
* @public
|
|
81
|
+
*/
|
|
82
|
+
export declare type RecordOpenEventPayload = {
|
|
83
|
+
doctype: string;
|
|
84
|
+
recordId: string;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Adapter that lets host applications (Nuxt, etc.) supply their own routing layer.
|
|
89
|
+
* When provided as a prop, Desktop uses these functions instead of reaching into
|
|
90
|
+
* the Vue Router instance baked into the Stonecrop registry.
|
|
91
|
+
* @public
|
|
92
|
+
*/
|
|
93
|
+
export declare type RouteAdapter = {
|
|
94
|
+
/** Returns the active doctype key (e.g. 'plan', 'recipe'). Called inside computed — should read reactive state. */
|
|
95
|
+
getCurrentDoctype: () => string;
|
|
96
|
+
/** Returns the active record ID, or '' when viewing a list. Called inside computed. */
|
|
97
|
+
getCurrentRecordId: () => string;
|
|
98
|
+
/** Returns which of the three views is currently active. Called inside computed. */
|
|
99
|
+
getCurrentView: () => 'doctypes' | 'records' | 'record';
|
|
100
|
+
/** Perform the navigation. Called after the host app has handled any side effects. */
|
|
101
|
+
navigate: (target: NavigationTarget) => void | Promise<void>;
|
|
102
|
+
};
|
|
103
|
+
|
|
55
104
|
export { SheetNav }
|
|
56
105
|
|
|
57
106
|
/**
|