@riducms/admin 0.1.5 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/package.json +11 -9
  2. package/src/app.css +63 -4
  3. package/src/app.svelte +12 -14
  4. package/src/components/ui/date-value-control/date-value-control-loader.svelte +15 -4
  5. package/src/components/ui/date-value-control/date-value-control.svelte +17 -17
  6. package/src/components/ui/select/select-trigger.svelte +1 -1
  7. package/src/core/forms/embedded-fields.ts +132 -0
  8. package/src/core/forms/embedded-schema-draft.svelte.ts +335 -0
  9. package/src/core/forms/field-editor-authoring.ts +47 -0
  10. package/src/core/forms/field-editor-binding.ts +228 -0
  11. package/src/core/forms/field-issue-focus.ts +42 -0
  12. package/src/core/forms/field-occurrence.ts +122 -0
  13. package/src/core/forms/form-controller.svelte.ts +475 -29
  14. package/src/core/forms/form-issue-correlation.ts +159 -0
  15. package/src/core/forms/form-schema.ts +211 -17
  16. package/src/core/forms/form-validation.ts +183 -17
  17. package/src/core/forms/live-validation.svelte.ts +317 -0
  18. package/src/core/forms/plugin-field-authoring.ts +160 -0
  19. package/src/core/forms/plugin-field-binding.ts +258 -0
  20. package/src/core/forms/plugin-field-write.ts +57 -0
  21. package/src/core/i18n/admin-i18n.svelte.ts +8 -1
  22. package/src/core/i18n/localized-schema.ts +33 -10
  23. package/src/core/plugins/field-registry.ts +29 -49
  24. package/src/core/plugins/row-label-registry.ts +25 -5
  25. package/src/core/runtime/admin-runtime.svelte.ts +25 -13
  26. package/src/features/auth/account-route.svelte +10 -5
  27. package/src/features/auth/account-security-route.svelte +1 -1
  28. package/src/features/collections/collection-list-bulk-editor.svelte +6 -6
  29. package/src/features/collections/collection-list-controller.svelte.ts +5 -5
  30. package/src/features/collections/collection-list-filter-popover.svelte +6 -6
  31. package/src/features/collections/collection-list-route.svelte +13 -14
  32. package/src/features/collections/list-workspace.ts +22 -8
  33. package/src/features/documents/document-controller.svelte.ts +8 -2
  34. package/src/features/documents/document-route.svelte +36 -43
  35. package/src/features/reference-browser/reference-browser-workflow.svelte.ts +30 -15
  36. package/src/features/reference-browser/reference-browser.svelte +215 -189
  37. package/src/features/uploads/upload-metadata-fields.svelte +6 -6
  38. package/src/features/versions/version-history-route.svelte +1 -1
  39. package/src/fields/embedded-schema-draft-editor.svelte +74 -0
  40. package/src/fields/embedded-schema-fields.svelte +44 -0
  41. package/src/fields/field-clipboard.ts +76 -16
  42. package/src/fields/field-collapsible.svelte +49 -0
  43. package/src/fields/field-layout.svelte +2 -18
  44. package/src/fields/field-messages.svelte +13 -18
  45. package/src/fields/field-renderer.svelte +53 -14
  46. package/src/fields/join/join-field.svelte +8 -1
  47. package/src/fields/live-validation-feedback.svelte +21 -0
  48. package/src/fields/local-field-editor.svelte +52 -0
  49. package/src/fields/nested/nested-field.svelte +159 -91
  50. package/src/fields/nested/scoped-field.ts +7 -9
  51. package/src/fields/plugin-field.svelte +156 -0
  52. package/src/fields/point/point-field.svelte +12 -2
  53. package/src/fields/primitive-list/primitive-list-field.svelte +170 -0
  54. package/src/fields/primitive-list/primitive-number-input.ts +6 -0
  55. package/src/fields/radio/radio-field.svelte +13 -9
  56. package/src/fields/relationship/relationship-field.svelte +36 -12
  57. package/src/fields/relationship/relationship-query.ts +4 -2
  58. package/src/fields/relationship/relationship-quick-picker.svelte +38 -10
  59. package/src/fields/scalar/date-control-value.ts +5 -5
  60. package/src/fields/scalar/date-value.ts +5 -5
  61. package/src/fields/scalar/scalar-field.svelte +47 -41
  62. package/src/fields/select/select-field.svelte +24 -24
  63. package/src/fields/select/select-value.ts +3 -3
  64. package/src/fields/text/slug-field.svelte +24 -8
  65. package/src/fields/text/text-field.svelte +8 -6
  66. package/src/index.ts +4 -12
@@ -1,3 +1,5 @@
1
+ import { resolveBlockTypes } from "@riducms/protocol";
2
+ import { transformEmbeddedPayloads } from "@admin/core/forms/embedded-fields";
1
3
  import type { SchemaField } from "@riducms/protocol";
2
4
 
3
5
  export type FieldClipboardKind = "field" | "row";
@@ -61,7 +63,7 @@ export function compatibleClipboardValue(
61
63
  payload.signature !== fieldClipboardSignature(field)
62
64
  )
63
65
  return undefined;
64
- return cloneForPaste(payload.value);
66
+ return cloneFieldForPaste(field, payload.value, kind);
65
67
  }
66
68
 
67
69
  export async function writeFieldClipboard(payload: FieldClipboardPayload) {
@@ -88,15 +90,49 @@ export async function readFieldClipboard() {
88
90
  return memoryClipboard;
89
91
  }
90
92
 
91
- function cloneForPaste(value: unknown): unknown {
92
- if (Array.isArray(value)) return value.map(cloneForPaste);
93
- if (value === null || typeof value !== "object") return value;
94
- return Object.fromEntries(
95
- Object.entries(value).map(([name, nested]) => [
96
- name,
97
- name === "_key" ? crypto.randomUUID() : cloneForPaste(nested),
98
- ])
99
- );
93
+ /** Clone owned repeating rows and declared payloads; arbitrary JSON retains its identity semantics. */
94
+ export function cloneFieldForPaste(
95
+ field: SchemaField,
96
+ value: unknown,
97
+ kind: FieldClipboardKind = "field"
98
+ ): unknown {
99
+ function row(input: unknown): unknown {
100
+ if (input === null || typeof input !== "object" || Array.isArray(input))
101
+ return cloneJSON(input);
102
+ const source = input as Record<string, unknown>;
103
+ const children =
104
+ field.type === "blocks"
105
+ ? (resolveBlockTypes(field.blocks).find((block) => block.slug === source.blockType)
106
+ ?.fields ?? [])
107
+ : (field.nested?.fields ?? []);
108
+ return { ...record(children, source), _key: crypto.randomUUID() };
109
+ }
110
+ if (kind === "row") return row(value);
111
+ if ((field.type === "array" || field.type === "blocks") && Array.isArray(value))
112
+ return value.map(row);
113
+ if (
114
+ field.type === "group" &&
115
+ value !== null &&
116
+ typeof value === "object" &&
117
+ !Array.isArray(value)
118
+ ) {
119
+ return record(field.nested?.fields ?? [], value as Record<string, unknown>);
120
+ }
121
+ if (field.type === "plugin")
122
+ return transformEmbeddedPayloads(field, cloneJSON(value), field.path, (occurrence) => ({
123
+ ...record(occurrence.block.fields, occurrence.payload),
124
+ [occurrence.case.identity]: crypto.randomUUID(),
125
+ }));
126
+ return cloneJSON(value);
127
+ }
128
+
129
+ function record(fields: readonly SchemaField[], source: Record<string, unknown>) {
130
+ const copy = cloneJSON(source) as Record<string, unknown>;
131
+ for (const field of fields) {
132
+ if (Object.hasOwn(source, field.name))
133
+ copy[field.name] = cloneFieldForPaste(field, source[field.name]);
134
+ }
135
+ return copy;
100
136
  }
101
137
 
102
138
  function cloneJSON(value: unknown) {
@@ -107,7 +143,7 @@ function fieldShape(field: SchemaField): unknown {
107
143
  return {
108
144
  type: field.type,
109
145
  category: field.category,
110
- select: field.select?.choices.map((choice) => choice.value),
146
+ select: field.select?.options.map((option) => option.value),
111
147
  relationship: field.relationship && {
112
148
  collection: field.relationship.collectionSlug,
113
149
  targets: field.relationship.targets?.map((target) => target.collectionSlug),
@@ -123,10 +159,34 @@ function fieldShape(field: SchemaField): unknown {
123
159
  maxRows: field.nested.maxRows ?? 0,
124
160
  fields: field.nested.fields.map((child) => ({ name: child.name, shape: fieldShape(child) })),
125
161
  },
126
- blocks: field.blocks?.types.map((block) => ({
127
- key: block.key,
128
- fields: block.fields.map((child) => ({ name: child.name, shape: fieldShape(child) })),
129
- })),
130
- plugin: field.plugin && { key: field.plugin.key, config: field.plugin.config },
162
+ blocks: field.blocks && {
163
+ minRows: field.blocks.minRows ?? 0,
164
+ maxRows: field.blocks.maxRows ?? 0,
165
+ types: resolveBlockTypes(field.blocks).map((block) => ({
166
+ key: block.slug,
167
+ fields: block.fields.map((child) => ({ name: child.name, shape: fieldShape(child) })),
168
+ })),
169
+ },
170
+ plugin: field.plugin && {
171
+ key: field.plugin.key,
172
+ config: field.plugin.config,
173
+ embeddedTrees: field.plugin.embeddedTrees?.map((tree) => ({
174
+ version: tree.version,
175
+ key: tree.key,
176
+ root: tree.root,
177
+ children: tree.children,
178
+ tag: tree.tag,
179
+ cases: tree.cases.map((branch) => ({
180
+ tagValue: branch.tagValue,
181
+ payload: branch.payload,
182
+ discriminator: branch.discriminator,
183
+ identity: branch.identity,
184
+ types: resolveBlockTypes(branch).map((block) => ({
185
+ key: block.slug,
186
+ fields: block.fields.map((child) => ({ name: child.name, shape: fieldShape(child) })),
187
+ })),
188
+ })),
189
+ })),
190
+ },
131
191
  };
132
192
  }
@@ -0,0 +1,49 @@
1
+ <script lang="ts">
2
+ import type { SchemaField } from "@riducms/protocol";
3
+ import type { FormController } from "@admin/core/forms/form-controller.svelte";
4
+ import { fieldIssueRevealEvent } from "@admin/core/forms/field-issue-focus";
5
+ import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
6
+ import FieldRenderer from "@admin/fields/field-renderer.svelte";
7
+
8
+ let {
9
+ fields,
10
+ form,
11
+ collapsible,
12
+ }: {
13
+ fields: readonly SchemaField[];
14
+ form: FormController;
15
+ collapsible: NonNullable<SchemaField["admin"]["collapsible"]>;
16
+ } = $props();
17
+ const runtime = getAdminRuntime();
18
+ // The disclosure's identity belongs to its keyed layout group.
19
+ // svelte-ignore state_referenced_locally
20
+ let open = $state(!collapsible.initiallyCollapsed);
21
+ function reveal(element: HTMLElement) {
22
+ const show = () => {
23
+ open = true;
24
+ };
25
+ element.addEventListener(fieldIssueRevealEvent, show);
26
+ return () => element.removeEventListener(fieldIssueRevealEvent, show);
27
+ }
28
+ </script>
29
+
30
+ <details
31
+ bind:open
32
+ {@attach reveal}
33
+ class="col-span-1 rounded-[4px] border border-control-border bg-background sm:col-span-12"
34
+ data-field-collapsible={collapsible.id}
35
+ >
36
+ <summary class="cursor-pointer px-5 py-4 text-[13.5px] font-medium text-foreground">
37
+ {runtime.i18n.text(collapsible.label, collapsible.labelTranslations)}
38
+ </summary>
39
+ {#if open}
40
+ <div
41
+ class="grid grid-cols-1 gap-x-4 gap-y-6 border-t border-control-border p-5 sm:grid-cols-12"
42
+ >
43
+ {#each fields as field (field.id)}<FieldRenderer {field} {form} />{/each}
44
+ </div>
45
+ {:else}
46
+ <!-- Addressable boundaries reveal the disclosure before mounting/focusing a field. -->
47
+ {#each fields as field (field.id)}<span hidden data-field-path={field.path}></span>{/each}
48
+ {/if}
49
+ </details>
@@ -2,10 +2,10 @@
2
2
  import type { SchemaField } from "@riducms/protocol";
3
3
 
4
4
  import type { FormController } from "@admin/core/forms/form-controller.svelte";
5
- import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
6
5
  import FieldRenderer from "@admin/fields/field-renderer.svelte";
7
6
  import { createFieldLayout } from "@admin/fields/field-layout";
8
7
  import FieldTabLayout from "@admin/fields/field-tab-layout.svelte";
8
+ import FieldCollapsible from "@admin/fields/field-collapsible.svelte";
9
9
 
10
10
  let {
11
11
  fields,
@@ -16,7 +16,6 @@
16
16
  form: FormController;
17
17
  suppressedTabGroupID?: string;
18
18
  } = $props();
19
- const runtime = getAdminRuntime();
20
19
  </script>
21
20
 
22
21
  {#snippet fieldGrid(tabFields: readonly SchemaField[])}
@@ -25,22 +24,7 @@
25
24
  {#if group.tabGroup !== undefined}
26
25
  <FieldTabLayout fields={group.fields} {form} tabGroup={group.tabGroup} />
27
26
  {:else if group.collapsible !== undefined}
28
- <details
29
- class="col-span-1 rounded-[4px] border border-control-border bg-background sm:col-span-12"
30
- open={!group.collapsible.initiallyCollapsed}
31
- data-field-collapsible={group.collapsible.id}
32
- >
33
- <summary class="cursor-pointer px-5 py-4 text-[13.5px] font-medium text-foreground">
34
- {runtime.i18n.text(group.collapsible.label, group.collapsible.labelTranslations)}
35
- </summary>
36
- <div
37
- class="grid grid-cols-1 gap-x-4 gap-y-6 border-t border-control-border p-5 sm:grid-cols-12"
38
- >
39
- {#each group.fields as field (field.id)}
40
- <FieldRenderer {field} {form} />
41
- {/each}
42
- </div>
43
- </details>
27
+ <FieldCollapsible fields={group.fields} {form} collapsible={group.collapsible} />
44
28
  {:else if group.row !== undefined}
45
29
  <div
46
30
  class="col-span-1 grid grid-cols-1 gap-x-4 gap-y-6 sm:col-span-12 sm:grid-cols-12"
@@ -1,29 +1,24 @@
1
1
  <script lang="ts">
2
2
  import type { ValidationIssue } from "@riducms/protocol";
3
- import CircleAlertIcon from "~icons/lucide/circle-alert";
3
+ import { FieldFeedback } from "@riducms/ui";
4
+ import type { Snippet } from "svelte";
4
5
 
5
6
  let {
6
- id,
7
+ controlID,
7
8
  issues = [],
8
9
  description,
9
10
  class: className,
11
+ children,
10
12
  }: {
11
- id: string;
12
- issues?: readonly ValidationIssue[];
13
- description?: string;
14
- class?: string;
13
+ controlID: string;
14
+ issues?: readonly ValidationIssue[] | undefined;
15
+ description?: string | undefined;
16
+ class?: string | undefined;
17
+ children: Snippet;
15
18
  } = $props();
19
+ const errors = $derived(issues.map((issue) => issue.message));
16
20
  </script>
17
21
 
18
- {#if issues.length > 0}
19
- <div {id} class={["flex items-start gap-1.5", className]} role="alert" aria-atomic="true">
20
- <CircleAlertIcon class="mt-0.5 size-3.5 shrink-0 text-destructive" aria-hidden="true" />
21
- <ul class="grid gap-0.5">
22
- {#each issues as issue (`${issue.path}:${issue.code}:${issue.message}`)}
23
- <li class="ridu-field-error">{issue.message}</li>
24
- {/each}
25
- </ul>
26
- </div>
27
- {:else if description !== undefined}
28
- <p {id} class={["ridu-field-help", className]}>{description}</p>
29
- {/if}
22
+ <FieldFeedback {controlID} {description} {errors} class={className}>
23
+ {@render children()}
24
+ </FieldFeedback>
@@ -13,6 +13,8 @@
13
13
  withContentLocaleLabel,
14
14
  } from "@admin/fields/localized-field-presentation";
15
15
 
16
+ import PluginField from "@admin/fields/plugin-field.svelte";
17
+ import LocalFieldEditor from "@admin/fields/local-field-editor.svelte";
16
18
  import SelectField from "@admin/fields/select/select-field.svelte";
17
19
  import RadioField from "@admin/fields/radio/radio-field.svelte";
18
20
  import PointField from "@admin/fields/point/point-field.svelte";
@@ -22,7 +24,9 @@
22
24
  import ScalarField from "@admin/fields/scalar/scalar-field.svelte";
23
25
  import NestedField from "@admin/fields/nested/nested-field.svelte";
24
26
  import RelationshipField from "@admin/fields/relationship/relationship-field.svelte";
27
+ import PrimitiveListField from "@admin/fields/primitive-list/primitive-list-field.svelte";
25
28
  import TextField from "@admin/fields/text/text-field.svelte";
29
+ import LiveValidationFeedback from "@admin/fields/live-validation-feedback.svelte";
26
30
 
27
31
  interface Props {
28
32
  field: SchemaField;
@@ -48,15 +52,13 @@
48
52
  referenceBrowser: ReferenceBrowser,
49
53
  findDocument: (collection, id, signal) =>
50
54
  runtime.client.find(collection, id, { signal, locale: form.contentLocale }),
51
- async requestPlugin<Result>(path: string, body: unknown, signal?: AbortSignal) {
52
- const owner = field.admin.component?.plugin ?? field.plugin?.key;
53
- if (owner === undefined) {
54
- throw new Error("This field renderer does not have an owning plugin");
55
- }
56
- return runtime.client.requestPlugin<Result>(owner, path, body, { signal });
57
- },
58
55
  };
59
- const plugin = $derived(runtime.fields.resolve(field));
56
+ const editor = $derived(
57
+ field.admin.editor === undefined
58
+ ? undefined
59
+ : runtime.editors[field.admin.editor.reference as `app:${string}`]
60
+ );
61
+ const plugin = $derived(editor === undefined ? runtime.fields.resolve(field) : undefined);
60
62
  const accessPath = $derived(fieldAccessPath(field));
61
63
  const visible = $derived(
62
64
  field.admin.hidden !== true &&
@@ -65,7 +67,7 @@
65
67
  evaluateFieldCondition(field.admin.condition, field.path, (path) => form.get(path)))
66
68
  );
67
69
  const renderedField = $derived(
68
- field.type === "join" || field.admin.readOnly || form.canWrite(field.path, accessPath)
70
+ field.admin.readOnly || form.canWrite(field.path, accessPath)
69
71
  ? field
70
72
  : { ...field, admin: { ...field.admin, readOnly: true } }
71
73
  );
@@ -101,11 +103,12 @@
101
103
  );
102
104
 
103
105
  function renderField() {
104
- if (plugin.component !== undefined) return plugin.component;
105
-
106
- switch (plugin.type) {
106
+ switch (plugin?.type) {
107
107
  case "text":
108
108
  return TextField;
109
+ case "text-list":
110
+ case "number-list":
111
+ return PrimitiveListField;
109
112
  case "select":
110
113
  return SelectField;
111
114
  case "radio":
@@ -131,9 +134,45 @@
131
134
  }
132
135
  </script>
133
136
 
134
- {#if visible}<div class={columnClass}>
137
+ {#if visible}<div
138
+ class={columnClass}
139
+ data-live-validation-field={field.path}
140
+ onfocusout={(event) => {
141
+ if (
142
+ event.target instanceof Element &&
143
+ event.target.closest("[data-live-validation-field]") === event.currentTarget &&
144
+ !(event.relatedTarget instanceof Node && event.currentTarget.contains(event.relatedTarget))
145
+ )
146
+ form.liveValidation.flush(field.path);
147
+ }}
148
+ >
135
149
  {const RenderField = $derived(renderField())}
136
- <RenderField field={presentedField} {form} {authoring} i18n={runtime.i18n} />
150
+ {#if editor !== undefined}
151
+ {#key form}
152
+ {#key `${form.editorEpoch}:${runtime.manifestRevision}:${field.id}`}
153
+ <LocalFieldEditor
154
+ schema={presentedField}
155
+ {form}
156
+ {editor}
157
+ {authoring}
158
+ i18n={runtime.i18n}
159
+ />
160
+ {/key}
161
+ {/key}
162
+ {:else if plugin?.extension !== undefined}
163
+ {#key form}
164
+ {#key `${form.editorEpoch}:${runtime.manifestRevision}:${field.id}`}
165
+ <PluginField schema={presentedField} {form} extension={plugin.extension} />
166
+ {/key}
167
+ {/key}
168
+ {:else}
169
+ <RenderField field={presentedField} {form} {authoring} />
170
+ {/if}
171
+ <LiveValidationFeedback
172
+ feedback={form.liveValidation.forField(field.path)}
173
+ i18n={runtime.i18n}
174
+ path={field.path}
175
+ />
137
176
  {#if inheritedFrom !== undefined}<p
138
177
  class="mt-1 font-mono text-[10px] text-foreground-faint"
139
178
  data-localization-source={inheritedFrom}
@@ -158,6 +158,7 @@
158
158
  sourceID !== undefined && field.join?.allowCreate !== false && targetOperations?.create === true
159
159
  );
160
160
  const canManage = $derived(!field.admin.readOnly && (canAttach || canCreate));
161
+ const editingBlocked = $derived(form.editingBlocked);
161
162
  let browserOpen = $state(false);
162
163
  let pending = $state(false);
163
164
  let mutationError = $state<string>();
@@ -179,6 +180,7 @@
179
180
  }
180
181
 
181
182
  async function commit(ids: string[]) {
183
+ if (editingBlocked) return false;
182
184
  const source = form.resource;
183
185
  if (
184
186
  source?.id === undefined ||
@@ -270,7 +272,12 @@
270
272
  })}
271
273
  </p>
272
274
  {#if canManage}
273
- <Button variant="outline" size="sm" disabled={pending} onclick={() => (browserOpen = true)}>
275
+ <Button
276
+ variant="outline"
277
+ size="sm"
278
+ disabled={pending || editingBlocked}
279
+ onclick={() => (browserOpen = true)}
280
+ >
274
281
  <ListPlusIcon class="size-3" />
275
282
  {runtime.i18n.t(pending ? "fields:updating" : "fields:manageRelationships")}
276
283
  </Button>
@@ -0,0 +1,21 @@
1
+ <script lang="ts">
2
+ import type { AdminI18n, FieldLiveValidation } from "@riducms/plugin";
3
+ import { Button } from "@riducms/ui";
4
+ let { feedback, i18n, path }: { feedback: FieldLiveValidation; i18n: AdminI18n; path: string } =
5
+ $props();
6
+ </script>
7
+
8
+ <div data-live-validation={feedback.status} data-live-validation-path={path} aria-live="polite">
9
+ {#if feedback.status === "pending"}
10
+ <p class="mt-1 text-xs text-muted-foreground">{i18n.t("fields:liveValidationPending")}</p>
11
+ {:else if feedback.status === "failed"}
12
+ <div class="mt-1 flex items-center gap-2 text-xs text-muted-foreground">
13
+ <p>{i18n.t("fields:liveValidationFailed")}</p>
14
+ <Button variant="ghost" size="sm" onclick={feedback.retry}>
15
+ {i18n.t("fields:liveValidationRetry")}
16
+ </Button>
17
+ </div>
18
+ {:else if feedback.status === "skipped"}
19
+ <p class="mt-1 text-xs text-muted-foreground">{i18n.t("fields:liveValidationSkipped")}</p>
20
+ {/if}
21
+ </div>
@@ -0,0 +1,52 @@
1
+ <script lang="ts">
2
+ import { onDestroy, type Component } from "svelte";
3
+ import type { SchemaField } from "@riducms/protocol";
4
+ import type { FieldAuthoringHost, AdminI18n } from "@riducms/plugin";
5
+ import type { FieldEditorProps, RegisteredFieldEditor } from "@riducms/plugin/editor";
6
+ import type { FormController } from "@admin/core/forms/form-controller.svelte";
7
+ import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
8
+ import { FieldEditorBinding } from "@admin/core/forms/field-editor-binding";
9
+ import { guardEditorAuthoring } from "@admin/core/forms/field-editor-authoring";
10
+
11
+ let {
12
+ schema,
13
+ form,
14
+ editor,
15
+ authoring,
16
+ i18n,
17
+ }: {
18
+ schema: SchemaField;
19
+ form: FormController;
20
+ editor: RegisteredFieldEditor;
21
+ authoring: FieldAuthoringHost;
22
+ i18n: AdminI18n;
23
+ } = $props();
24
+ const runtime = getAdminRuntime();
25
+ // Parent keys this host by form, lifetime epoch, manifest revision and occurrence ID.
26
+ // Each capability belongs to this mount; reordering only updates the schema getter.
27
+ // svelte-ignore state_referenced_locally
28
+ const config = editor.decode(schema);
29
+ // Registration correlates props; decode above validates this manifest selection.
30
+ // Keep this erasure at the host boundary so registrations cannot retype components.
31
+ // svelte-ignore state_referenced_locally
32
+ const Editor = editor.component as Component<
33
+ Omit<FieldEditorProps<RegisteredFieldEditor["type"], unknown>, "config"> & { config?: unknown }
34
+ >;
35
+ // svelte-ignore state_referenced_locally
36
+ const field = new FieldEditorBinding(
37
+ form,
38
+ () => schema,
39
+ editor.type,
40
+ () => runtime.manifestRevision
41
+ );
42
+ const guardedAuthoring = guardEditorAuthoring(() => authoring, field.assertActive);
43
+ onDestroy(field.destroy);
44
+ </script>
45
+
46
+ <Editor
47
+ {field}
48
+ {...config === undefined ? {} : { config }}
49
+ form={field.form}
50
+ authoring={guardedAuthoring}
51
+ {i18n}
52
+ />