@riducms/plugin 0.2.3 → 0.4.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.
@@ -0,0 +1,145 @@
1
+ import type { SchemaCollection, SchemaGlobal } from "@riducms/protocol";
2
+ import type { Component, Snippet } from "svelte";
3
+
4
+ import type { FieldDocument } from "../authoring";
5
+ import type { AdminI18n, ExtensionTranslationKey } from "../i18n";
6
+ import type {
7
+ AdminExtensionNotificationTone,
8
+ AdminExtensionProps,
9
+ AdminViewComponent,
10
+ } from "./shared";
11
+
12
+ /** Sign-in operations provided to a custom login screen. */
13
+ export interface AdminLoginExtensionHost {
14
+ /** Sign in, check admin access and enter the admin. Rejects if authentication/access fails. */
15
+ login: (credentials: { email: string; password: string }) => Promise<void>;
16
+ /** Show a temporary success or error message. Does not perform an operation itself. */
17
+ notify: (tone: AdminExtensionNotificationTone, title: string, message?: string) => void;
18
+ }
19
+
20
+ export interface AdminLoginComponentProps extends AdminExtensionProps {
21
+ /** Render with `{@render defaultView()}` to keep Ridu's sign-in screen inside your wrapper. */
22
+ defaultView: Snippet;
23
+ host: AdminLoginExtensionHost;
24
+ }
25
+
26
+ /** Add content before/after sign-in, or replace it with a custom screen. */
27
+ export interface AdminLoginComponent {
28
+ key: string;
29
+ component: Component<AdminLoginComponentProps>;
30
+ /** Defaults to after. Only one replace entry is allowed across plugins and the application. */
31
+ position?: "before" | "after" | "replace";
32
+ }
33
+
34
+ export type AdminAccountSurface = "profile" | "security";
35
+
36
+ /** Operations for keeping a custom account screen in sync with the signed-in session. */
37
+ export interface AdminAccountExtensionHost {
38
+ /** Reloads the signed-in document and updates the shell identity. */
39
+ refreshUser: () => Promise<FieldDocument | undefined>;
40
+ /** Ends the current session and returns to sign-in. */
41
+ logout: () => Promise<void>;
42
+ /** Show a temporary success or error message. */
43
+ notify: (tone: AdminExtensionNotificationTone, title: string, message?: string) => void;
44
+ }
45
+
46
+ export interface AdminAccountComponentProps extends AdminExtensionProps {
47
+ surface: AdminAccountSurface;
48
+ /** Render with `{@render defaultView()}` to keep Ridu's account screen inside your wrapper. */
49
+ defaultView: Snippet;
50
+ host: AdminAccountExtensionHost;
51
+ }
52
+
53
+ /** Add to or replace the profile/security screen identified by `surface`. */
54
+ export interface AdminAccountComponent {
55
+ key: string;
56
+ surface: AdminAccountSurface;
57
+ component: Component<AdminAccountComponentProps>;
58
+ /** Defaults to after. Each account surface permits at most one replacement. */
59
+ position?: "before" | "after" | "replace";
60
+ }
61
+
62
+ export type AdminCoreViewSurface =
63
+ "collectionList" | "collectionCreate" | "collectionEdit" | "global" | "notFound";
64
+
65
+ /** Refresh tools for a custom collection/global screen. They do not save documents. */
66
+ export interface AdminCoreViewHost {
67
+ /** Fetch the current Go schema again and update the admin's schema-dependent UI. */
68
+ refreshManifest: () => Promise<void>;
69
+ /** Notify Ridu that your code changed documents so dependent lists/lookups can refresh. */
70
+ documentsChanged: () => void;
71
+ /** Show a temporary success or error message. */
72
+ notify: (tone: AdminExtensionNotificationTone, title: string, message?: string) => void;
73
+ }
74
+
75
+ export interface AdminCoreViewProps extends AdminExtensionProps {
76
+ surface: AdminCoreViewSurface;
77
+ collection?: SchemaCollection;
78
+ global?: SchemaGlobal;
79
+ documentID?: string;
80
+ /** Render with `{@render defaultView()}` to wrap the normal screen rather than rebuilding it. */
81
+ defaultView: Snippet;
82
+ host: AdminCoreViewHost;
83
+ }
84
+
85
+ interface AdminCollectionCoreView {
86
+ key: string;
87
+ surface: "collectionList" | "collectionCreate" | "collectionEdit";
88
+ /** Omit to replace this surface for every collection. */
89
+ collection?: string;
90
+ }
91
+
92
+ interface AdminGlobalCoreView {
93
+ key: string;
94
+ surface: "global";
95
+ /** Omit to replace this surface for every global. */
96
+ global?: string;
97
+ }
98
+
99
+ interface AdminNotFoundCoreView {
100
+ key: string;
101
+ surface: "notFound";
102
+ }
103
+
104
+ /**
105
+ * Replace a collection/global/not-found screen. A resource-specific registration
106
+ * wins over an all-resources fallback for that surface; duplicate targets fail.
107
+ * The supplied `defaultView` lets your component wrap the existing screen.
108
+ */
109
+ export type AdminCoreView = (
110
+ AdminCollectionCoreView | AdminGlobalCoreView | AdminNotFoundCoreView
111
+ ) &
112
+ AdminViewComponent<AdminCoreViewProps>;
113
+
114
+ export interface AdminDashboardPanelProps {
115
+ manifest: AdminExtensionProps["manifest"];
116
+ user?: FieldDocument;
117
+ i18n: AdminI18n;
118
+ }
119
+
120
+ /** Add dashboard content before/after Ridu's overview, or replace that overview. */
121
+ interface AdminDashboardPanelRegistration {
122
+ /** Unique name among dashboard entries; duplicates throw. Array order controls display order. */
123
+ key: string;
124
+ /** Defaults to after. Replace hides Ridu's overview and may only be registered once. */
125
+ position?: "before" | "after" | "replace";
126
+ }
127
+
128
+ export type AdminDashboardPanel = AdminDashboardPanelRegistration &
129
+ AdminViewComponent<AdminDashboardPanelProps>;
130
+
131
+ export interface AdminRouteNavigation {
132
+ /** Human-readable navigation label inside the authenticated admin shell. */
133
+ label: string;
134
+ labelKey?: ExtensionTranslationKey;
135
+ /** Optional grouping hint reserved for shells that render grouped plugin navigation. */
136
+ group?: string;
137
+ }
138
+
139
+ /** One statically bundled route mounted beneath the authenticated admin layout. */
140
+ export type AdminRoute = {
141
+ /** Relative admin path, without a leading slash. Paired plugins must match their Go descriptor. */
142
+ path: string;
143
+ /** Optional navigation entry; omit it for a route reachable only by links or redirects. */
144
+ navigation?: AdminRouteNavigation;
145
+ } & AdminViewComponent<AdminExtensionProps>;
package/src/admin.ts CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  /**
28
28
  * Settings for your application's admin, exported from `admin/src/admin.config.ts`.
29
29
  * Register your custom components here. Keep `generatedAdminPlugins` in `plugins`
30
- * to load installed plugins such as rich text. Use `fields` to replace inputs for
30
+ * to load installed plugins such as rich text. Use `fieldEditors` to replace inputs for
31
31
  * text, textarea, email, date, code, number and checkbox fields.
32
32
  */
33
33
  export interface AdminConfig extends AdminContributions, FieldEditorConfig {
@@ -46,8 +46,8 @@ export interface AdminConfig extends AdminContributions, FieldEditorConfig {
46
46
  *
47
47
  * Export the result as the default export of `admin/src/admin.config.ts`. Keep
48
48
  * `plugins: generatedAdminPlugins` to load installed packages such as rich text.
49
- * Add `fields` for inputs made with `defineFieldEditor`, `rowLabels` for headings
50
- * made with `defineRowLabel`, or options such as `dashboard`, `routes`, and
49
+ * Add `fieldEditors` for inputs made with `defineFieldEditor`, `rowLabels` for headings
50
+ * made with `defineRowLabel`, or options such as `dashboardPanels`, `routes`, and
51
51
  * `documentActions` for other parts of the admin. All options are optional.
52
52
  *
53
53
  * Application field and row-label keys use `app:name`; select the same key in Go
@@ -70,7 +70,7 @@ export interface AdminConfig extends AdminContributions, FieldEditorConfig {
70
70
  *
71
71
  * export default defineAdmin({
72
72
  * plugins: generatedAdminPlugins,
73
- * dashboard: [{ key: 'welcome', component: WelcomePanel, position: 'before' }]
73
+ * dashboardPanels: [{ key: 'welcome', component: WelcomePanel, position: 'before' }]
74
74
  * });
75
75
  * ```
76
76
  */
@@ -81,8 +81,8 @@ export function defineAdmin(config: AdminConfig): AdminConfig {
81
81
  /** Static configuration owned by one mounted admin. Changes require remount/HMR. */
82
82
  export interface ResolvedAdminConfig {
83
83
  readonly plugins: readonly AdminPlugin[];
84
- readonly fields: readonly ResolvedPluginField[];
85
- readonly editors: NonNullable<AdminConfig["fields"]>;
84
+ readonly pluginFields: readonly ResolvedPluginField[];
85
+ readonly fieldEditors: NonNullable<AdminConfig["fieldEditors"]>;
86
86
  readonly rowLabels: NonNullable<AdminConfig["rowLabels"]>;
87
87
  readonly languages: readonly TranslationLanguage[];
88
88
  readonly extensions: Readonly<ResolvedAdminExtensions>;
@@ -102,8 +102,8 @@ export function resolveAdminConfig(config: AdminConfig = {}): ResolvedAdminConfi
102
102
  const plugins = Object.freeze([...(config.plugins ?? [])]);
103
103
  return Object.freeze({
104
104
  plugins,
105
- fields: resolvePluginFields(plugins),
106
- editors: Object.freeze({ ...config.fields }),
105
+ pluginFields: resolvePluginFields(plugins),
106
+ fieldEditors: Object.freeze({ ...config.fieldEditors }),
107
107
  rowLabels: Object.freeze({ ...config.rowLabels }),
108
108
  languages: Object.freeze(config.languages?.length ? [...config.languages] : [en]),
109
109
  extensions: Object.freeze(resolveAdminExtensions(plugins, config)),
@@ -120,22 +120,22 @@ export function resolveAdminConfig(config: AdminConfig = {}): ResolvedAdminConfi
120
120
  export function validateAdminManifest(
121
121
  config: ResolvedAdminConfig,
122
122
  manifest: Pick<SchemaManifest, "collections" | "globals" | "blocks"> &
123
- Partial<Pick<SchemaManifest, "plugins">>,
123
+ Partial<Pick<SchemaManifest, "plugins" | "application">>,
124
124
  options: { completeManifest?: boolean } = {}
125
125
  ): void {
126
126
  validateManifestPluginPairs(
127
127
  config.plugins,
128
- config.fields,
128
+ config.pluginFields,
129
129
  manifest,
130
130
  options.completeManifest === true
131
131
  );
132
- const validatePluginField = createPluginFieldValidator(config.fields);
132
+ const validatePluginField = createPluginFieldValidator(config.pluginFields);
133
133
  const failures: string[] = [];
134
134
  bindSchemaManifest(manifest);
135
135
  const inspect = (fields: readonly SchemaField[], owner: string) => {
136
136
  for (const field of fields) {
137
137
  try {
138
- validateFieldEditorSelection(config.editors, field);
138
+ validateFieldEditorSelection(config.fieldEditors, field);
139
139
  validatePluginField(field);
140
140
  const selection = field.nested?.rowLabelComponent;
141
141
  if (selection?.reference !== undefined) {
@@ -174,14 +174,46 @@ export function validateAdminManifest(
174
174
  const collections = new Map(manifest.collections.map((item) => [item.slug, item]));
175
175
  const globals = new Set((manifest.globals ?? []).map((item) => item.slug));
176
176
  const extensions = config.extensions;
177
- for (const cell of extensions.listCells) {
177
+
178
+ // Loader keys may be reused by several views, but every reference must carry the
179
+ // exact generated contract for the compiled Go loader. This rejects stale or
180
+ // hand-authored TypeScript contracts before the view can request data.
181
+ for (const view of [
182
+ ...extensions.dashboardPanels,
183
+ ...extensions.routes,
184
+ ...extensions.coreViews,
185
+ ]) {
186
+ if (view.loader === undefined) continue;
187
+ const contract = manifest.application?.adminLoaders?.find(
188
+ (loader) => loader.key === view.loader.key
189
+ );
190
+ if (contract === undefined || JSON.stringify(contract) !== JSON.stringify(view.loader.contract))
191
+ throw new Error(
192
+ `Admin view ${"key" in view ? view.key : view.path} requires a matching generated Go loader; run ridu generate.`
193
+ );
194
+ }
195
+ for (const results of extensions.listResultsRenderers) {
196
+ if (!collections.has(results.collection))
197
+ throw new Error(
198
+ `Admin list results ${results.key} selects unknown collection ${results.collection}.`
199
+ );
200
+ if (
201
+ extensions.coreViews.some(
202
+ (view) =>
203
+ view.surface === "collectionList" &&
204
+ (view.collection === undefined || view.collection === results.collection)
205
+ )
206
+ )
207
+ throw new Error(`Admin list results ${results.key} targets a replaced collection view.`);
208
+ }
209
+ for (const cell of extensions.listCellRenderers) {
178
210
  const collection = collections.get(cell.collection);
179
211
  if (!collection?.fields.some((field) => field.name === cell.field || field.path === cell.field))
180
212
  throw new Error(
181
213
  `Admin list cell ${cell.key} selects unknown field ${cell.collection}.${cell.field}.`
182
214
  );
183
215
  }
184
- for (const view of extensions.views) {
216
+ for (const view of extensions.coreViews) {
185
217
  if ("collection" in view && view.collection !== undefined && !collections.has(view.collection))
186
218
  throw new Error(`Admin core view ${view.key} selects unknown collection ${view.collection}.`);
187
219
  if ("global" in view && view.global !== undefined && !globals.has(view.global))
@@ -26,7 +26,7 @@ const authoringAPIVersion = 1;
26
26
  *
27
27
  * Use `fields` for the default editors of new Go field types. Each map key matches
28
28
  * a Go `PluginFieldType.Key`, and each value is returned by `definePluginField`.
29
- * Use `components` for alternative editors made with `defineFieldComponent`,
29
+ * Use `fieldEditors` for alternative editors made with `defineFieldComponent`,
30
30
  * selected explicitly in Go with `field.PluginComponent`. The other options add
31
31
  * pages, dashboard panels, navigation, and the UI described by `AdminContributions`.
32
32
  *
@@ -60,9 +60,9 @@ export function defineAdminPlugin<const Plugin extends Omit<AdminPlugin, "apiVer
60
60
  const result = Object.freeze({
61
61
  ...plugin,
62
62
  ...(plugin.fields === undefined ? {} : { fields: Object.freeze({ ...plugin.fields }) }),
63
- ...(plugin.components === undefined
63
+ ...(plugin.fieldEditors === undefined
64
64
  ? {}
65
- : { components: Object.freeze({ ...plugin.components }) }),
65
+ : { fieldEditors: Object.freeze({ ...plugin.fieldEditors }) }),
66
66
  apiVersion: authoringAPIVersion,
67
67
  });
68
68
  const definition: Omit<AdminPlugin, "apiVersion"> = plugin;
package/src/authoring.ts CHANGED
@@ -41,6 +41,11 @@ export interface FieldReferenceBrowserProps {
41
41
  field: SchemaField;
42
42
  /** The target collection's resolved schema, available from `authoring.collections`. */
43
43
  collection: SchemaCollection;
44
+ /**
45
+ * Optional collection choices. Only readable collections are offered; switching clears the
46
+ * current list query and uncommitted selection.
47
+ */
48
+ collections?: readonly SchemaCollection[];
44
49
  /** Allow multiple selections when true; otherwise select at most one document. */
45
50
  hasMany: boolean;
46
51
  /** IDs currently selected by your editor. */
@@ -51,8 +56,20 @@ export interface FieldReferenceBrowserProps {
51
56
  initialDocument?: FieldDocument;
52
57
  /** Fetch and open this document when `initialDocument` is not supplied. */
53
58
  initialDocumentID?: string;
54
- /** Limit selectable documents; the server still applies its own access rules. */
55
- optionFilter?: FieldReferenceFilter | readonly FieldReferenceFilter[];
59
+ /** Open a new document form immediately when creation is permitted. */
60
+ initialCreate?: boolean;
61
+ /** File dropped into an upload field; opens a new metadata form before any upload is saved. */
62
+ initialFile?: File;
63
+ /**
64
+ * Limit selectable documents; the server still applies its own access rules. A static filter
65
+ * applies to every collection. Use a resolver when collection schemas need different fields.
66
+ */
67
+ optionFilter?:
68
+ | FieldReferenceFilter
69
+ | readonly FieldReferenceFilter[]
70
+ | ((
71
+ collection: SchemaCollection
72
+ ) => FieldReferenceFilter | readonly FieldReferenceFilter[] | undefined);
56
73
  /** Initial values for a new related document, not changes to an existing document. */
57
74
  defaultValues?: Readonly<Record<string, unknown>>;
58
75
  /** Allow creation when access permits it. Set false to hide creation. */
@@ -60,12 +77,12 @@ export interface FieldReferenceBrowserProps {
60
77
  /** Content locale for loading and editing related documents. */
61
78
  locale?: string;
62
79
  /**
63
- * Called when the user confirms the selection. Update your field here.
80
+ * Called with selected IDs and their collection slug. Update your field here.
64
81
  * Return false (or resolve to false) to keep the browser open. Returning void or
65
82
  * true accepts the selection and closes it while editing remains allowed. A thrown
66
83
  * error/rejected promise keeps it open and Ridu displays an error notification.
67
84
  */
68
- onCommit: (ids: string[]) => void | boolean | Promise<void | boolean>;
85
+ onCommit: (ids: string[], collectionSlug: string) => void | boolean | Promise<void | boolean>;
69
86
  /** Called when the browser closes; update your open/closed UI state here. */
70
87
  onClose: () => void;
71
88
  }
@@ -206,6 +223,12 @@ export interface FieldAuthoringHost {
206
223
  readonly documentRevision: number;
207
224
  /** The content locale being edited, separate from the admin interface language. */
208
225
  readonly locale?: string;
226
+ /**
227
+ * Whether the current admin session may create documents in this collection.
228
+ * This is a presentation hint for create controls, not authorization. Access is
229
+ * checked again by the reference browser and operation engine when work is saved.
230
+ */
231
+ canCreateDocument(collection: string): boolean;
209
232
  /** Related-document picker/editor component. Its `onCommit` handler updates your field. */
210
233
  referenceBrowser: Component<FieldReferenceBrowserProps>;
211
234
  /**
@@ -37,7 +37,7 @@ export type FieldEditorDefinition<Type extends FieldEditorType, Config = undefin
37
37
 
38
38
  const registration = Symbol("ridu-field-editor");
39
39
  const registeredEditors = new WeakSet<RegisteredFieldEditor>();
40
- /** Registration returned by `defineFieldEditor`; store it in `defineAdmin({ fields: ... })`. */
40
+ /** Registration returned by `defineFieldEditor`; store it in `defineAdmin({ fieldEditors: ... })`. */
41
41
  export interface RegisteredFieldEditor {
42
42
  readonly [registration]: true;
43
43
  readonly type: FieldEditorType;
@@ -51,7 +51,7 @@ export interface RegisteredFieldEditor {
51
51
  * Register a Svelte component to replace a field input in your application.
52
52
  * Supports text, textarea, email, date, code, number, checkbox, text-list and number-list fields.
53
53
  *
54
- * Put the result under an `app:name` key in `defineAdmin({ fields: ... })` and
54
+ * Put the result under an `app:name` key in `defineAdmin({ fieldEditors: ... })` and
55
55
  * select that same key in Go with `field.Admin{Editor: field.Component("app:name")}`. The Go field keeps
56
56
  * its original storage, validation and filtering behaviour.
57
57
  *
@@ -67,7 +67,7 @@ export interface RegisteredFieldEditor {
67
67
  * TypeScript infers the `Config` type from the decoder's return value.
68
68
  *
69
69
  * @param definition The existing field type, Svelte component, and optional settings decoder.
70
- * @returns A frozen registration for `defineAdmin`'s `fields` map. This does not
70
+ * @returns A frozen registration for `defineAdmin`'s `fieldEditors` map. This does not
71
71
  * render the component, select it on a Go field, or change the stored value type.
72
72
  * @throws If the type is unsupported or the component is invalid. `ridu check`
73
73
  * also checks the selected Go field type and its settings before deployment.
@@ -80,7 +80,7 @@ export interface RegisteredFieldEditor {
80
80
  *
81
81
  * export default defineAdmin({
82
82
  * plugins: generatedAdminPlugins,
83
- * fields: {
83
+ * fieldEditors: {
84
84
  * 'app:titleCounter': defineFieldEditor({ type: 'text', component: TitleField })
85
85
  * }
86
86
  * });
@@ -117,12 +117,12 @@ export function defineFieldEditor<const Type extends FieldEditorType, Config = u
117
117
 
118
118
  export interface FieldEditorConfig {
119
119
  /** Custom field inputs selected by Go `field.Admin{Editor: field.Component("app:name")}`. Use `defineFieldEditor` for each entry. */
120
- fields?: Readonly<Record<`app:${string}`, RegisteredFieldEditor>>;
120
+ fieldEditors?: Readonly<Record<`app:${string}`, RegisteredFieldEditor>>;
121
121
  }
122
122
 
123
123
  /** The application owns one static configuration; packaged plugins retain their pairing contracts. */
124
124
  export function validateFieldEditorRegistrations(config: FieldEditorConfig): void {
125
- for (const [reference, editor] of Object.entries(config.fields ?? {})) {
125
+ for (const [reference, editor] of Object.entries(config.fieldEditors ?? {})) {
126
126
  if (!localEditorReference.test(reference))
127
127
  throw new Error(
128
128
  `Invalid editor reference ${reference}; expected app:name in admin/src/admin.config.ts.`
@@ -134,14 +134,14 @@ export function validateFieldEditorRegistrations(config: FieldEditorConfig): voi
134
134
 
135
135
  /** Check one local selection using the same decoder as the mounted editor. */
136
136
  export function validateFieldEditorSelection(
137
- editors: NonNullable<FieldEditorConfig["fields"]>,
137
+ fieldEditors: NonNullable<FieldEditorConfig["fieldEditors"]>,
138
138
  field: SchemaField
139
139
  ): void {
140
140
  const reference = field.admin.editor?.reference;
141
141
  if (reference === undefined) return;
142
142
  if (!localEditorReference.test(reference))
143
143
  throw new Error(`Malformed editor reference ${reference}; expected app:name.`);
144
- const editor = editors[reference as `app:${string}`];
144
+ const editor = fieldEditors[reference as `app:${string}`];
145
145
  if (editor === undefined)
146
146
  throw new Error(
147
147
  `Editor ${reference} is not registered. Register it in admin/src/admin.config.ts or change the field component in Go.`
@@ -95,7 +95,7 @@ export type FieldEditorProps<Type extends FieldEditorType = FieldEditorType, Con
95
95
  */
96
96
  authoring: Pick<
97
97
  FieldAuthoringHost,
98
- "collections" | "documentRevision" | "referenceBrowser" | "findDocument"
98
+ "collections" | "documentRevision" | "canCreateDocument" | "referenceBrowser" | "findDocument"
99
99
  > & { readonly locale: string | undefined };
100
100
  } & ([Config] extends [undefined]
101
101
  ? { config?: never }
package/src/field.ts CHANGED
@@ -299,7 +299,7 @@ type NamedFieldTarget<Type extends FieldType, Key extends string> = Type extends
299
299
  /**
300
300
  * Register a plugin component that can replace an existing field's editor.
301
301
  *
302
- * Put the result in `defineAdminPlugin({ components: { colorSwatch: ... } })`.
302
+ * Put the result in `defineAdminPlugin({ fieldEditors: { colorSwatch: ... } })`.
303
303
  * Go selects that name through `field.PluginComponent(pluginKey, "colorSwatch")`
304
304
  * in the field's `Admin.Editor` setting. This changes the input while keeping
305
305
  * the original Go field's storage, validation, permissions, and generated types.
@@ -319,7 +319,7 @@ type NamedFieldTarget<Type extends FieldType, Key extends string> = Type extends
319
319
  * errors for invalid input. Go still validates the document when it is saved.
320
320
  *
321
321
  * @param definition The existing field type, component, and value/settings decoders.
322
- * @returns A frozen registration for the plugin's `components` map. It preserves
322
+ * @returns A frozen registration for the plugin's `fieldEditors` map. It preserves
323
323
  * the selected field type and inferred value/input types; it does not create a new field type.
324
324
  * @throws If the field type, component, decoders, or plugin field-type selection are invalid.
325
325
  * @example
@@ -330,7 +330,7 @@ type NamedFieldTarget<Type extends FieldType, Key extends string> = Type extends
330
330
  * export const editorialAdminPlugin = defineAdminPlugin({
331
331
  * key: 'editorial-tools',
332
332
  * pairingVersion: 1,
333
- * components: {
333
+ * fieldEditors: {
334
334
  * colorSwatch: defineFieldComponent({
335
335
  * type: 'text',
336
336
  * component: ColorSwatch,
@@ -359,7 +359,7 @@ export function defineFieldComponent<
359
359
  }
360
360
  ): PluginFieldRegistration<Value, Input, Type> & NamedFieldTarget<Type, Key>;
361
361
  /**
362
- * Name an alternative editor in your paired plugin's `components` map. Go selects
362
+ * Name an alternative editor in your paired plugin's `fieldEditors` map. Go selects
363
363
  * it with `.Admin(field.Admin{Editor: field.PluginComponent(pluginKey, componentName, config)})`. Set `type` to
364
364
  * the actual field type; also supply `fieldType` for plugin values. `decodeValue`
365
365
  * checks reads/writes and `decodeConfig` checks settings, synchronously. Changing
@@ -379,7 +379,7 @@ export function defineFieldComponent<
379
379
  }
380
380
  ): PluginFieldRegistration<Value, Value, Type> & NamedFieldTarget<Type, Key>;
381
381
  /**
382
- * Name an alternative editor without settings in your plugin's `components` map.
382
+ * Name an alternative editor without settings in your plugin's `fieldEditors` map.
383
383
  * Go selects it with `field.Admin.Editor`. Set the actual schema `type`, plus
384
384
  * `fieldType` for plugin values. Supply synchronous `decodeValue` and `decodeInput`
385
385
  * for different saved/write shapes. Omit the Go settings argument; no config prop is
@@ -400,7 +400,7 @@ export function defineFieldComponent<
400
400
  }
401
401
  ): PluginFieldRegistration<Value, Input, Type> & NamedFieldTarget<Type, Key>;
402
402
  /**
403
- * Name an alternative editor without settings in your plugin's `components` map.
403
+ * Name an alternative editor without settings in your plugin's `fieldEditors` map.
404
404
  * Go selects it with `field.Admin.Editor`. Set the actual schema `type`, plus
405
405
  * `fieldType` for plugin values. `decodeValue` synchronously checks reads/writes.
406
406
  * Omit the Go settings argument; no config prop is passed. Any supplied component
@@ -463,10 +463,10 @@ function createRegistration(
463
463
  blocks: true,
464
464
  plugin: true,
465
465
  };
466
- if (!Object.hasOwn(supported, type)) throw new Error(`Unsupported field renderer type ${type}.`);
466
+ if (!Object.hasOwn(supported, type)) throw new Error(`Unsupported field editor type ${type}.`);
467
467
  if ("canRender" in definition || "key" in definition || "componentKey" in definition)
468
468
  throw new Error(
469
- "Renderer matching comes from its keyed registration; key/canRender/componentKey are not supported."
469
+ "Field editor matching comes from its keyed registration; key/canRender/componentKey are not supported."
470
470
  );
471
471
  if (
472
472
  typeof definition.component !== "function" ||
package/src/index.ts CHANGED
@@ -42,6 +42,9 @@ export {
42
42
  type AdminContributions,
43
43
  type AdminDashboardPanel,
44
44
  type AdminDashboardPanelProps,
45
+ type AdminLoaderProps,
46
+ type AdminViewComponent,
47
+ withAdminLoader,
45
48
  type AdminExtensionProps,
46
49
  type AdminLoginComponent,
47
50
  type AdminLoginComponentProps,
@@ -63,9 +66,9 @@ export {
63
66
  type AdminBrandComponent,
64
67
  type AdminBrandComponentProps,
65
68
  type AdminBrandSurface,
66
- type AdminShellComponent,
67
- type AdminShellComponentProps,
68
- type AdminShellPosition,
69
+ type AdminShellSlot,
70
+ type AdminShellSlotProps,
71
+ type AdminShellSlotPosition,
69
72
  type AdminProvider,
70
73
  type AdminProviderProps,
71
74
  type AdminDocumentAction,
@@ -73,8 +76,11 @@ export {
73
76
  type AdminDocumentExtensionProps,
74
77
  type AdminDocumentView,
75
78
  type AdminExtensionNotificationTone,
76
- type AdminListCell,
77
- type AdminListCellProps,
79
+ type AdminListCellRenderer,
80
+ type AdminListCellRendererProps,
81
+ type AdminCollectionList,
82
+ type AdminListResultsRenderer,
83
+ type AdminListResultsRendererProps,
78
84
  type AdminRouteNavigation,
79
85
  type AdminPluginPair,
80
86
  type AdminRoute,
@@ -8,14 +8,14 @@ export interface ResolvedPluginField {
8
8
  readonly owner: string;
9
9
  /** Field-type key for a default editor, or component name for an explicitly selected editor. */
10
10
  readonly key: string;
11
- /** Present only for editors registered in `components` and selected by Go `AdminComponent`. */
11
+ /** Present only for editors registered in `fieldEditors` and selected by Go `AdminComponent`. */
12
12
  readonly componentKey?: string;
13
13
  readonly registration: RegisteredPluginField;
14
14
  }
15
15
 
16
16
  /**
17
17
  * Collect field registrations, checking plugin IDs, field-type ownership, named
18
- * components and API versions. Throws on malformed/duplicate registrations.
18
+ * field editors and API versions. Throws on malformed/duplicate registrations.
19
19
  * Used by Ridu's runtime/check tooling; authors normally call the registration
20
20
  * helpers and retain their generated plugin list instead of invoking this directly.
21
21
  */
@@ -37,7 +37,7 @@ export function resolvePluginFields(
37
37
  owners.add(plugin.key);
38
38
  for (const [kind, entries] of [
39
39
  ["fields", plugin.fields],
40
- ["components", plugin.components],
40
+ ["fieldEditors", plugin.fieldEditors],
41
41
  ] as const) {
42
42
  if (entries === undefined) continue;
43
43
  if (
@@ -63,15 +63,14 @@ export function resolvePluginFields(
63
63
  )
64
64
  throw new Error(`Plugin ${plugin.key} ${kind}.${key} uses the wrong registration kind.`);
65
65
  const identity = kind === "fields" ? `field:${key}` : `component:${plugin.key}:${key}`;
66
- if (identities.has(identity))
67
- throw new Error(`Duplicate admin field renderer ${identity}.`);
66
+ if (identities.has(identity)) throw new Error(`Duplicate admin field editor ${identity}.`);
68
67
  identities.add(identity);
69
68
  resolved.push(
70
69
  Object.freeze({
71
70
  owner: plugin.key,
72
71
  key,
73
72
  registration,
74
- ...(kind === "components" ? { componentKey: key } : {}),
73
+ ...(kind === "fieldEditors" ? { componentKey: key } : {}),
75
74
  })
76
75
  );
77
76
  }
@@ -89,7 +88,7 @@ export function validateManifestPluginPairs(
89
88
  complete: boolean
90
89
  ): void {
91
90
  // Startup may receive only public or access-filtered schemas. Build/check own
92
- // completeness; selected fields below still validate their exact renderer.
91
+ // completeness; selected fields below still validate their exact editor.
93
92
  if (complete && manifest.plugins !== undefined)
94
93
  for (const item of registrations) {
95
94
  if (
@@ -99,7 +98,7 @@ export function validateManifestPluginPairs(
99
98
  )
100
99
  )
101
100
  throw new Error(
102
- `Named renderer ${item.owner}:${item.key} selects undeclared field type ${item.registration.fieldType}.`
101
+ `Named field editor ${item.owner}:${item.key} selects undeclared field type ${item.registration.fieldType}.`
103
102
  );
104
103
  }
105
104
  if (manifest.plugins !== undefined) {
@@ -136,7 +135,7 @@ export function createPluginFieldValidator(registrations: readonly ResolvedPlugi
136
135
  const fields = new Map(
137
136
  registrations.filter((item) => item.componentKey === undefined).map((item) => [item.key, item])
138
137
  );
139
- const components = new Map(
138
+ const fieldEditors = new Map(
140
139
  registrations
141
140
  .filter((item) => item.componentKey !== undefined)
142
141
  .map((item) => [`${item.owner}:${item.key}`, item])
@@ -145,14 +144,14 @@ export function createPluginFieldValidator(registrations: readonly ResolvedPlugi
145
144
  const selection = field.admin.component;
146
145
  const selected =
147
146
  selection !== undefined
148
- ? components.get(`${selection.plugin}:${selection.component}`)
147
+ ? fieldEditors.get(`${selection.plugin}:${selection.component}`)
149
148
  : field.type === "plugin"
150
149
  ? fields.get(field.plugin?.key ?? "")
151
150
  : undefined;
152
151
  if (selection !== undefined || field.type === "plugin") {
153
152
  if (selected === undefined)
154
153
  throw new Error(
155
- `Missing renderer for ${selection !== undefined ? `${selection.plugin}:${selection.component}` : field.plugin?.key}.`
154
+ `Missing field editor for ${selection !== undefined ? `${selection.plugin}:${selection.component}` : field.plugin?.key}.`
156
155
  );
157
156
  selected.registration.decodeConfig(field);
158
157
  }