@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 CHANGED
@@ -1,11 +1,188 @@
1
- # Stonecrop Desktop
1
+ # @stonecrop/desktop
2
2
 
3
- A collection of components for use in a browser/desktop UI
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
- - Spreadheet/ tabbed navigation component
6
- - Command Palette with search
5
+ ## Features
7
6
 
8
- ## To do
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
- - Think about a grayscale filter for logos and background UIs
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
  /**