@riducms/plugin 0.1.4 → 0.2.0

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/src/authoring.ts CHANGED
@@ -1,6 +1,7 @@
1
- import type { SchemaCollection, SchemaField } from "@riducms/protocol";
2
- import type { Component } from "svelte";
1
+ import type { SchemaCollection, SchemaField, ValidationIssue } from "@riducms/protocol";
2
+ import type { Component, Snippet } from "svelte";
3
3
 
4
+ /** A saved document returned by Ridu. Check custom fields before using their unknown values. */
4
5
  export type FieldDocument = Record<string, unknown> & {
5
6
  id: string;
6
7
  createdAt?: string;
@@ -13,6 +14,7 @@ export type FieldDocument = Record<string, unknown> & {
13
14
  };
14
15
  };
15
16
 
17
+ /** A condition that narrows documents offered by the reference browser. It does not grant access. */
16
18
  export interface FieldReferenceFilter {
17
19
  field: string;
18
20
  operator:
@@ -27,28 +29,184 @@ export interface FieldReferenceFilter {
27
29
  value: string | number | boolean;
28
30
  }
29
31
 
32
+ /**
33
+ * Props for Ridu's related-document picker and editor. Render the component supplied
34
+ * as `authoring.referenceBrowser`; your `onCommit` decides how selected IDs change
35
+ * your field. Saving a related document inside the browser is a separate server save.
36
+ */
30
37
  export interface FieldReferenceBrowserProps {
38
+ /** Whether the browser is visible. */
31
39
  open?: boolean;
40
+ /** The relationship/upload field whose rules the browser should use. */
32
41
  field: SchemaField;
42
+ /** The target collection's resolved schema, available from `authoring.collections`. */
33
43
  collection: SchemaCollection;
44
+ /** Allow multiple selections when true; otherwise select at most one document. */
34
45
  hasMany: boolean;
46
+ /** IDs currently selected by your editor. */
35
47
  selectedIDs: readonly string[];
48
+ /** Further restrict editing; false cannot override Ridu's access restrictions. */
36
49
  readOnly?: boolean;
50
+ /** Open this already-loaded document for editing instead of starting at the list. */
37
51
  initialDocument?: FieldDocument;
52
+ /** Fetch and open this document when `initialDocument` is not supplied. */
38
53
  initialDocumentID?: string;
54
+ /** Limit selectable documents; the server still applies its own access rules. */
39
55
  optionFilter?: FieldReferenceFilter | readonly FieldReferenceFilter[];
56
+ /** Initial values for a new related document, not changes to an existing document. */
40
57
  defaultValues?: Readonly<Record<string, unknown>>;
58
+ /** Allow creation when access permits it. Set false to hide creation. */
41
59
  allowCreate?: boolean;
60
+ /** Content locale for loading and editing related documents. */
42
61
  locale?: string;
62
+ /**
63
+ * Called when the user confirms the selection. Update your field here.
64
+ * Return false (or resolve to false) to keep the browser open. Returning void or
65
+ * true accepts the selection and closes it while editing remains allowed. A thrown
66
+ * error/rejected promise keeps it open and Ridu displays an error notification.
67
+ */
43
68
  onCommit: (ids: string[]) => void | boolean | Promise<void | boolean>;
69
+ /** Called when the browser closes; update your open/closed UI state here. */
44
70
  onClose: () => void;
45
71
  }
46
72
 
73
+ /**
74
+ * Select an existing group of ordinary fields inside a plugin value, such as the
75
+ * fields of one rich-text card. Go must declare the embedded tree and its schemas.
76
+ * Ridu locates the data by its stable identity, even when the card moves.
77
+ */
78
+ export interface EmbeddedSchemaFormScope {
79
+ /** The embedded tree key declared by the Go field, for example `"widgets"`. */
80
+ treeKey: string;
81
+ /** The existing item's stable ID, as declared by that tree's identity configuration. */
82
+ identity: string;
83
+ /** Display without editing; false does not override document or field access rules. */
84
+ readOnly?: boolean;
85
+ }
86
+
87
+ /** Select a Go-declared embedded variant when creating or copying an item's field data. */
88
+ export interface EmbeddedSchemaVariantScope {
89
+ /** The embedded tree key declared by the Go field. */
90
+ treeKey: string;
91
+ /** The tree case's configured tag value, not the name of the property holding the tag. */
92
+ caseTag: string;
93
+ /** The variant key within that case, for example `"hero"`. */
94
+ variantSlug: string;
95
+ }
96
+
97
+ /**
98
+ * A temporary Apply/Cancel form for fields inside one plugin item.
99
+ * Its edits stay separate from the document until your Apply handler accepts them.
100
+ * This is not a saved document draft/version and cannot save to the server itself.
101
+ * Ridu tracks pending edits so the outer form can ask the user to Apply or Cancel.
102
+ */
103
+ export interface EmbeddedSchemaDraft {
104
+ /** Unique ID for this temporary editing session. */
105
+ readonly id: string;
106
+ /** Stable ID of the item being edited, or the proposed ID for a new item. */
107
+ readonly identity: string;
108
+ /** Whether edits await Apply/Cancel. A proposed new item counts as dirty even before typing. */
109
+ readonly dirty: boolean;
110
+ /**
111
+ * Whether the draft is no longer usable. Removal, changed source data, document/
112
+ * locale/schema/access changes, discard, or closing the parent editor can expire it.
113
+ * Reopen a draft instead of applying old data. Moving the same item alone is allowed.
114
+ */
115
+ readonly stale: boolean;
116
+ /** Current validation issues shown in this draft. */
117
+ readonly issues: readonly ValidationIssue[];
118
+ /** Copy the draft's current field values; throws if it has expired or editing is blocked. */
119
+ payload(): Record<string, unknown>;
120
+ /**
121
+ * Check the draft against its declared field rules and update `issues`.
122
+ * Returns whether it passes. Go validators and hooks run later, on document save.
123
+ * The guarded plugin method throws if the draft has expired or editing is blocked.
124
+ */
125
+ validate(): boolean;
126
+ /** Release this draft and abandon its pending edits. Safe to call more than once. */
127
+ discard(): void;
128
+ }
129
+
130
+ /** Options for rendering a draft in Ridu's drawer with Apply and Cancel controls. */
131
+ export interface EmbeddedSchemaDraftEditorProps {
132
+ /** A draft created by this field's `authoring.beginSchemaDraft`. */
133
+ draft: EmbeddedSchemaDraft;
134
+ /** Optional heading for the drawer. */
135
+ title?: string;
136
+ /**
137
+ * Receives checked field data when Apply succeeds. Insert or update the item in
138
+ * your plugin value and call `field.set` (or serialize your editor's updated state).
139
+ * Close your drawer UI here. Ridu discards the temporary draft after this handler
140
+ * returns; it does not insert data into your plugin value or save the document.
141
+ */
142
+ onApply: (payload: Record<string, unknown>) => void;
143
+ /** Close your editor UI after the pending draft has been discarded. */
144
+ onCancel: () => void;
145
+ }
146
+
147
+ /**
148
+ * Ridu tools supplied to a plugin field for embedded forms and related documents.
149
+ * Optional methods must be checked before use. They work within the current field's
150
+ * lifetime: async reads/requests reject if the field expires before they complete.
151
+ * Already-dispatched server work is not undone. Cancel requests when appropriate,
152
+ * and prevent an older request from overwriting a newer result in your own UI.
153
+ */
47
154
  export interface FieldAuthoringHost {
155
+ /**
156
+ * Render the ordinary fields of an existing embedded item using
157
+ * `{@render authoring.schemaForm({ treeKey, identity })}`.
158
+ * Edits update the parent document's unsaved form immediately. Use a draft below
159
+ * if the user needs Apply/Cancel. Neither approach saves the document itself.
160
+ */
161
+ schemaForm?: Snippet<[EmbeddedSchemaFormScope]>;
162
+ /**
163
+ * Start a temporary Apply/Cancel form. Pass `{ treeKey, identity }` to edit an
164
+ * existing item, or `{ treeKey, caseTag, variantSlug }` to prepare a new one using
165
+ * Go-declared field defaults. Creation does not insert anything into your value.
166
+ * Call `discard` when abandoning a draft; closing the field also cleans it up.
167
+ */
168
+ beginSchemaDraft?: (
169
+ scope: EmbeddedSchemaFormScope | EmbeddedSchemaVariantScope
170
+ ) => EmbeddedSchemaDraft;
171
+ /** Render a draft in Ridu's drawer; your `onApply` writes the accepted data into your plugin value. */
172
+ schemaDraftEditor?: Snippet<[EmbeddedSchemaDraftEditorProps]>;
173
+ /** Read current parent-form issues for an existing embedded item, for example for a card badge. */
174
+ schemaIssues?: (scope: EmbeddedSchemaFormScope) => readonly ValidationIssue[];
175
+ /**
176
+ * Copy an item's ordinary field data and give its schema-declared nested rows/items
177
+ * fresh IDs. Use when duplicating to avoid sharing identities. This does not insert
178
+ * the copy or copy your outer plugin item; update that structure in your editor.
179
+ */
180
+ copySchemaPayload?: (
181
+ scope: EmbeddedSchemaVariantScope,
182
+ payload: Readonly<Record<string, unknown>>
183
+ ) => Record<string, unknown>;
184
+ /** Collection definitions available in the current admin schema, not fetched documents. */
48
185
  readonly collections: readonly SchemaCollection[];
186
+ /**
187
+ * Admin change counter: it advances when Ridu is notified that documents changed.
188
+ * Useful for refreshing lookup UI; it is not a document's saved `_revision` number.
189
+ */
49
190
  readonly documentRevision: number;
191
+ /** The content locale being edited, separate from the admin interface language. */
50
192
  readonly locale?: string;
193
+ /** Related-document picker/editor component. Its `onCommit` handler updates your field. */
51
194
  referenceBrowser: Component<FieldReferenceBrowserProps>;
195
+ /**
196
+ * Fetch a saved document by collection slug and ID in the current content locale.
197
+ * Does not include unsaved edits from the current form. Rejects on request failure
198
+ * or if this field expires while waiting. Returns a copy; edits to it are local.
199
+ */
52
200
  findDocument(collection: string, id: string, signal?: AbortSignal): Promise<FieldDocument>;
201
+ /**
202
+ * POST to an endpoint declared by this renderer's paired Go plugin. Supply the
203
+ * plugin-relative path, not an arbitrary URL. This sends a real server request;
204
+ * it does not automatically update or save the containing form.
205
+ *
206
+ * Requires an editable field and rejects if the field expires while waiting.
207
+ * Server work already sent may still finish. The `Result` generic describes your
208
+ * expected response; it does not validate it. Use `unknown` and decode the result
209
+ * when the response shape needs checking.
210
+ */
53
211
  requestPlugin?<Result>(path: string, body: unknown, signal?: AbortSignal): Promise<Result>;
54
212
  }
@@ -0,0 +1,51 @@
1
+ /** Validates the explicit settings selected for a static component before decoding. */
2
+ export function decodeComponentConfig(
3
+ selection: { config?: unknown } | undefined,
4
+ decoder: ((value: unknown) => unknown) | undefined
5
+ ): unknown {
6
+ const supplied = selection !== undefined && Object.hasOwn(selection, "config");
7
+ if (!supplied) {
8
+ if (decoder !== undefined)
9
+ throw new Error("config is required by decodeConfig; supply a configuration object in Go");
10
+ return undefined;
11
+ }
12
+ if (decoder === undefined)
13
+ throw new Error(
14
+ "config was supplied but there is no decodeConfig; remove the Go configuration or register a decoder"
15
+ );
16
+ if (typeof decoder !== "function") throw new Error("decodeConfig must be a synchronous function");
17
+ const raw = selection.config;
18
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
19
+ throw new Error("supplied config must be a finite JSON object");
20
+ const seen = new Set<object>();
21
+ const copy = (value: unknown, depth: number): unknown => {
22
+ if (depth > 100) throw new Error("config exceeds the supported JSON depth");
23
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
24
+ if (typeof value === "number" && Number.isFinite(value)) return value;
25
+ if (
26
+ typeof value !== "object" ||
27
+ (!Array.isArray(value) &&
28
+ Object.getPrototypeOf(value) !== Object.prototype &&
29
+ Object.getPrototypeOf(value) !== null) ||
30
+ seen.has(value)
31
+ )
32
+ throw new Error("config must contain finite, acyclic JSON data");
33
+ seen.add(value);
34
+ const result = Array.isArray(value)
35
+ ? Array.from(value, (child) => copy(child, depth + 1))
36
+ : Object.fromEntries(
37
+ Object.entries(value).map(([key, child]) => [key, copy(child, depth + 1)])
38
+ );
39
+ seen.delete(value);
40
+ return result;
41
+ };
42
+ const decoded = decoder(copy(raw, 0));
43
+ if (
44
+ decoded !== null &&
45
+ (typeof decoded === "object" || typeof decoded === "function") &&
46
+ "then" in decoded &&
47
+ typeof decoded.then === "function"
48
+ )
49
+ throw new Error("config decoders must be synchronous");
50
+ return decoded;
51
+ }
@@ -0,0 +1,21 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import { FieldFrame } from "@riducms/ui";
4
+ import type { FieldBinding } from "./types";
5
+
6
+ let { field, children }: { field: Pick<FieldBinding, "schema" | "issues">; children: Snippet } =
7
+ $props();
8
+ </script>
9
+
10
+ <div data-field-path={field.schema.path}>
11
+ <FieldFrame
12
+ controlID={field.schema.id}
13
+ label={field.schema.admin.label}
14
+ required={field.schema.required}
15
+ readOnly={field.schema.admin.readOnly}
16
+ description={field.schema.admin.description}
17
+ errors={field.issues.map((issue) => issue.message)}
18
+ >
19
+ {@render children()}
20
+ </FieldFrame>
21
+ </div>
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Adds a label, description and validation messages around your custom field input.
3
+ *
4
+ * Import `Field from "@riducms/plugin/editor/field"` and wrap your input in
5
+ * `<Field {field}>...</Field>` to show its label, description and validation errors.
6
+ * Your input still needs its value, read-only state and change handler. The wrapper
7
+ * does not register the field or own form values; Ridu already does that.
8
+ */
9
+ export { default } from "./field.svelte";
@@ -0,0 +1,3 @@
1
+ export type { FieldBinding, FieldEditorProps, FieldEditorType, FieldEditorValue } from "./types";
2
+ export { defineFieldEditor } from "./registry";
3
+ export type { FieldEditorDefinition, RegisteredFieldEditor } from "./registry";
@@ -0,0 +1,174 @@
1
+ import { bindSchemaManifest } from "@riducms/protocol";
2
+ import { resolveBlockTypes } from "@riducms/protocol";
3
+ import type { Component } from "svelte";
4
+ import type { SchemaField, SchemaManifest } from "@riducms/protocol";
5
+
6
+ import type { FieldEditorProps, FieldEditorType } from "./types";
7
+ import { decodeComponentConfig } from "../component-config";
8
+
9
+ export const localEditorReference = /^app:[A-Za-z][A-Za-z0-9_]*$/;
10
+ const editorTypes = new Set<string>([
11
+ "text",
12
+ "textarea",
13
+ "email",
14
+ "date",
15
+ "code",
16
+ "number",
17
+ "checkbox",
18
+ "text-list",
19
+ "number-list",
20
+ ]);
21
+
22
+ /**
23
+ * A custom field input and its optional settings decoder. `type` must match
24
+ * the Go field's type. When Config is not undefined, `decodeConfig` is required;
25
+ * naming a config type alone does not check serialized settings.
26
+ */
27
+ export type FieldEditorDefinition<Type extends FieldEditorType, Config = undefined> = {
28
+ /** The existing field type to display differently, for example `"text"` or `"number"`. */
29
+ type: Type;
30
+ /** Svelte component accepting the matching `FieldEditorProps<Type, Config>`. */
31
+ component: Component<FieldEditorProps<NoInfer<Type>, NoInfer<Config>>>;
32
+ } & (0 extends 1 & Config
33
+ ? { decodeConfig: (value: unknown) => Config }
34
+ : [Config] extends [never]
35
+ ? { decodeConfig: (value: unknown) => Config }
36
+ : [Config] extends [undefined]
37
+ ? { decodeConfig?: (value: unknown) => Config }
38
+ : { decodeConfig: (value: unknown) => Config });
39
+
40
+ const registration = Symbol("ridu-field-editor");
41
+ const registeredEditors = new WeakSet<RegisteredFieldEditor>();
42
+ /** Registration returned by `defineFieldEditor`; store it in `defineAdmin({ fields: ... })`. */
43
+ export interface RegisteredFieldEditor {
44
+ readonly [registration]: true;
45
+ readonly type: FieldEditorType;
46
+ // Erasure must not advertise that a specific component accepts every value/config.
47
+ // Only the validated host render boundary may recover its props.
48
+ readonly component: Component<never>;
49
+ decode(field: SchemaField): unknown;
50
+ }
51
+
52
+ /**
53
+ * Register a Svelte component to replace a field input in your application.
54
+ * Supports text, textarea, email, date, code, number, checkbox, text-list and number-list fields.
55
+ *
56
+ * Put the result under an `app:name` key in `defineAdmin({ fields: ... })` and
57
+ * select that same key in Go with `field.Admin{Editor: field.Component("app:name")}`. The Go field keeps
58
+ * its original storage, validation and filtering behaviour.
59
+ *
60
+ * `type` and `component` are required. The component receives `FieldEditorProps`
61
+ * for that type: read `field.value`, update the unsaved form with `field.set`, and
62
+ * render labels/errors with the `Field` component from `@riducms/plugin/editor/field`.
63
+ * Ridu provides the value type, so this helper needs no `decodeValue` option.
64
+ *
65
+ * Add `decodeConfig` only when Go supplies settings through `field.Component`.
66
+ * It checks unknown JSON and returns the component's `config` prop synchronously;
67
+ * throw a useful error for invalid settings. With a decoder, Go must supply an
68
+ * object even for empty settings. Without one, omit the settings argument in Go.
69
+ * TypeScript infers the `Config` type from the decoder's return value.
70
+ *
71
+ * @param definition The existing field type, Svelte component, and optional settings decoder.
72
+ * @returns A frozen registration for `defineAdmin`'s `fields` map. This does not
73
+ * render the component, select it on a Go field, or change the stored value type.
74
+ * @throws If the type is unsupported or the component is invalid. `ridu check`
75
+ * also checks the selected Go field type and its settings before deployment.
76
+ * @example
77
+ * ```ts
78
+ * import { defineAdmin } from '@riducms/plugin/admin';
79
+ * import { defineFieldEditor } from '@riducms/plugin/editor';
80
+ * import { generatedAdminPlugins } from './ridu.plugins.generated';
81
+ * import TitleField from './components/title-field.svelte';
82
+ *
83
+ * export default defineAdmin({
84
+ * plugins: generatedAdminPlugins,
85
+ * fields: {
86
+ * 'app:titleCounter': defineFieldEditor({ type: 'text', component: TitleField })
87
+ * }
88
+ * });
89
+ * ```
90
+ */
91
+ export function defineFieldEditor<const Type extends FieldEditorType, Config = undefined>(
92
+ definition: FieldEditorDefinition<Type, Config>
93
+ ): RegisteredFieldEditor {
94
+ if (!editorTypes.has(definition.type) || typeof definition.component !== "function")
95
+ throw new Error("A field editor requires a supported field type and a Svelte component.");
96
+ const { type, component, decodeConfig } = definition;
97
+ const editor = Object.freeze({
98
+ [registration]: true as const,
99
+ type,
100
+ component,
101
+ decode(field: SchemaField): unknown {
102
+ const reference = field.admin.editor?.reference;
103
+ if (field.type !== type)
104
+ throw new Error(
105
+ `Editor ${reference} expects ${type}, but ${field.path} is ${field.type}. Change the Go editor selection or registration type.`
106
+ );
107
+ try {
108
+ return decodeComponentConfig(field.admin.editor, decodeConfig);
109
+ } catch (error) {
110
+ throw new Error(
111
+ `Editor ${reference} config for ${field.path} is invalid: ${error instanceof Error ? error.message : String(error)}. Fix the field component config in Go or the decoder in admin/src/admin.config.ts.`
112
+ );
113
+ }
114
+ },
115
+ });
116
+ registeredEditors.add(editor);
117
+ return editor;
118
+ }
119
+
120
+ export interface FieldEditorConfig {
121
+ /** Custom field inputs selected by Go `field.Admin{Editor: field.Component("app:name")}`. Use `defineFieldEditor` for each entry. */
122
+ fields?: Readonly<Record<`app:${string}`, RegisteredFieldEditor>>;
123
+ }
124
+
125
+ /** The application owns one static configuration; packaged plugins retain their pairing contracts. */
126
+ export function validateFieldEditorRegistrations(config: FieldEditorConfig): void {
127
+ for (const [reference, editor] of Object.entries(config.fields ?? {})) {
128
+ if (!localEditorReference.test(reference))
129
+ throw new Error(
130
+ `Invalid editor reference ${reference}; expected app:name in admin/src/admin.config.ts.`
131
+ );
132
+ if (!registeredEditors.has(editor))
133
+ throw new Error(`Editor ${reference} must use defineFieldEditor.`);
134
+ }
135
+ }
136
+
137
+ /** Uses the same executable registrations and decoders in the browser and build checks. */
138
+ export function validateAdminEditors(
139
+ config: FieldEditorConfig,
140
+ manifest: Pick<SchemaManifest, "collections" | "globals" | "blocks">
141
+ ): void {
142
+ validateFieldEditorRegistrations(config);
143
+ const failures: string[] = [];
144
+ bindSchemaManifest(manifest);
145
+ const inspect = (fields: readonly SchemaField[], owner: string) => {
146
+ for (const field of fields) {
147
+ const reference = field.admin.editor?.reference;
148
+ if (reference !== undefined) {
149
+ const editor = config.fields?.[reference as `app:${string}`];
150
+ try {
151
+ if (!localEditorReference.test(reference))
152
+ throw new Error(`Malformed editor reference ${reference}; expected app:name.`);
153
+ if (editor === undefined)
154
+ throw new Error(
155
+ `Editor ${reference} is not registered. Register it in admin/src/admin.config.ts or change the field component in Go.`
156
+ );
157
+ editor.decode(field);
158
+ } catch (error) {
159
+ failures.push(
160
+ `${owner}.${field.path}: ${error instanceof Error ? error.message : String(error)}`
161
+ );
162
+ }
163
+ }
164
+ for (const tree of field.plugin?.embeddedTrees ?? [])
165
+ for (const item of tree.cases)
166
+ for (const variant of resolveBlockTypes(item)) inspect(variant.fields, owner);
167
+ if (field.nested !== undefined) inspect(field.nested.fields, owner);
168
+ for (const block of resolveBlockTypes(field.blocks) ?? []) inspect(block.fields, owner);
169
+ }
170
+ };
171
+ for (const collection of manifest.collections) inspect(collection.fields, collection.slug);
172
+ for (const global of manifest.globals ?? []) inspect(global.fields, global.slug);
173
+ if (failures.length !== 0) throw new Error(failures.join("\n"));
174
+ }
@@ -0,0 +1,104 @@
1
+ import type { SchemaField, ValidationIssue } from "@riducms/protocol";
2
+ import type { FieldAuthoringHost } from "../authoring";
3
+ import type { FieldDocumentForm, FieldLiveValidation } from "../form";
4
+ import type { AdminI18n } from "../i18n";
5
+
6
+ /** Field types whose inputs you can replace with a custom component. Lists contain primitive strings or numbers; the list is one field. */
7
+ export type FieldEditorType =
8
+ | "text"
9
+ | "textarea"
10
+ | "email"
11
+ | "date"
12
+ | "code"
13
+ | "number"
14
+ | "checkbox"
15
+ | "text-list"
16
+ | "number-list";
17
+ /** The value your field component reads and writes. Date and code fields contain strings. */
18
+ export type FieldEditorValue<Type extends FieldEditorType> = Type extends "text-list"
19
+ ? string[]
20
+ : Type extends "number-list"
21
+ ? number[]
22
+ : Type extends "number"
23
+ ? number
24
+ : Type extends "checkbox"
25
+ ? boolean
26
+ : string;
27
+
28
+ /**
29
+ * The current field value, validation messages and input controls for your component.
30
+ * Ridu registers it and cleans it up. In arrays/blocks it follows the row's stable
31
+ * `_key` through reorder. Removing the row, closing the editor, or replacing the
32
+ * document, locale, schema or saved/reset values makes it stale. Old connections
33
+ * cannot edit a new row that happens to occupy the same position.
34
+ */
35
+ export interface FieldBinding<Type extends FieldEditorType = FieldEditorType> {
36
+ /**
37
+ * A copy of the resolved field definition, including its current path, label and rules.
38
+ * `schema.admin.readOnly` reflects field configuration and access, not a pending document save.
39
+ */
40
+ readonly schema: SchemaField & { type: Type };
41
+ /** Current form value, including unsaved edits; null/undefined mean empty or absent. */
42
+ readonly value: FieldEditorValue<Type> | null | undefined;
43
+ /** Current validation issues, including errors returned by a document save. */
44
+ readonly issues: readonly ValidationIssue[];
45
+ /** Opted-in server feedback, managed by the host; save validation stays independent. */
46
+ readonly liveValidation: FieldLiveValidation;
47
+ /** Whether changes are blocked right now, including during a document save. `set` checks this again when called. */
48
+ readonly readOnly: boolean;
49
+ /** True once the connection expires. Value becomes undefined, issues empty and writes throw. */
50
+ readonly stale: boolean;
51
+ /**
52
+ * ID, name and accessibility attributes for your input. Spread these onto the
53
+ * control, and handle `field.readOnly` and value changes separately.
54
+ * The optional Field wrapper supplies the matching label and error messages.
55
+ * Native `required` is false for boolean and list controls: a list requires
56
+ * entries, not nonempty text in every item. Ridu validates the container on save.
57
+ */
58
+ readonly inputProps: {
59
+ id: string;
60
+ name: string;
61
+ required: boolean;
62
+ "aria-invalid": boolean;
63
+ "aria-describedby": string | undefined;
64
+ "aria-errormessage": string | undefined;
65
+ };
66
+ /**
67
+ * Change the value in the unsaved form; `null` clears it. This does not save to
68
+ * the server. Throws if stale, read-only, or given the wrong value type. Lists are copied on reads and writes.
69
+ * Normal field validation and server authorization still apply on document save.
70
+ */
71
+ set(value: FieldEditorValue<Type> | null): void;
72
+ }
73
+
74
+ /**
75
+ * Props for a custom field input selected by Go `field.Admin{Editor: field.Component("app:name")}` and
76
+ * registered with `defineFieldEditor` in `admin/src/admin.config.ts`.
77
+ * Use `FieldEditorProps<"text", MyConfig>` for a text editor with decoded settings.
78
+ */
79
+ export type FieldEditorProps<Type extends FieldEditorType = FieldEditorType, Config = undefined> = {
80
+ /** Read the current value and update it with `field.set(...)`. */
81
+ field: FieldBinding<Type>;
82
+ /** Interface translations and formatting using the admin's language/preferences. */
83
+ i18n: AdminI18n;
84
+ /**
85
+ * Read other values in this unsaved document form. These methods return copies
86
+ * and expire with the editor. Local editors update only their own `field`;
87
+ * this form interface does not expose `bind` for writing another field.
88
+ */
89
+ form: Pick<FieldDocumentForm, "contentLocale" | "resource" | "get" | "issuesFor"> &
90
+ Partial<Pick<FieldDocumentForm, "snapshot">>;
91
+ /**
92
+ * Browse or fetch related documents. These are the local editor's available tools;
93
+ * a local editor mounted in an embedded form reads and writes that form’s detached payload.
94
+ * The containing paired plugin owns Apply/Cancel and Go plugin requests.
95
+ */
96
+ authoring: Pick<
97
+ FieldAuthoringHost,
98
+ "collections" | "documentRevision" | "referenceBrowser" | "findDocument"
99
+ > & { readonly locale: string | undefined };
100
+ } & ([Config] extends [undefined]
101
+ ? { config?: never }
102
+ : {
103
+ /** Go settings validated by the registration's decoder before rendering. */ config: Config;
104
+ });