@docyrus/docyrus 0.0.66 → 0.0.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/main.js +136 -136
  2. package/main.js.map +3 -3
  3. package/package.json +3 -3
  4. package/resources/pi-agent/extensions/context.ts +22 -16
  5. package/resources/pi-agent/extensions/control.ts +43 -27
  6. package/resources/pi-agent/extensions/knowledge.ts +42 -16
  7. package/resources/pi-agent/extensions/loop.ts +44 -27
  8. package/resources/pi-agent/extensions/pi-bash-live-view/widget.ts +2 -17
  9. package/resources/pi-agent/extensions/plan.ts +79 -40
  10. package/resources/pi-agent/extensions/prompt-url-widget.ts +39 -18
  11. package/resources/pi-agent/extensions/review.ts +13 -2
  12. package/resources/pi-agent/shared/extensionLifecycle.ts +11 -0
  13. package/resources/pi-agent/skills/docyrus-app-dev-react/references/component-selection-guide.md +42 -5
  14. package/resources/pi-agent/skills/docyrus-data-grid-page-design/SKILL.md +58 -0
  15. package/resources/pi-agent/skills/docyrus-data-grid-page-design/references/advanced-saved-view-query-patterns.md +158 -0
  16. package/resources/pi-agent/skills/docyrus-data-grid-page-design/references/hook-pages.md +183 -0
  17. package/resources/pi-agent/skills/docyrus-data-grid-page-design/references/manual-pages.md +145 -0
  18. package/resources/pi-agent/skills/docyrus-data-grid-page-design/references/tenant-and-users-providers.md +290 -0
  19. package/resources/pi-agent/skills/docyrus-record-detail-form-design/SKILL.md +54 -0
  20. package/resources/pi-agent/skills/docyrus-record-detail-form-design/references/advanced-inline-edit-and-renderers.md +140 -0
  21. package/resources/pi-agent/skills/docyrus-record-detail-form-design/references/field-type-mapping-and-fallbacks.md +150 -0
  22. package/resources/pi-agent/skills/docyrus-record-detail-form-design/references/hook-form-view.md +127 -0
  23. package/resources/pi-agent/skills/docyrus-record-detail-form-design/references/manual-form-detail-patterns.md +125 -0
@@ -0,0 +1,145 @@
1
+ # Manual DataGrid Page Patterns
2
+
3
+ ## Use this path when
4
+
5
+ - The page uses local/demo data.
6
+ - You need a custom toolbar layout.
7
+ - You want backend-saved views but your own row-query lifecycle.
8
+ - You only need local views and not Docyrus view persistence.
9
+
10
+ ## Local views with full manual composition
11
+
12
+ ```tsx
13
+ 'use client';
14
+
15
+ import { useEffect, useMemo, useState } from 'react';
16
+
17
+ import {
18
+ DataGrid,
19
+ DataGridFilterMenu,
20
+ DataGridGroupMenu,
21
+ DataGridRowHeightMenu,
22
+ DataGridSortMenu,
23
+ applyViewToTable,
24
+ getDataGridActionsColumn,
25
+ getDataGridSelectColumn,
26
+ useDataGrid,
27
+ type ColumnDef,
28
+ type SavedDataGridView
29
+ } from '@docyrus/ui/components/data-grid';
30
+ import { DataGridViewSelect } from '@docyrus/ui/components/data-grid-view-select';
31
+
32
+ const columns: ColumnDef<Row>[] = [
33
+ getDataGridSelectColumn<Row>(),
34
+ getDataGridActionsColumn<Row>({ cell: ({ row }) => <OpenButton row={row.original} /> }),
35
+ ...dataColumns
36
+ ];
37
+
38
+ export function TasksPage() {
39
+ const [views, setViews] = useState<SavedDataGridView[]>(INITIAL_VIEWS);
40
+ const [activeViewId, setActiveViewId] = useState('all');
41
+ const activeView = useMemo(() => views.find(view => view.id === activeViewId), [views, activeViewId]);
42
+
43
+ const { table, ...gridProps } = useDataGrid<Row>({
44
+ data: rows,
45
+ columns,
46
+ enableSearch: true,
47
+ enableGrouping: true
48
+ });
49
+
50
+ useEffect(() => {
51
+ if (!activeView) return;
52
+ applyViewToTable(table, activeView);
53
+ }, [table, activeView]);
54
+
55
+ return (
56
+ <div className="flex h-full flex-col gap-4 overflow-hidden">
57
+ <div className="shrink-0 flex items-center gap-2">
58
+ <DataGridViewSelect
59
+ table={table}
60
+ variant="horizontal-tabs"
61
+ editable
62
+ views={views}
63
+ activeViewId={activeViewId}
64
+ onViewChange={(view) => setActiveViewId(view.id)}
65
+ onViewSave={(view) => setViews(prev => prev.map(item => item.id === view.id ? view : item))}
66
+ onViewCreate={(view) => setViews(prev => [...prev, view])}
67
+ onViewDelete={(viewId) => setViews(prev => prev.filter(item => item.id !== viewId))} />
68
+ <DataGridFilterMenu table={table} />
69
+ <DataGridGroupMenu table={table} />
70
+ <DataGridSortMenu table={table} />
71
+ <DataGridRowHeightMenu table={table} />
72
+ </div>
73
+ <div className="min-h-0 flex-1">
74
+ <DataGrid table={table} {...gridProps} height="auto" />
75
+ </div>
76
+ </div>
77
+ );
78
+ }
79
+ ```
80
+
81
+ `DataGridViewSelect` applies views automatically when the user clicks a different tab. The `useEffect` above is still needed for the initial/default view.
82
+
83
+ ## Backend saved views with custom row fetching
84
+
85
+ ```tsx
86
+ const viewSelect = useDocyrusDataViewSelect({
87
+ client,
88
+ appSlug: 'crm',
89
+ dataSourceSlug: 'contacts',
90
+ defaultRowGroupingColumn: 'status',
91
+ systemViews: [ALL_CONTACTS_VIEW]
92
+ });
93
+
94
+ const activeView = useMemo(
95
+ () => viewSelect.views.find(view => view.id === viewSelect.activeViewId),
96
+ [viewSelect.views, viewSelect.activeViewId]
97
+ );
98
+
99
+ const { table, ...gridProps } = useDataGrid<Row>({ data: rows, columns, enableGrouping: true });
100
+
101
+ useEffect(() => {
102
+ if (!activeView) return;
103
+ applyViewToTable(table, activeView);
104
+ }, [table, activeView]);
105
+
106
+ <DataGridViewSelect table={table} editable {...viewSelect.gridViewSelectProps} />
107
+ ```
108
+
109
+ Important: `useDocyrusDataViewSelect` gives you `views`, `fields`, `activeViewId`, CRUD callbacks, hidden-view state, and persistence. It does **not** fetch rows. If the active view should also shape the backend query, translate `activeView.columnVisibility`, `columnOrder`, `sorting`, and `filterQuery` into your request yourself or switch to `useDocyrusDataGrid`.
110
+
111
+ ## Manual toolbar building blocks
112
+
113
+ Use these when you do not want the prebuilt hook toolbar:
114
+
115
+ - `DataGridViewSelect`
116
+ - `DataGridFilterMenu`
117
+ - `DataGridSortMenu`
118
+ - `DataGridGroupMenu`
119
+ - `DataGridRowHeightMenu`
120
+ - `DataGridDisplayMenu`
121
+ - `DataGridViewMenu`
122
+ - your own search input wired to `table.setGlobalFilter(...)` or to backend query state
123
+
124
+ For persistence, system views, paging ownership, or manual translation of the active view into Docyrus query params, also read `advanced-saved-view-query-patterns.md`.
125
+
126
+ ## What you lose by going manual
127
+
128
+ `useDocyrusDataGrid` does several things automatically that manual mode does **not**:
129
+
130
+ - **Auto-`expand`** for reference fields. In manual mode you must add reference field slugs to your request's `expand` array yourself, or rich cells (user / relation / select / status) will only see bare IDs.
131
+ - **Reserved-column pinning.** `select` and `actions` are not auto-pinned to the left in manual mode — call `table.setColumnPinning({ left: ['select', 'actions'], right: [] })` after applying the active view.
132
+ - **Filter menu refetch wiring.** `DataGridFilterMenu` writes through to `table.setColumnFilters`, but a manual page's row query doesn't watch that state. You need to read `table.getState().columnFilters` (or lift the state into the host) and translate the filter values into your request's `filters` payload. Reset `pageIndex` to 0 on changes.
133
+ - **Async option search for relation/user filter columns.** The filter menu will show empty option lists unless you pass a `getAsyncOptions` resolver (signature: `(column) => AsyncOptionsConfig | undefined`) that calls `/v1/users` for user fields and `/v1/apps/{appSlug}/data-sources/{slug}/items` for relations.
134
+ - **Tenant-aware formatters.** Pass `formatDate`, `formatDateTime`, `formatNumber` through `useDataGrid({ meta: { ... } })`. Cells read them from `tableMeta`.
135
+ - **Shared users list.** Pass `users: ReadonlyArray<CellUserOption>` to `buildTanstackColumnDef({ users })` (or set the `cell.options` directly via `mapColumn`) so user cells get avatar + label from the global list.
136
+
137
+ If any of these matter to the page, prefer `useDocyrusDataGrid` and use `mapColumn` / `extraColumns` / toolbar enable flags to customize.
138
+
139
+ ## Gotchas
140
+
141
+ - Local/manual `SavedDataGridView` objects can include reserved columns like `select` and `actions` in `columnOrder`.
142
+ - Backend Docyrus-saved views store real field slugs only. If you want the standard reserved-column behavior, `useDocyrusDataGrid` is the safest path.
143
+ - Pass `fields` into `DataGridViewSelect` when you want the filter builder inside the editor.
144
+ - Pass `isSaving` and `isLoading` when the host owns async view CRUD.
145
+ - For manual Docyrus item requests, always send `columns`. Add reference-type field slugs (`field-userSelect`, `field-userMultiSelect`, `field-relation`, `field-relatedField`, `field-select`, `field-radioGroup`, `field-enum`, `field-systemEnum`, `field-multiSelect`, `field-tagSelect`, `field-status`, `field-approvalStatus`) to `expand` so the API returns `{ id, name, ... }` payloads.
@@ -0,0 +1,290 @@
1
+ # Tenant Preferences and Shared Users Providers
2
+
3
+ `useDocyrusDataGrid` accepts two tenant-scoped inputs that should be sourced from app-level providers, not refetched per page:
4
+
5
+ - **Tenant preferences** → drives `formatDate`, `formatDateTime`, `formatNumber` so date and money cells respect regional settings.
6
+ - **Shared users list** → seeds `field-userSelect` / `field-userMultiSelect` cell options so avatar + name render instantly without per-row fetches.
7
+
8
+ Both should be fetched **once** at app boot and re-used everywhere.
9
+
10
+ ## Tenant preferences provider
11
+
12
+ Source the utilities from `@docyrus/app-utils`:
13
+
14
+ ```ts
15
+ import {
16
+ getTenantPreferences,
17
+ createDateUtils,
18
+ createNumberUtils,
19
+ type DateUtils,
20
+ type NumberUtils,
21
+ type TenantPreferences
22
+ } from '@docyrus/app-utils';
23
+ ```
24
+
25
+ ### Provider shape
26
+
27
+ ```tsx
28
+ 'use client';
29
+
30
+ import {
31
+ createContext, useContext, useMemo, type ReactNode
32
+ } from 'react';
33
+
34
+ import { useDocyrusAuth } from '@docyrus/signin';
35
+ import { useQuery } from '@tanstack/react-query';
36
+
37
+ interface TenantContextValue {
38
+ preferences: TenantPreferences | null;
39
+ dateUtils: DateUtils | null;
40
+ numberUtils: NumberUtils | null;
41
+ isLoading: boolean;
42
+ }
43
+
44
+ const TenantContext = createContext<TenantContextValue>({
45
+ preferences: null,
46
+ dateUtils: null,
47
+ numberUtils: null,
48
+ isLoading: false
49
+ });
50
+
51
+ export function TenantProvider({ children }: { children: ReactNode }) {
52
+ const { client, status, user } = useDocyrusAuth();
53
+ const userTimezone = (user as { timeZone?: { id?: string } } | null)?.timeZone?.id;
54
+
55
+ const query = useQuery<TenantPreferences>({
56
+ queryKey: ['tenant', 'preferences'],
57
+ queryFn: () => getTenantPreferences(client!),
58
+ enabled: status === 'authenticated' && Boolean(client),
59
+ staleTime: 30 * 60_000
60
+ });
61
+
62
+ const value = useMemo<TenantContextValue>(() => {
63
+ const preferences = query.data ?? null;
64
+
65
+ if (!preferences) {
66
+ return {
67
+ preferences: null, dateUtils: null, numberUtils: null, isLoading: query.isLoading
68
+ };
69
+ }
70
+
71
+ return {
72
+ preferences,
73
+ dateUtils: createDateUtils({ preferences, userTimezone }),
74
+ numberUtils: createNumberUtils({ preferences }),
75
+ isLoading: query.isLoading
76
+ };
77
+ }, [query.data, query.isLoading, userTimezone]);
78
+
79
+ return <TenantContext.Provider value={value}>{children}</TenantContext.Provider>;
80
+ }
81
+
82
+ export function useTenant() {
83
+ return useContext(TenantContext);
84
+ }
85
+ ```
86
+
87
+ ### Wrapping for the data-grid hook
88
+
89
+ The grid hook expects three callable formatters. Wrap the utils once and expose them via a small hook:
90
+
91
+ ```tsx
92
+ export function useGridFormatters() {
93
+ const { dateUtils, numberUtils } = useTenant();
94
+
95
+ return useMemo(() => {
96
+ const formatDate = (value: unknown): string => {
97
+ if (value == null) return '';
98
+ if (!dateUtils) return String(value);
99
+ try { return dateUtils.formatDate(value as string | Date) ?? ''; } catch { return String(value); }
100
+ };
101
+
102
+ const formatDateTime = (value: unknown): string => {
103
+ if (value == null) return '';
104
+ if (!dateUtils) return String(value);
105
+ try { return dateUtils.formatDateTime(value as string | Date) ?? ''; } catch { return String(value); }
106
+ };
107
+
108
+ const formatNumber = (
109
+ value: unknown,
110
+ opts?: { variant?: 'number' | 'currency' | 'percent'; currency?: string }
111
+ ): string => {
112
+ if (value == null || value === '') return '';
113
+
114
+ const numeric = typeof value === 'number' ? value : Number(value);
115
+
116
+ if (Number.isNaN(numeric)) return String(value);
117
+ if (!numberUtils) return String(value);
118
+
119
+ const variant = opts?.variant ?? 'number';
120
+ const base = numberUtils.formatNumber(variant === 'percent' ? numeric * 100 : numeric) ?? String(numeric);
121
+
122
+ if (variant === 'currency' && opts?.currency) return `${base} ${opts.currency}`;
123
+ if (variant === 'percent') return `${base}%`;
124
+
125
+ return base;
126
+ };
127
+
128
+ return { formatDate, formatDateTime, formatNumber };
129
+ }, [dateUtils, numberUtils]);
130
+ }
131
+ ```
132
+
133
+ Note the `formatNumber` wrapper handles the variant semantics:
134
+
135
+ - `'percent'` multiplies by 100 and appends `%`.
136
+ - `'currency'` appends the currency code provided in the cell meta.
137
+ - Plain `'number'` goes straight through `numberUtils.formatNumber`.
138
+
139
+ If the consuming app uses different conventions (e.g. always show a currency symbol), customize the wrapper accordingly.
140
+
141
+ ## Shared users provider
142
+
143
+ `useDocyrusDataGrid` accepts `users: ReadonlyArray<CellUserOption>` (`{ value, label, avatarUrl?, initials? }`). Populate it from `/v1/users`:
144
+
145
+ ```tsx
146
+ 'use client';
147
+
148
+ import {
149
+ createContext, useContext, useMemo, type ReactNode
150
+ } from 'react';
151
+
152
+ import { useDocyrusAuth } from '@docyrus/signin';
153
+ import { useQuery } from '@tanstack/react-query';
154
+
155
+ import { type CellUserOption } from '@docyrus/ui/components/data-grid';
156
+
157
+ interface UserRecord {
158
+ id: string;
159
+ firstname?: string | null;
160
+ lastname?: string | null;
161
+ email?: string | null;
162
+ photo?: string | null;
163
+ [key: string]: unknown;
164
+ }
165
+
166
+ const UsersContext = createContext<{ users: ReadonlyArray<CellUserOption>; records: ReadonlyArray<UserRecord>; isLoading: boolean }>({
167
+ users: [], records: [], isLoading: false
168
+ });
169
+
170
+ function pickString(record: Record<string, unknown>, ...keys: Array<string>): string | null {
171
+ for (const key of keys) {
172
+ const value = record[key];
173
+
174
+ if (typeof value === 'string' && value.length > 0) return value;
175
+ }
176
+
177
+ return null;
178
+ }
179
+
180
+ function toUserOption(record: UserRecord): CellUserOption | null {
181
+ if (!record.id) return null;
182
+
183
+ const firstName = pickString(record, 'firstname', 'firstName', 'first_name');
184
+ const lastName = pickString(record, 'lastname', 'lastName', 'last_name');
185
+ const fullName = firstName ? lastName ? `${firstName} ${lastName}` : firstName : lastName;
186
+ const email = pickString(record, 'email');
187
+ const label = fullName ?? email ?? record.id;
188
+ const avatarUrl = pickString(record, 'photo', 'avatar_url', 'avatarUrl', 'profile_image_url', 'profileImageUrl') ?? undefined;
189
+
190
+ return { value: record.id, label, ...(avatarUrl ? { avatarUrl } : {}) };
191
+ }
192
+
193
+ export function UsersProvider({ children }: { children: ReactNode }) {
194
+ const { client, status } = useDocyrusAuth();
195
+
196
+ const query = useQuery<Array<UserRecord>>({
197
+ queryKey: ['users', 'all'],
198
+ queryFn: async () => {
199
+ const response = await client!.get<Array<UserRecord> | { data?: Array<UserRecord> }>(
200
+ '/v1/users',
201
+ { limit: 1000 }
202
+ );
203
+
204
+ return Array.isArray(response) ? response : (response.data ?? []);
205
+ },
206
+ enabled: status === 'authenticated' && Boolean(client),
207
+ staleTime: 5 * 60_000
208
+ });
209
+
210
+ const value = useMemo(() => {
211
+ const records = query.data ?? [];
212
+ const users = records
213
+ .map(toUserOption)
214
+ .filter((option): option is CellUserOption => option !== null);
215
+
216
+ return { users, records, isLoading: query.isLoading };
217
+ }, [query.data, query.isLoading]);
218
+
219
+ return <UsersContext.Provider value={value}>{children}</UsersContext.Provider>;
220
+ }
221
+
222
+ export function useUsers() {
223
+ return useContext(UsersContext);
224
+ }
225
+ ```
226
+
227
+ ### User record field name fallbacks
228
+
229
+ The Docyrus `/v1/users` API returns `firstname` / `lastname` (lowercase, no separator) plus `email` and `photo`. The provider also accepts the common camelCase / snake_case aliases as fallbacks so the same code works against tenants that have customized the user schema.
230
+
231
+ ## Wiring providers into the app shell
232
+
233
+ Place the providers inside the auth + query providers and outside the routes:
234
+
235
+ ```tsx
236
+ <AuthProvider>
237
+ <QueryProvider>
238
+ <DevtoolsProvider>
239
+ <TenantProvider>
240
+ <UsersProvider>
241
+ <Routes>{/* ... */}</Routes>
242
+ </UsersProvider>
243
+ </TenantProvider>
244
+ </DevtoolsProvider>
245
+ </QueryProvider>
246
+ </AuthProvider>
247
+ ```
248
+
249
+ Order matters:
250
+
251
+ - `AuthProvider` must wrap both because the providers need an authenticated `client` to fetch.
252
+ - `QueryProvider` must wrap both because they use `useQuery`.
253
+ - `TenantProvider` and `UsersProvider` are siblings; either order is fine.
254
+
255
+ ## Wiring providers into a grid page
256
+
257
+ ```tsx
258
+ const { users } = useUsers();
259
+ const { formatDate, formatDateTime, formatNumber } = useGridFormatters();
260
+
261
+ const { table, gridProps, toolbar } = useDocyrusDataGrid<Row>({
262
+ client,
263
+ appSlug,
264
+ dataSourceSlug,
265
+ users,
266
+ formatDate,
267
+ formatDateTime,
268
+ formatNumber,
269
+ // ... other options
270
+ });
271
+ ```
272
+
273
+ The grid hook routes:
274
+
275
+ - `formatDate` / `formatDateTime` / `formatNumber` into TanStack `tableMeta`. `DateCell`, `DateTimeCell`, `NumberCell` (and its currency / percent variants) read these via `tableMeta?.formatDate` etc. and prefer them over their built-in fallbacks.
276
+ - `users` into the cell-options builder for `field-userSelect` and `field-userMultiSelect` columns. The cells render avatar + label from this list immediately. Rows pointing at users not in the list still render correctly via the expanded-payload fallback.
277
+
278
+ ## Why centralize these
279
+
280
+ - Single network round-trip per session for tenant prefs and the user roster.
281
+ - Consistent formatting and avatar display across every grid, form, and dialog in the app.
282
+ - Pages don't need to know about formatters or user fetching — they just consume context.
283
+ - Tenant settings change rarely (long staleTime); user roster also changes slowly (5 min staleTime is a safe default).
284
+
285
+ ## Debug checklist
286
+
287
+ - **Dates / numbers showing as raw values?** Tenant query likely failed or hasn't resolved yet. Check `useTenant().isLoading` and the network tab for `getTenantPreferences`.
288
+ - **User cells show "Unassigned" instead of names?** Either `users` wasn't passed in, or the row's user payload didn't expand. Verify `useDocyrusDataGrid` sees both the global `users` list (via context) and that the user column slug appears in `expand` (auto-added; check `resolvedListParams.expand`).
289
+ - **Currency shows "1.234,50 USD" but you wanted "$1,234.50"?** Customize the `formatNumber` wrapper for that variant. The default is locale-formatted number + currency code suffix.
290
+ - **Avatars missing?** The provider reads `photo` first, then `avatar_url` / `avatarUrl` / `profile_image_url` / `profileImageUrl`. Confirm the API returns one of those fields.
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: docyrus-record-detail-form-design
3
+ description: Build Docyrus React record forms, detail sheets, inspector panels, and inline-edit record UIs with `useDocyrusFormView`, `DynamicFormField`, `DynamicValue`, `EditableRecordDetail`, `EditableValue`, and value renderers. Use when asked to create or refactor create/edit/view pages, modal or sheet record forms, click-to-edit detail panels, metadata-driven field layouts, manual read-only record summaries, or field-type mapping logic that must choose correctly between form inputs, inline editors, and value-renderer fallbacks.
4
+ ---
5
+
6
+ # Docyrus Record Detail Form Design
7
+
8
+ Build full Docyrus record create, edit, and detail experiences around shared field metadata.
9
+
10
+ ## Choose the build mode
11
+
12
+ 1. **Standard create/edit/view record screen** → use `useDocyrusFormView`.
13
+ - Best when the page is a normal Docyrus form or detail surface.
14
+ - Handles metadata loading, item loading, option hydration, field rendering, and submit behavior.
15
+ - Read `references/hook-form-view.md`.
16
+
17
+ 2. **Custom manual form or detail layout** → use `DynamicFormField`, `DynamicValue`, or `useDocyrusFieldComponent`.
18
+ - Best when the page already owns form state, layout, or data fetching.
19
+ - Read `references/manual-form-detail-patterns.md`.
20
+
21
+ 3. **Inline editing, click-to-edit detail rows, or field-by-field editing** → use `EditableRecordDetail` and `EditableValue`.
22
+ - Best when users should edit a record in place without switching to a full form.
23
+ - Read `references/advanced-inline-edit-and-renderers.md`.
24
+
25
+ 4. **Complex query/schema/API work** → also load `docyrus-api-dev` or `docyrus-app-dev-react`.
26
+ 5. **Field-type mapping or unsupported-field decisions** → read `references/field-type-mapping-and-fallbacks.md`.
27
+
28
+ ## Default workflow
29
+
30
+ 1. Confirm `appSlug`, `dataSourceSlug`, `mode`, and `itemId` if the record already exists.
31
+ 2. Decide whether the page should be hook-first or fully manual.
32
+ 3. Keep editable fields and read-only renderers on the same field-type registry.
33
+ 4. Hydrate enum, user, and relation options before declaring the page done.
34
+ 5. Verify create, edit, view, reset, and save behavior.
35
+ 6. If the page supports inline editing, verify companion-field saves for composite types such as money, phone, and status.
36
+
37
+ ## Non-negotiables
38
+
39
+ - Prefer `useDocyrusFormView` for standard create/edit/view pages.
40
+ - Manual editable layouts should use `DynamicFormField`, specific form-field components, or `useDocyrusFieldComponent(..., 'form-field')`.
41
+ - Manual read-only layouts should use `DynamicValue`, specific value renderers, or `useDocyrusFieldComponent(..., 'value-renderer')`.
42
+ - Do not invent parallel per-field switch statements when the shared registry already solves the dispatch.
43
+ - `useDocyrusFormView` already keeps form fields, value renderers, and inline detail mode aligned through `useDocyrusFieldComponent`.
44
+ - `clickToEdit` on `useDocyrusFormView` view layouts routes through `EditableRecordDetail`; use it when you want view-first UX with inline saves.
45
+ - Manual Docyrus item fetching must always send `columns`. `useDocyrusFormView` derives them automatically, including companion columns.
46
+ - Composite fields can require companion keys on read and write: money, phone, status, avatar, and similar types must stay intact in manual save flows.
47
+ - If a field has no editable component, create-mode UX should usually skip it; edit/view UX should usually fall back to a value renderer unless you have a better explicit design.
48
+
49
+ ## References
50
+
51
+ - `references/hook-form-view.md` — hook-first create, edit, view, layout, submit, and click-to-edit patterns.
52
+ - `references/manual-form-detail-patterns.md` — manual form and detail composition with field components, renderers, and shared field maps.
53
+ - `references/advanced-inline-edit-and-renderers.md` — `EditableRecordDetail`, `EditableValue`, renderer selection rules, and companion-field save patterns.
54
+ - `references/field-type-mapping-and-fallbacks.md` — shared field maps, fallback rules, and unsupported-field strategy by render mode.
@@ -0,0 +1,140 @@
1
+ # Advanced Inline Editing and Renderer Patterns
2
+
3
+ ## Read this when
4
+
5
+ - The page should edit fields inline instead of switching to a dedicated form.
6
+ - You need change tracking or save/discard actions.
7
+ - You need to decide between `EditableRecordDetail`, `EditableValue`, `DynamicValue`, and raw form fields.
8
+ - A field type has companion values that must survive save flows.
9
+
10
+ ## `EditableRecordDetail` for record-level inline editing
11
+
12
+ Use `EditableRecordDetail` when the whole record should be displayed as a detail surface, but each row can become editable.
13
+
14
+ ```tsx
15
+ import {
16
+ EditableRecordDetail,
17
+ EditableRecordDetailField
18
+ } from '@docyrus/ui/components/editable-record-detail';
19
+
20
+ <EditableRecordDetail
21
+ fields={fields}
22
+ record={record}
23
+ onSave={async (changes, values) => {
24
+ void changes;
25
+ void values;
26
+ }}
27
+ trackChanges>
28
+ <EditableRecordDetailField slug="full_name" />
29
+ <EditableRecordDetailField slug="email" />
30
+ <EditableRecordDetailField slug="status" />
31
+ </EditableRecordDetail>
32
+ ```
33
+
34
+ Use it when:
35
+
36
+ - the page is a detail panel or sheet
37
+ - users should edit a few fields in place
38
+ - you want a floating save/discard bar with changed-field summaries
39
+
40
+ ## `EditableValue` for single-field inline editing
41
+
42
+ Use `EditableValue` when only one field should toggle between display and edit modes.
43
+
44
+ ```tsx
45
+ <EditableValue
46
+ field={field}
47
+ value={record[field.slug]}
48
+ record={record}
49
+ enumOptions={enumOptions}
50
+ onValueChange={(next) => saveOneField(field.slug, next)}
51
+ onCompanionChange={(companion) => saveCompanionFields(companion)}
52
+ changed={isChanged}
53
+ trackChanges
54
+ />
55
+ ```
56
+
57
+ This is the right primitive for compact inline edits inside cards, summaries, or custom detail rows.
58
+
59
+ ## Field behavior categories inside `EditableValue`
60
+
61
+ `EditableValue` is not a simple text editor. It dispatches behavior by field type:
62
+
63
+ - inline types save on blur or Enter
64
+ - instant-save types commit immediately on change
65
+ - explicit-save types show confirm/cancel actions
66
+ - popover types keep edit mode stable while focus moves into portaled content
67
+ - read-only types stay display-only
68
+
69
+ That is why `useDocyrusFieldComponent(..., 'editable-value')` always returns `EditableValue` instead of a field-type-specific component.
70
+
71
+ ## Companion fields matter
72
+
73
+ Manual inline save flows must preserve companion keys for composite field types.
74
+
75
+ Common examples:
76
+
77
+ - money → value + `__slug_currency`
78
+ - phone → value + `__slug_country`
79
+ - status → value + `__slug_secondary`, `__slug_description`, `__slug_followup_date`
80
+ - avatar → main value plus mapped companion fields
81
+
82
+ If you save only the visible main field, you can silently corrupt or flatten the record state.
83
+
84
+ ## Renderer selection rules
85
+
86
+ Use these defaults:
87
+
88
+ - full editable form → form-field component
89
+ - full read-only detail → value renderer
90
+ - one-off inline field → `EditableValue`
91
+ - view-first detail screen with record-level inline save → `EditableRecordDetail`
92
+
93
+ If a field has no editable form component, prefer a value-render fallback instead of inventing a broken partial editor.
94
+
95
+ ## Value-renderer guidance
96
+
97
+ Value renderers are not just pretty labels. They understand field semantics:
98
+
99
+ - `StatusValue` reads status companion data like description and follow-up date
100
+ - `MoneyValue` formats amounts with currency context
101
+ - `UserValue` and `UserMultiValue` present users properly
102
+ - `RelationValue` renders relationship display values
103
+ - `RichTextValue` and `DocEditorValue` display stored rich content safely
104
+
105
+ Prefer the value renderer over custom text formatting whenever you are showing a Docyrus field type in read-only mode.
106
+
107
+ For the pure field-type dispatch matrix and unsupported-field defaults, also read `field-type-mapping-and-fallbacks.md`.
108
+
109
+ ## Shared maps are the source of truth
110
+
111
+ The safest advanced pattern is to stay on the shared registries exposed by `useDocyrusFieldComponent`:
112
+
113
+ - `FORM_FIELD_MAP`
114
+ - `VALUE_RENDERER_MAP`
115
+ - `CELL_COMPONENT_MAP`
116
+
117
+ That keeps forms, detail views, data-grid cells, and inline editing behavior aligned.
118
+
119
+ ## Good combinations
120
+
121
+ ### Detail page with manual sections + inline fields
122
+
123
+ - layout shell: your own markup
124
+ - read-only rows: `DynamicValue`
125
+ - editable rows: `EditableValue`
126
+ - bulk record save experience: `EditableRecordDetail`
127
+
128
+ ### Hook-first detail page with selective inline editing
129
+
130
+ - outer data workflow: `useDocyrusFormView`
131
+ - default layout: `renderLayout()` in `view` mode
132
+ - inline edit UX: `clickToEdit: true`
133
+
134
+ ## Debug checklist
135
+
136
+ - **Field never becomes editable?** It may be a read-only field type or lack a registered form-field component.
137
+ - **Inline save loses currency/country/status metadata?** You forgot companion-field handling.
138
+ - **Renderer shows weak output?** Check whether `record` or `enumOptions` is incomplete.
139
+ - **Detail page edits but does not show change state?** Enable `trackChanges` and wire changed-state inputs correctly.
140
+ - **Custom manual switch statement is drifting from Docyrus UI behavior?** Replace it with `useDocyrusFieldComponent`, `DynamicFormField`, or `DynamicValue`.