@docyrus/docyrus 0.0.67 → 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.
@@ -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`.
@@ -0,0 +1,150 @@
1
+ # Field-Type Mapping and Unsupported-Field Strategy
2
+
3
+ ## Read this when
4
+
5
+ - You need to decide which component stack should render a Docyrus field.
6
+ - A field type has no editable component yet.
7
+ - You are building manual metadata-driven UIs and want safe fallback behavior.
8
+ - You need to extend Docyrus UI for a new field type without breaking forms, details, and inline editing.
9
+
10
+ ## Single source of truth
11
+
12
+ Docyrus field rendering should flow through the shared field maps exposed by `useDocyrusFieldComponent`:
13
+
14
+ - `FORM_FIELD_MAP`
15
+ - `VALUE_RENDERER_MAP`
16
+ - `CELL_COMPONENT_MAP`
17
+
18
+ Use these indirectly through:
19
+
20
+ - `DynamicFormField`
21
+ - `DynamicValue`
22
+ - `useDocyrusFormView`
23
+ - `useDocyrusFieldComponent`
24
+
25
+ Do **not** create a parallel local switch statement unless there is a very strong reason.
26
+
27
+ ## Render-context lookup matrix
28
+
29
+ `useDocyrusFieldComponent(field.type, kind)` supports five render kinds:
30
+
31
+ | kind | Typical use | Unknown / unsupported fallback |
32
+ |------|-------------|--------------------------------|
33
+ | `form-field` | editable forms | `null` |
34
+ | `value-renderer` | read-only detail | `TextValue` |
35
+ | `data-grid-cell-variant` | grid cells | `ShortTextCell` |
36
+ | `editable-value` | single-field inline edit | `EditableValue` |
37
+ | `tanstack-column-def` | dynamic table columns | builder with short-text cell fallback |
38
+
39
+ This fallback behavior is the baseline for every manual page and hook-first page.
40
+
41
+ ## What each dispatcher does
42
+
43
+ ### `DynamicFormField`
44
+
45
+ - resolves `form-field`
46
+ - returns `null` when the field type has no registered editable component
47
+ - best for metadata-driven editable forms
48
+
49
+ ### `DynamicValue`
50
+
51
+ - resolves `value-renderer`
52
+ - always returns a renderable component because the fallback is `TextValue`
53
+ - best for metadata-driven read-only layouts
54
+
55
+ ### `EditableValue`
56
+
57
+ - is the inline-edit dispatcher
58
+ - returned for the `editable-value` kind regardless of field type
59
+ - internally decides whether the field behaves like inline edit, instant save, explicit save, popover edit, or read-only display
60
+
61
+ ### `useDocyrusFormView`
62
+
63
+ - uses `form-field` in editable mode
64
+ - uses `value-renderer` in read-only mode
65
+ - handles unsupported editable fields differently by mode:
66
+ - create mode default: `unsupportedFieldBehavior = 'skip'`
67
+ - edit/view mode default: `unsupportedFieldBehavior = 'value'`
68
+
69
+ That default is usually correct because create screens should not tease unusable inputs, while edit/view screens can still show existing values safely.
70
+
71
+ ## Recommended unsupported-field strategy
72
+
73
+ ### Create mode
74
+
75
+ Prefer skipping unsupported editable fields unless the page explicitly designs a read-only preview row.
76
+
77
+ Why:
78
+
79
+ - users expect every visible field in a create form to be fillable
80
+ - showing a read-only renderer inside create mode can be confusing
81
+ - the hook default already follows this rule
82
+
83
+ ### Edit mode
84
+
85
+ Prefer value-render fallback when the field cannot be edited.
86
+
87
+ Why:
88
+
89
+ - users can still review the field
90
+ - the record detail stays complete
91
+ - you avoid broken partial editors
92
+
93
+ ### View mode
94
+
95
+ Always prefer value renderers.
96
+
97
+ That is the natural mode for detail pages, approval reviews, summaries, and read-only sheets.
98
+
99
+ ## Safe manual pattern
100
+
101
+ When building a manual dynamic page, use this decision order:
102
+
103
+ ```tsx
104
+ const FormField = useDocyrusFieldComponent(field.type, 'form-field');
105
+ const Value = useDocyrusFieldComponent(field.type, 'value-renderer');
106
+
107
+ if (mode === 'view') {
108
+ return <Value field={field} value={value} record={record} enumOptions={options} />;
109
+ }
110
+
111
+ if (FormField) {
112
+ return <FormField field={field} form={form} enumOptions={options} />;
113
+ }
114
+
115
+ return mode === 'create'
116
+ ? null
117
+ : <Value field={field} value={value} record={record} enumOptions={options} />;
118
+ ```
119
+
120
+ This keeps manual pages aligned with hook-first Docyrus behavior.
121
+
122
+ ## When a field type needs more than mapping
123
+
124
+ Some field types require more than just selecting a component:
125
+
126
+ - enum/select-like fields need `enumOptions`
127
+ - user and relation editors need hydrated option lists
128
+ - status fields may need companion values like description and follow-up date
129
+ - money and phone fields need companion currency/country keys
130
+ - avatar-like fields can require companion image or color keys
131
+
132
+ A field can be “supported” in the map but still render weakly if its supporting data is missing.
133
+
134
+ ## Adding support for a new field type
135
+
136
+ When extending Docyrus UI, check all relevant layers:
137
+
138
+ 1. add the editable component to `FORM_FIELD_MAP` if the type is editable
139
+ 2. add the read-only renderer to `VALUE_RENDERER_MAP`
140
+ 3. add the grid cell component or fallback strategy if the type should appear richly in grids
141
+ 4. update any companion read/write logic if the field stores secondary keys
142
+ 5. verify `useDocyrusFormView`, `DynamicFormField`, `DynamicValue`, and `EditableValue` behavior
143
+
144
+ ## Debug checklist
145
+
146
+ - **Field disappears in manual edit mode?** `form-field` likely resolved to `null`.
147
+ - **Field shows raw text instead of richer UI?** You are hitting the `value-renderer` fallback or missing supporting props.
148
+ - **Create form unexpectedly shows read-only rows?** Your unsupported strategy is too permissive for create mode.
149
+ - **Inline field never edits?** The field may be effectively read-only in `EditableValue` or missing required option data.
150
+ - **New field type works in forms but not in details or grids?** You updated one map but not the others.
@@ -0,0 +1,127 @@
1
+ # Hook-first Record Forms and Detail Views
2
+
3
+ ## Use this path when
4
+
5
+ - The page is a standard Docyrus create, edit, or read-only record screen.
6
+ - You want field metadata, item loading, option hydration, and submit behavior handled in one place.
7
+ - You want the same field list to render as editable inputs in one mode and value renderers in another.
8
+
9
+ ## Minimal create form
10
+
11
+ ```tsx
12
+ 'use client';
13
+
14
+ import { useDocyrusAuth } from '@docyrus/signin';
15
+ import { useDocyrusFormView } from '@docyrus/ui/library/hooks/use-docyrus-form-view';
16
+ import { Button } from '@docyrus/ui/primitives/ui/button';
17
+
18
+ export function CreateContactForm() {
19
+ const { client } = useDocyrusAuth();
20
+
21
+ if (!client) return null;
22
+
23
+ const formView = useDocyrusFormView({
24
+ client,
25
+ appSlug: 'crm',
26
+ dataSourceSlug: 'contacts',
27
+ mode: 'create',
28
+ gridColumns: 2,
29
+ defaultValues: { status: 'lead' },
30
+ fieldOrder: ['full_name', 'email', 'phone', 'status', 'notes'],
31
+ fieldLayout: {
32
+ notes: { colSpan: 'full' }
33
+ }
34
+ });
35
+
36
+ return (
37
+ <form
38
+ className="space-y-4"
39
+ onSubmit={async (event) => {
40
+ event.preventDefault();
41
+ await formView.submit();
42
+ }}>
43
+ {formView.renderLayout()}
44
+ <div className="flex items-center gap-2">
45
+ <Button type="submit" disabled={formView.isSubmitting || formView.isLoading}>Create</Button>
46
+ <Button type="button" variant="outline" onClick={formView.reset}>Reset</Button>
47
+ </div>
48
+ </form>
49
+ );
50
+ }
51
+ ```
52
+
53
+ ## Minimal edit and view patterns
54
+
55
+ - **edit**: pass `mode: 'edit'` and `itemId`.
56
+ - **view**: pass `mode: 'view'` and `itemId`.
57
+ - **prefetched record**: pass `item` to skip the item query.
58
+ - **generated collection**: pass `collection` so the hook uses `collection.get`, `collection.create`, and `collection.update`.
59
+
60
+ ## Click-to-edit detail mode
61
+
62
+ In `view` mode, `clickToEdit: true` swaps the plain value layout for `EditableRecordDetail` while still using the normal Docyrus submit pipeline.
63
+
64
+ ```tsx
65
+ const detailView = useDocyrusFormView({
66
+ client,
67
+ appSlug: 'crm',
68
+ dataSourceSlug: 'contacts',
69
+ itemId: contactId,
70
+ mode: 'view',
71
+ clickToEdit: true,
72
+ fieldOrder: ['full_name', 'email', 'phone', 'status', 'notes']
73
+ });
74
+
75
+ return detailView.renderLayout();
76
+ ```
77
+
78
+ Use this when the page should feel like a detail sheet first, but still allow inline field edits.
79
+
80
+ ## High-value options
81
+
82
+ - `fieldOrder`: impose explicit field ordering.
83
+ - `fieldSlugs`: whitelist fields.
84
+ - `hiddenFieldSlugs`: hard-hide fields.
85
+ - `fieldLayout`: override hidden, required, readOnly, disabled, colSpan, label, description, and per-field props.
86
+ - `layout`: build nested `fieldset`, `tabpanel`, and `tab` sections.
87
+ - `mapField`: transform normalized field metadata or drop a field entirely.
88
+ - `includeReadOnlyFields`: keep read-only rows in the rendered layout.
89
+ - `unsupportedFieldBehavior`: choose whether unsupported editable fields are skipped or rendered as values.
90
+ - `resolveUserOptions`, `resolveRelationOptions`, `enumOptions`: control option hydration.
91
+ - `transformSubmit`: final payload transform before create/update.
92
+ - `onSubmit`: replace default submit behavior entirely.
93
+ - `onSubmitSuccess`, `onSubmitError`: react to mutation outcomes.
94
+
95
+ ## How render mode is decided
96
+
97
+ The hook resolves components from the shared field-component registry:
98
+
99
+ - editable mode → `form-field`
100
+ - read-only mode → `value-renderer`
101
+
102
+ It does not keep a separate form/detail mapping system. That means `useDocyrusFormView`, `DynamicFormField`, `DynamicValue`, and data-grid field behavior stay aligned when the registry changes.
103
+
104
+ ## Why this hook is usually the default
105
+
106
+ It handles three difficult layers together:
107
+
108
+ 1. data-source metadata
109
+ 2. current item loading
110
+ 3. dynamic option hydration for enum, user, and relation fields
111
+
112
+ It also derives the required backend `columns`, including companion columns for composite fields.
113
+
114
+ ## Important behavior
115
+
116
+ - `renderField(slug)` renders a single resolved field.
117
+ - `renderLayout()` renders the full layout grid and is usually the fastest path.
118
+ - `reset()` restores the latest committed baseline.
119
+ - successful `submit()` updates the clean baseline, so the page stops being dirty.
120
+ - in `view` mode, `submit()` returns current values and skips normal validation.
121
+
122
+ ## Default recommendation
123
+
124
+ Start with `useDocyrusFormView` unless the page already has its own form state, its own item query lifecycle, or a heavily custom layout system that would fight the hook.
125
+
126
+ For deeper inline editing and renderer behavior, also read `advanced-inline-edit-and-renderers.md`.
127
+ For shared field-map behavior and unsupported-field defaults, also read `field-type-mapping-and-fallbacks.md`.
@@ -0,0 +1,125 @@
1
+ # Manual Form and Detail Patterns
2
+
3
+ ## Use this path when
4
+
5
+ - The page already owns its form state.
6
+ - You want a completely custom layout.
7
+ - The record is already loaded elsewhere.
8
+ - You need precise control over when editable inputs versus read-only renderers appear.
9
+
10
+ ## Manual editable form with `DynamicFormField`
11
+
12
+ ```tsx
13
+ 'use client';
14
+
15
+ import { useForm } from '@tanstack/react-form';
16
+ import { DynamicFormField, type IField, type EnumOption } from '@docyrus/ui/components/form-fields';
17
+
18
+ const fields: IField[] = [
19
+ { id: '1', name: 'Full Name', slug: 'full_name', type: 'field-text' },
20
+ { id: '2', name: 'Status', slug: 'status', type: 'field-select' }
21
+ ];
22
+
23
+ const statusOptions: EnumOption[] = [
24
+ { id: 'lead', slug: 'lead', name: 'Lead', color: '#64748b' },
25
+ { id: 'customer', slug: 'customer', name: 'Customer', color: '#22c55e' }
26
+ ];
27
+
28
+ export function ContactForm() {
29
+ const form = useForm({
30
+ defaultValues: {
31
+ full_name: '',
32
+ status: 'lead'
33
+ },
34
+ onSubmit: async ({ value }) => {
35
+ void value;
36
+ }
37
+ });
38
+
39
+ return (
40
+ <form onSubmit={(event) => { event.preventDefault(); void form.handleSubmit(); }}>
41
+ {fields.map((field) => (
42
+ <DynamicFormField
43
+ key={field.id}
44
+ field={field}
45
+ form={form}
46
+ enumOptions={field.slug === 'status' ? statusOptions : undefined} />
47
+ ))}
48
+ </form>
49
+ );
50
+ }
51
+ ```
52
+
53
+ Use the specific form-field components directly when you want more explicit imports or field-specific props. Use `DynamicFormField` when the field list is metadata-driven.
54
+
55
+ ## Manual read-only detail with `DynamicValue`
56
+
57
+ ```tsx
58
+ import { DynamicValue } from '@docyrus/ui/components/value-renderers';
59
+
60
+ function ContactDetail({ fields, record, statusOptions }: {
61
+ fields: IField[];
62
+ record: Record<string, unknown>;
63
+ statusOptions: EnumOption[];
64
+ }) {
65
+ return (
66
+ <div className="grid gap-4 md:grid-cols-2">
67
+ {fields.map((field) => (
68
+ <div key={field.id} className="space-y-1">
69
+ <div className="text-sm font-medium">{field.name}</div>
70
+ <DynamicValue
71
+ field={field}
72
+ value={record[field.slug]}
73
+ record={record}
74
+ enumOptions={field.slug === 'status' ? statusOptions : undefined} />
75
+ </div>
76
+ ))}
77
+ </div>
78
+ );
79
+ }
80
+ ```
81
+
82
+ Use this when the page is read-only or when some fields are intentionally not editable.
83
+
84
+ ## Shared registry pattern with `useDocyrusFieldComponent`
85
+
86
+ When you need total control, use the registry hook directly instead of writing a per-type switch.
87
+
88
+ ```tsx
89
+ const FormField = useDocyrusFieldComponent(field.type, 'form-field');
90
+ const Value = useDocyrusFieldComponent(field.type, 'value-renderer');
91
+
92
+ if (isEditing && FormField) {
93
+ return <FormField field={field} form={form} enumOptions={options} />;
94
+ }
95
+
96
+ return <Value field={field} value={value} record={record} enumOptions={options} />;
97
+ ```
98
+
99
+ This is the safest manual pattern because it stays aligned with the Docyrus UI field maps.
100
+
101
+ ## When to choose which primitive
102
+
103
+ - `DynamicFormField`: metadata-driven editable inputs.
104
+ - specific form-field component: explicit field type, custom props, or smaller import surface.
105
+ - `DynamicValue`: metadata-driven read-only display.
106
+ - specific value renderer: explicit display control for a known field type.
107
+ - `useDocyrusFieldComponent`: custom render orchestration that still stays on the shared registry.
108
+
109
+ ## Manual option requirements
110
+
111
+ Manual pages must supply the option data that the renderer or field needs:
112
+
113
+ - select-like fields need `enumOptions`
114
+ - relation renderers usually need `record` context
115
+ - user and relation field editors often need hydrated options upstream
116
+ - composite renderers such as status, money, phone, and avatar depend on companion values in the record object
117
+
118
+ If those values are absent, the component may render a weaker fallback.
119
+
120
+ ## Common manual pattern
121
+
122
+ Use editable inputs in edit mode and read-only renderers in view mode, but keep the same field list and the same metadata source. That keeps create, edit, and detail pages consistent even when the layout differs.
123
+
124
+ For inline editing and change tracking, also read `advanced-inline-edit-and-renderers.md`.
125
+ For field-type dispatch rules and unsupported-field fallback strategy, also read `field-type-mapping-and-fallbacks.md`.