@riducms/plugin 0.2.3 → 0.3.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/README.md CHANGED
@@ -7,14 +7,22 @@ the browser, without a JavaScript server in production.
7
7
 
8
8
  ## Choose the API for your job
9
9
 
10
- | Job | Import | Register/select it |
11
- | --- | --- | --- |
12
- | Add a field type with its own Go rules and structured value | `defineAdminPlugin`, `definePluginField` from `@riducms/plugin/authoring/v1` | Plugin `fields` map; matching field-type key declared in Go |
13
- | Supply an alternative advanced field editor in a paired plugin | `defineFieldComponent` from `@riducms/plugin/authoring/v1` | Plugin `components` map; Go `.Admin(field.Admin{Editor: field.PluginComponent(owner, name, config)})` |
14
- | Change how one application's text/number/checkbox-style field looks | `defineFieldEditor` from `@riducms/plugin/editor` | `defineAdmin({ fields })`; Go `.Admin(field.Admin{Editor: field.Component("app:name", config)})` |
15
- | Add application routes, dashboard panels or other admin UI | `defineAdmin` from `@riducms/plugin/admin` | `admin/src/admin.config.ts` |
16
- | Customize an application's array/block row headings | `defineRowLabel` from `@riducms/plugin/admin` | `defineAdmin({ rowLabels })`; Go `.Admin(field.Admin{RowLabel: field.Component("app:name", config)})` |
17
- | Use the shared visual controls | `@riducms/ui` | Compose controls in your component |
10
+ Collection cards can use `listResultsRenderers` without replacing Ridu's list controller. Custom dashboards
11
+ can use `withAdminLoader` with a generated Go loader reference and `AdminLoaderProps<Data>`.
12
+ The same helper pairs loaders with literal custom routes and collection/global/not-found replacement
13
+ views. Results renderers own their links through `list.documentHref(document)` and `Link` from
14
+ `@riducms/admin/routing`.
15
+ See [custom admin views and data](https://riducms.com/docs/custom-components/custom-views/) for the complete
16
+ Go-to-Svelte contract and its deliberate limits.
17
+
18
+ | Job | Import | Register/select it |
19
+ | ------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
20
+ | Add a field type with its own Go rules and structured value | `defineAdminPlugin`, `definePluginField` from `@riducms/plugin/authoring/v1` | Plugin `fields` map; matching field-type key declared in Go |
21
+ | Supply an alternative advanced field editor in a paired plugin | `defineFieldComponent` from `@riducms/plugin/authoring/v1` | Plugin `fieldEditors` map; Go `.Admin(field.Admin{Editor: field.PluginComponent(owner, name, config)})` |
22
+ | Change how one application's text/number/checkbox-style field looks | `defineFieldEditor` from `@riducms/plugin/editor` | `defineAdmin({ fieldEditors })`; Go `.Admin(field.Admin{Editor: field.Component("app:name", config)})` |
23
+ | Add application routes, dashboard panels or other admin UI | `defineAdmin` from `@riducms/plugin/admin` | `admin/src/admin.config.ts` |
24
+ | Customize an application's array/block row headings | `defineRowLabel` from `@riducms/plugin/admin` | `defineAdmin({ rowLabels })`; Go `.Admin(field.Admin{RowLabel: field.Component("app:name", config)})` |
25
+ | Use the shared visual controls | `@riducms/ui` | Compose controls in your component |
18
26
 
19
27
  Public component/host types are exported from `@riducms/plugin`. The versioned authoring import
20
28
  also exports the plugin field types. Do not import private `admin/src` implementations.
@@ -54,27 +62,39 @@ unknown data, returns the shape your component expects, or throws a useful error
54
62
 
55
63
  ```ts
56
64
  export interface NoteValue {
57
- text: string;
65
+ text: string;
58
66
  }
59
67
  export interface NoteConfig {
60
- copyTo: string;
68
+ copyTo: string;
61
69
  }
62
70
 
63
71
  export function decodeNote(raw: unknown): NoteValue {
64
- if (typeof raw !== "object" || raw === null || Array.isArray(raw) ||
65
- !("text" in raw) || typeof raw.text !== "string" || Object.keys(raw).length !== 1) {
66
- throw new Error("A note must be an object containing only a text string.");
67
- }
68
- return { text: raw.text };
72
+ if (
73
+ typeof raw !== "object" ||
74
+ raw === null ||
75
+ Array.isArray(raw) ||
76
+ !("text" in raw) ||
77
+ typeof raw.text !== "string" ||
78
+ Object.keys(raw).length !== 1
79
+ ) {
80
+ throw new Error("A note must be an object containing only a text string.");
81
+ }
82
+ return { text: raw.text };
69
83
  }
70
84
 
71
85
  export function decodeNoteConfig(raw: unknown): NoteConfig {
72
- if (typeof raw !== "object" || raw === null || Array.isArray(raw) ||
73
- !("copyTo" in raw) || typeof raw.copyTo !== "string" || !raw.copyTo.trim() ||
74
- Object.keys(raw).length !== 1) {
75
- throw new Error("Note settings must contain a nonempty copyTo field path.");
76
- }
77
- return { copyTo: raw.copyTo };
86
+ if (
87
+ typeof raw !== "object" ||
88
+ raw === null ||
89
+ Array.isArray(raw) ||
90
+ !("copyTo" in raw) ||
91
+ typeof raw.copyTo !== "string" ||
92
+ !raw.copyTo.trim() ||
93
+ Object.keys(raw).length !== 1
94
+ ) {
95
+ throw new Error("Note settings must contain a nonempty copyTo field path.");
96
+ }
97
+ return { copyTo: raw.copyTo };
78
98
  }
79
99
  ```
80
100
 
@@ -82,35 +102,34 @@ Then write `note-field.svelte`:
82
102
 
83
103
  ```svelte
84
104
  <script lang="ts">
85
- import type { PluginFieldProps } from "@riducms/plugin/authoring/v1";
86
- import type { NoteValue, NoteConfig } from "./value";
105
+ import type { PluginFieldProps } from "@riducms/plugin/authoring/v1";
106
+ import type { NoteValue, NoteConfig } from "./value";
87
107
 
88
- let { field, config, form }: PluginFieldProps<NoteValue, NoteConfig> = $props();
89
- // Remember this document's Title field while this note editor is open.
90
- // svelte-ignore state_referenced_locally
91
- const title = form.bind(config.copyTo);
108
+ let { field, config, form }: PluginFieldProps<NoteValue, NoteConfig> = $props();
109
+ // Remember this document's Title field while this note editor is open.
110
+ // svelte-ignore state_referenced_locally
111
+ const title = form.bind(config.copyTo);
92
112
  </script>
93
113
 
94
114
  <label for={field.schema.id}>{field.schema.admin.label}</label>
95
115
  <textarea
96
- id={field.schema.id}
97
- name={field.schema.path}
98
- required={field.schema.required}
99
- readonly={field.readOnly}
100
- aria-invalid={field.issues.length > 0}
101
- aria-describedby={`${field.schema.id}-issues`}
102
- value={field.value?.text ?? ""}
103
- oninput={(event) => field.set({ text: event.currentTarget.value })}
104
- ></textarea>
116
+ id={field.schema.id}
117
+ name={field.schema.path}
118
+ required={field.schema.required}
119
+ readonly={field.readOnly}
120
+ aria-invalid={field.issues.length > 0}
121
+ aria-describedby={`${field.schema.id}-issues`}
122
+ value={field.value?.text ?? ""}
123
+ oninput={(event) => field.set({ text: event.currentTarget.value })}></textarea>
105
124
  <div id={`${field.schema.id}-issues`} aria-live="polite">
106
- {#each field.issues as issue}<p>{issue.message}</p>{/each}
125
+ {#each field.issues as issue}<p>{issue.message}</p>{/each}
107
126
  </div>
108
127
  <button
109
- type="button"
110
- disabled={field.readOnly || title.readOnly || !field.value?.text}
111
- onclick={() => title.set(field.value?.text ?? null)}
128
+ type="button"
129
+ disabled={field.readOnly || title.readOnly || !field.value?.text}
130
+ onclick={() => title.set(field.value?.text ?? null)}
112
131
  >
113
- Use note as title
132
+ Use note as title
114
133
  </button>
115
134
  ```
116
135
 
@@ -122,15 +141,15 @@ import NoteField from "./note-field.svelte";
122
141
  import { decodeNote, decodeNoteConfig } from "./value";
123
142
 
124
143
  export const editorialAdminPlugin = defineAdminPlugin({
125
- key: "editorial-tools",
126
- pairingVersion: 1,
127
- fields: {
128
- "review-note": definePluginField({
129
- component: NoteField,
130
- decodeValue: decodeNote,
131
- decodeConfig: decodeNoteConfig,
132
- }),
133
- },
144
+ key: "editorial-tools",
145
+ pairingVersion: 1,
146
+ fields: {
147
+ "review-note": definePluginField({
148
+ component: NoteField,
149
+ decodeValue: decodeNote,
150
+ decodeConfig: decodeNoteConfig,
151
+ }),
152
+ },
134
153
  });
135
154
  ```
136
155
 
@@ -185,14 +204,14 @@ expects. This is different from your own `field`, which has your registered deco
185
204
 
186
205
  ## Values, config and validation
187
206
 
188
- | API | Meaning |
189
- | --- | --- |
190
- | `field.value` | Latest checked form value, including unsaved edits. Objects/arrays are copies. |
191
- | `field.rawValue` | Copied data before decoding, useful for recovery UI when old data is unsupported. |
207
+ | API | Meaning |
208
+ | ----------------- | ------------------------------------------------------------------------------------------ |
209
+ | `field.value` | Latest checked form value, including unsaved edits. Objects/arrays are copies. |
210
+ | `field.rawValue` | Copied data before decoding, useful for recovery UI when old data is unsupported. |
192
211
  | `field.set(next)` | Replace this value in the normal unsaved form. Use `null` to clear; undefined is rejected. |
193
- | `field.issues` | Current form/server validation issues for the field and its children. |
194
- | `form.get(path)` | Copy a value at its current document-root path; does not remember a row's identity. |
195
- | `form.snapshot()` | Copy current form values once. It is neither live state nor a server fetch. |
212
+ | `field.issues` | Current form/server validation issues for the field and its children. |
213
+ | `form.get(path)` | Copy a value at its current document-root path; does not remember a row's identity. |
214
+ | `form.snapshot()` | Copy current form values once. It is neither live state nor a server fetch. |
196
215
 
197
216
  Changing an object returned by a read does not edit the form. Call `set` with a replacement value.
198
217
  Ridu copies values supplied to `set` too, so mutating your original object afterward does not
@@ -226,11 +245,17 @@ final checks on saving; UI customization does not change storage, filtering or o
226
245
  - `collections`: available collection definitions, not the documents themselves.
227
246
  - `locale`: content locale, separate from the admin interface language.
228
247
  - `documentRevision`: an admin change counter for refreshing UI, not a saved `_revision` number.
248
+ - `canCreateDocument(collection)`: a live presentation hint for showing collection create controls.
249
+ The reference browser and operation engine still authorize the actual create operation.
229
250
  - `findDocument(collection, id, signal?)`: fetch a saved document in the current locale. It does
230
251
  not read the current form's unsaved changes.
231
252
  - `referenceBrowser`: the related-document picker/editor. Render it and update your field in
232
- `onCommit(ids)`. Return false to keep it open; otherwise it closes after acceptance. Saving a
233
- related document inside the picker is a separate server operation.
253
+ `onCommit(ids, collectionSlug)`. Supply `collections` to offer a collection selector; switching
254
+ clears the list query and uncommitted selection. The admin offers only collections the current
255
+ session can read and does not browse or commit when none are available. A static `optionFilter`
256
+ applies to every offered collection; pass a collection-aware resolver when their filter fields
257
+ differ. Return false to keep it open; otherwise it closes after acceptance. Saving a related
258
+ document inside the picker is a separate server operation.
234
259
  - `requestPlugin<Result>(path, body, signal?)`: POST to this renderer's paired Go plugin endpoint,
235
260
  using a relative path such as `generate-title`. The response generic does not validate data;
236
261
  request `unknown` and decode it when needed. This does not update your form automatically.
@@ -250,7 +275,7 @@ To edit an existing item's fields directly in the parent form, render:
250
275
 
251
276
  ```svelte
252
277
  {#if selectedIdentity !== undefined && authoring.schemaForm !== undefined}
253
- {@render authoring.schemaForm({ treeKey: "widgets", identity: selectedIdentity })}
278
+ {@render authoring.schemaForm({ treeKey: "widgets", identity: selectedIdentity })}
254
279
  {/if}
255
280
  ```
256
281
 
@@ -264,11 +289,11 @@ name input and its feedback:
264
289
 
265
290
  ```svelte
266
291
  {#if authoring.schemaHeader !== undefined}
267
- {@render authoring.schemaHeader({
268
- treeKey: "widgets",
269
- identity: selectedIdentity,
270
- onChange: ({ field, value }) => updateWidgetField(selectedIdentity, field, value),
271
- })}
292
+ {@render authoring.schemaHeader({
293
+ treeKey: "widgets",
294
+ identity: selectedIdentity,
295
+ onChange: ({ field, value }) => updateWidgetField(selectedIdentity, field, value),
296
+ })}
272
297
  {/if}
273
298
  ```
274
299
 
@@ -305,15 +330,20 @@ embedded-form example and the [rich-text package](../plugin-richtext/) for an ed
305
330
 
306
331
  ## Other admin UI
307
332
 
308
- Both paired plugins and `defineAdmin` can register `routes`, `dashboard`, `login`, `account`,
309
- `navigation`, `logoutButton`, `views`, `branding`, `shell`, `providers`, `listCells`,
310
- `documentActions` and `documentViews`.
333
+ Both paired plugins and `defineAdmin` can register `routes`, `dashboardPanels`, `login`, `account`,
334
+ `navigation`, `logoutButton`, `coreViews`, `branding`, `shellSlots`, `providers`,
335
+ `listCellRenderers`, `listResultsRenderers`, `documentActions` and `documentViews`.
311
336
 
312
337
  Replacement login/account/navigation/core-view components receive a `defaultView` snippet.
313
338
  Render `{@render defaultView()}` to keep the normal screen inside your wrapper. Providers must
314
339
  render their `defaultView` to include the nested admin. Host methods perform login/logout,
315
340
  refreshes and notifications; application-specific operations use the generated SDK.
316
341
 
342
+ `logoutButton` replaces the sign-out control in the navigation footer and receives `host.logout()`.
343
+ The Payload-style shell places this control there rather than in the account menu. A full navigation
344
+ replacement should render its `defaultView` when it wants to retain the framework navigation and
345
+ logout control.
346
+
317
347
  Collection/global view replacements may target one resource or act as a fallback. An exact
318
348
  resource match wins over its fallback; duplicate targets and exclusive replacements fail.
319
349
  Table cells and document action/view props contain document data, not a field-form binding.
@@ -367,6 +397,9 @@ interactions as well.
367
397
  For custom UI, use the documented editor contracts and run the checks above in the consuming
368
398
  application so its Svelte and TypeScript configuration checks the complete integration.
369
399
 
400
+ The [custom components guide](https://riducms.com/docs/custom-components/) explains when a
401
+ component is a routed view, form-bound editor, read-only renderer, shell slot, or reusable primitive.
402
+
370
403
  - [Plugin fields and editors](https://riducms.com/docs/fields/plugin/)
371
404
  - [Building plugins](https://riducms.com/docs/plugins/)
372
405
  - [Application-local field components](https://riducms.com/docs/custom-components/field-components/)
package/package.json CHANGED
@@ -3,9 +3,10 @@
3
3
  "url": "https://github.com/riducms/ridu/issues"
4
4
  },
5
5
  "dependencies": {
6
- "@riducms/protocol": "0.2.3",
7
- "@riducms/translations": "0.2.3",
8
- "@riducms/ui": "0.2.3"
6
+ "@riducms/protocol": "0.3.0",
7
+ "@riducms/sdk": "0.3.0",
8
+ "@riducms/translations": "0.3.0",
9
+ "@riducms/ui": "0.3.0"
9
10
  },
10
11
  "description": "Public TypeScript contracts for statically registered Ridu admin plugins.",
11
12
  "exports": {
@@ -39,5 +40,5 @@
39
40
  "test": "bun test"
40
41
  },
41
42
  "type": "module",
42
- "version": "0.2.3"
43
+ "version": "0.3.0"
43
44
  }
@@ -0,0 +1,42 @@
1
+ import type { OperationCapabilities, SchemaCollection } from "@riducms/protocol";
2
+ import type { Component } from "svelte";
3
+
4
+ import type { FieldDocument } from "../authoring";
5
+ import type { AdminI18n, ExtensionTranslationKey } from "../i18n";
6
+ import type { AdminExtensionNotificationTone } from "./shared";
7
+
8
+ /** Tools for a document action or extra document view after it completes its own operation. */
9
+ export interface AdminDocumentExtensionHost {
10
+ /** Reload the current document from the server. This is not a save of unsaved form edits. */
11
+ refresh: () => Promise<void>;
12
+ /** Show a temporary success or error message. */
13
+ notify: (tone: AdminExtensionNotificationTone, title: string, message?: string) => void;
14
+ }
15
+
16
+ /** Saved document data for actions/views; use the generated SDK for application-specific operations. */
17
+ export interface AdminDocumentExtensionProps {
18
+ collection: SchemaCollection;
19
+ document: FieldDocument;
20
+ host: AdminDocumentExtensionHost;
21
+ i18n: AdminI18n;
22
+ }
23
+
24
+ /** Add a component alongside the document's standard actions. Implement the operation in that component. */
25
+ export interface AdminDocumentAction {
26
+ key: string;
27
+ /** Omit to show the action for every non-global collection. */
28
+ collection?: string;
29
+ /** Hide unless this document operation is allowed. Visibility does not replace server authorization. */
30
+ requires?: keyof OperationCapabilities;
31
+ component: Component<AdminDocumentExtensionProps>;
32
+ }
33
+
34
+ /** Add a tab beside Edit and API. Keys `edit` and `api` are reserved for Ridu. */
35
+ export interface AdminDocumentView {
36
+ key: string;
37
+ label: string;
38
+ labelKey?: ExtensionTranslationKey;
39
+ /** Collection or global slug; omit to show for every collection and global. */
40
+ collection?: string;
41
+ component: Component<AdminDocumentExtensionProps>;
42
+ }
@@ -0,0 +1,81 @@
1
+ import type { PluginMessageCatalog } from "../i18n";
2
+ import type { RowLabelPlugin } from "../row-label";
3
+ import type { AdminDocumentAction, AdminDocumentView } from "./documents";
4
+ import type { AdminListCellRenderer, AdminListResultsRenderer } from "./lists";
5
+ import type {
6
+ AdminBrandComponent,
7
+ AdminLogoutButton,
8
+ AdminNavigationComponent,
9
+ AdminProvider,
10
+ AdminShellSlot,
11
+ } from "./shell";
12
+ import type {
13
+ AdminAccountComponent,
14
+ AdminCoreView,
15
+ AdminDashboardPanel,
16
+ AdminLoginComponent,
17
+ AdminRoute,
18
+ } from "./views";
19
+
20
+ export * from "./documents";
21
+ export * from "./lists";
22
+ export * from "./shared";
23
+ export * from "./shell";
24
+ export * from "./views";
25
+
26
+ /**
27
+ * Places where a plugin or application can add admin UI. Arrays keep registration
28
+ * order; application entries follow plugin entries. Replacement slots allow one
29
+ * matching replacement. Conflicts are reported instead of silently overriding UI.
30
+ */
31
+ export interface AdminContributions {
32
+ /** Authenticated admin routes contributed by this package. */
33
+ routes?: readonly AdminRoute[];
34
+ /** Panels composed into or replacing the authenticated dashboard. */
35
+ dashboardPanels?: readonly AdminDashboardPanel[];
36
+ /** Components composed around or replacing the sign-in screen. */
37
+ login?: readonly AdminLoginComponent[];
38
+ /** Components composed around or replacing the framework account screens. */
39
+ account?: readonly AdminAccountComponent[];
40
+ /** Components composed around or replacing the framework navigation. */
41
+ navigation?: readonly AdminNavigationComponent[];
42
+ /** Replaces the default sign-out control in the navigation footer. */
43
+ logoutButton?: AdminLogoutButton;
44
+ /** Wraps or replaces framework collection, global, and not-found views. */
45
+ coreViews?: readonly AdminCoreView[];
46
+ /** Replaces framework-owned brand graphics at exact shell surfaces. */
47
+ branding?: readonly AdminBrandComponent[];
48
+ /** Adds components to explicit global shell slots. */
49
+ shellSlots?: readonly AdminShellSlot[];
50
+ /** Wraps the admin in statically ordered Svelte context providers. */
51
+ providers?: readonly AdminProvider[];
52
+ /** Collection list cells rendered for exact collection/field pairs. */
53
+ listCellRenderers?: readonly AdminListCellRenderer[];
54
+ /** Replace only the results layout; Ridu retains list loading, controls and pagination. */
55
+ listResultsRenderers?: readonly AdminListResultsRenderer[];
56
+ /** Actions rendered alongside framework-owned document actions. */
57
+ documentActions?: readonly AdminDocumentAction[];
58
+ /** Read-only or operational views rendered beside Edit and API. */
59
+ documentViews?: readonly AdminDocumentView[];
60
+ }
61
+
62
+ /** Checked registrations collected for Ridu's admin runtime. Normally consumed by generated code. */
63
+ export interface ResolvedAdminExtensions {
64
+ rowLabels: readonly RowLabelPlugin[];
65
+ routes: readonly AdminRoute[];
66
+ dashboardPanels: readonly AdminDashboardPanel[];
67
+ login: readonly AdminLoginComponent[];
68
+ account: readonly AdminAccountComponent[];
69
+ navigation: readonly AdminNavigationComponent[];
70
+ logoutButton: AdminLogoutButton | undefined;
71
+ coreViews: readonly AdminCoreView[];
72
+ branding: readonly AdminBrandComponent[];
73
+ shellSlots: readonly AdminShellSlot[];
74
+ providers: readonly AdminProvider[];
75
+ listCellRenderers: readonly AdminListCellRenderer[];
76
+ listResultsRenderers: readonly AdminListResultsRenderer[];
77
+ documentActions: readonly AdminDocumentAction[];
78
+ documentViews: readonly AdminDocumentView[];
79
+ messages: Readonly<Record<string, PluginMessageCatalog>>;
80
+ applicationMessages: PluginMessageCatalog | undefined;
81
+ }
@@ -0,0 +1,56 @@
1
+ import type { SchemaCollection, SchemaField } from "@riducms/protocol";
2
+ import type { Component } from "svelte";
3
+
4
+ import type { FieldDocument } from "../authoring";
5
+ import type { AdminI18n, ExtensionTranslationKey } from "../i18n";
6
+
7
+ /** Live, read-only projections and selection commands from Ridu's list controller. */
8
+ export interface AdminCollectionList {
9
+ readonly documents: readonly FieldDocument[];
10
+ readonly selectedIDs: ReadonlySet<string>;
11
+ readonly selectionEnabled: boolean;
12
+ readonly allOnPageSelected: boolean;
13
+ readonly pageSelectionIndeterminate: boolean;
14
+ toggleDocument(id: string, selected: boolean): void;
15
+ togglePage(selected: boolean): void;
16
+ title(document: FieldDocument): string;
17
+ /** Router-root destination with an encoded ID and the current content locale. */
18
+ documentHref(document: FieldDocument): string;
19
+ }
20
+
21
+ export interface AdminListResultsRendererProps {
22
+ collection: SchemaCollection;
23
+ list: AdminCollectionList;
24
+ i18n: AdminI18n;
25
+ }
26
+
27
+ /** Collection-specific results layout. Trash retains its framework restore/delete controls. */
28
+ export interface AdminListResultsRenderer {
29
+ key: string;
30
+ collection: string;
31
+ component: Component<AdminListResultsRendererProps>;
32
+ }
33
+
34
+ /** Data for displaying a collection table cell. This is not an editable document-form binding. */
35
+ export interface AdminListCellRendererProps {
36
+ collection: SchemaCollection;
37
+ field: SchemaField;
38
+ document: FieldDocument;
39
+ /** This field's saved value. Check its type before rendering; changing it does not save a document. */
40
+ value: unknown;
41
+ i18n: AdminI18n;
42
+ }
43
+
44
+ /** Replace the cell display for one collection/field pair; duplicate targets are rejected. */
45
+ export interface AdminListCellRenderer {
46
+ key: string;
47
+ /** Collection slug, for example `"posts"`. */
48
+ collection: string;
49
+ /** Top-level field name/path in that collection, for example `"title"`. */
50
+ field: string;
51
+ /** Fallback column heading. */
52
+ label: string;
53
+ /** Optional translation key from this plugin's messages, or app:... for application entries. */
54
+ labelKey?: ExtensionTranslationKey;
55
+ component: Component<AdminListCellRendererProps>;
56
+ }
@@ -0,0 +1,48 @@
1
+ import type { SchemaManifest } from "@riducms/protocol";
2
+ import type { AdminLoader } from "@riducms/sdk";
3
+ import type { Component } from "svelte";
4
+
5
+ import type { FieldDocument } from "../authoring";
6
+ import type { AdminI18n } from "../i18n";
7
+
8
+ /** Common data supplied to admin extension components. Use the generated SDK for requests. */
9
+ export interface AdminExtensionProps {
10
+ /** Resolved Go schema available to this admin session; it may omit inaccessible resources. */
11
+ manifest: SchemaManifest;
12
+ /** Signed-in user document, absent on screens where no user is signed in yet. */
13
+ user?: FieldDocument;
14
+ /** Current admin interface translations and formatting preferences. */
15
+ i18n: AdminI18n;
16
+ }
17
+
18
+ export type AdminExtensionNotificationTone = "success" | "error";
19
+
20
+ /** Prepared application data and the mounted view's explicit refresh operation. */
21
+ export interface AdminLoaderProps<Data> {
22
+ data: Data;
23
+ refresh: () => Promise<void>;
24
+ refreshing: boolean;
25
+ }
26
+
27
+ export type AdminViewComponent<Props extends object> =
28
+ | { component: Component<Props> | Component; loader?: never }
29
+ | {
30
+ component: Component<Props & AdminLoaderProps<unknown>>;
31
+ loader: AdminLoader<never, unknown>;
32
+ };
33
+
34
+ /** Spread this pairing into a dashboard panel, custom route or core-view registration. */
35
+ export function withAdminLoader<Input, Data, Props extends AdminLoaderProps<NoInfer<Data>>>(
36
+ loader: AdminLoader<Input, Data>,
37
+ component: Component<Props>
38
+ ): {
39
+ loader: AdminLoader<never, unknown>;
40
+ component: Component<Omit<Props, keyof AdminLoaderProps<unknown>> & AdminLoaderProps<unknown>>;
41
+ } {
42
+ // Erase the paired generic only after checking both sides. The host decodes
43
+ // with this same loader before handing its output to this same component.
44
+ return { loader, component } as unknown as {
45
+ loader: AdminLoader<never, unknown>;
46
+ component: Component<Omit<Props, keyof AdminLoaderProps<unknown>> & AdminLoaderProps<unknown>>;
47
+ };
48
+ }
@@ -0,0 +1,72 @@
1
+ import type { Component, Snippet } from "svelte";
2
+
3
+ import type { AdminI18n } from "../i18n";
4
+ import type { AdminExtensionProps } from "./shared";
5
+
6
+ /** Choose the whole navigation's boundary, the resource-link list's boundary, or replace navigation. */
7
+ export type AdminNavigationPosition = "before" | "beforeLinks" | "afterLinks" | "after" | "replace";
8
+
9
+ export interface AdminNavigationComponentProps extends AdminExtensionProps {
10
+ /** Render with `{@render defaultView()}` to include Ridu's navigation in a replacement. */
11
+ defaultView: Snippet;
12
+ }
13
+
14
+ /** Insert a component at a navigation position. At most one entry can replace navigation. */
15
+ export interface AdminNavigationComponent {
16
+ key: string;
17
+ component: Component<AdminNavigationComponentProps>;
18
+ position: AdminNavigationPosition;
19
+ }
20
+
21
+ export interface AdminLogoutExtensionHost {
22
+ /** End the current session and return to sign-in. Await this instead of only changing local UI. */
23
+ logout: () => Promise<void>;
24
+ }
25
+
26
+ export interface AdminLogoutButtonProps extends AdminExtensionProps {
27
+ host: AdminLogoutExtensionHost;
28
+ }
29
+
30
+ /** Replace the navigation footer's sign-out button; call the supplied `host.logout()` on activation. */
31
+ export interface AdminLogoutButton {
32
+ key: string;
33
+ component: Component<AdminLogoutButtonProps>;
34
+ }
35
+
36
+ export type AdminBrandSurface = "loginLogo" | "navigationLogo" | "accountAvatar";
37
+
38
+ export interface AdminBrandComponentProps extends AdminExtensionProps {
39
+ surface: AdminBrandSurface;
40
+ }
41
+
42
+ /** Replace one logo/avatar location. Only one component may claim each surface. */
43
+ export interface AdminBrandComponent {
44
+ key: string;
45
+ surface: AdminBrandSurface;
46
+ component: Component<AdminBrandComponentProps>;
47
+ }
48
+
49
+ export type AdminShellSlotPosition = "header" | "actions" | "settingsMenu";
50
+
51
+ export interface AdminShellSlotProps extends AdminExtensionProps {
52
+ position: AdminShellSlotPosition;
53
+ }
54
+
55
+ /** Add UI to the global header, actions area or account settings menu in registration order. */
56
+ export interface AdminShellSlot {
57
+ key: string;
58
+ position: AdminShellSlotPosition;
59
+ component: Component<AdminShellSlotProps>;
60
+ }
61
+
62
+ export interface AdminProviderProps {
63
+ /** Render `{@render defaultView()}` after setting context so the nested providers/admin appear. */
64
+ defaultView: Snippet;
65
+ i18n: AdminI18n;
66
+ }
67
+
68
+ /** Wrap the admin in a Svelte context provider. Render the supplied `defaultView` to continue the UI. */
69
+ export interface AdminProvider {
70
+ key: string;
71
+ component: Component<AdminProviderProps>;
72
+ }