@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,19 +1,19 @@
1
- import type { SchemaDatePickerAppearance } from "@riducms/protocol";
1
+ import type { SchemaDateFormat } from "@riducms/protocol";
2
2
  import type { AdminI18n } from "@riducms/plugin";
3
3
 
4
4
  export function formatDateDisplay(
5
5
  value: unknown,
6
- appearance: SchemaDatePickerAppearance | undefined,
6
+ appearance: SchemaDateFormat | undefined,
7
7
  i18n: AdminI18n
8
8
  ) {
9
9
  const encoded = String(value ?? "");
10
10
  if (encoded === "") return "—";
11
- if (appearance === "timeOnly") return encoded.slice(0, 5);
11
+ if (appearance === "time") return encoded.slice(0, 5);
12
12
  const date = new Date(
13
- appearance === "dayOnly" || appearance === undefined ? `${encoded}T00:00:00Z` : encoded
13
+ appearance === "date" || appearance === undefined ? `${encoded}T00:00:00Z` : encoded
14
14
  );
15
15
  if (Number.isNaN(date.valueOf())) return encoded;
16
- if (appearance === "dayAndTime") {
16
+ if (appearance === "date-time") {
17
17
  return i18n.formatDate(date, {
18
18
  dateStyle: "medium",
19
19
  timeStyle: "short",
@@ -1,5 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { SchemaField, ValidationIssue } from "@riducms/protocol";
3
+ import { fieldControlARIA } from "@riducms/ui";
4
+ import { onDestroy } from "svelte";
3
5
 
4
6
  import { Checkbox } from "@admin/components/ui/checkbox";
5
7
  import { DateValueControl } from "@admin/components/ui/date-value-control";
@@ -14,54 +16,63 @@
14
16
  const runtime = getAdminRuntime();
15
17
  const value = $derived(form.get(field.path));
16
18
  const issues = $derived(form.issuesFor(field.path));
19
+ const editingBlocked = $derived(field.admin.readOnly === true || form.editingBlocked);
17
20
  let jsonError = $state<string>();
21
+ let releaseUnfinishedInput: (() => void) | undefined;
22
+ onDestroy(() => releaseUnfinishedInput?.());
18
23
  const displayIssues = $derived<ValidationIssue[]>(
19
24
  jsonError === undefined
20
25
  ? issues
21
26
  : [{ code: "invalid_json", path: field.path, message: jsonError }]
22
27
  );
23
- const hasMessage = $derived(displayIssues.length > 0 || field.admin.description !== undefined);
24
- const dateAppearance = $derived(field.date?.pickerAppearance);
28
+ const controlARIA = $derived(
29
+ fieldControlARIA(field.id, field.admin.description !== undefined, displayIssues.length > 0)
30
+ );
31
+ const dateAppearance = $derived(field.date?.format);
25
32
  const stringConstraints = $derived(field.type === "code" ? field.code : field.textarea);
26
33
 
27
34
  $effect(() => form.register(field.path));
28
35
 
29
36
  function setJSON(encoded: string) {
37
+ if (editingBlocked) return;
30
38
  try {
31
39
  form.set(field.path, JSON.parse(encoded));
40
+ releaseUnfinishedInput?.();
41
+ releaseUnfinishedInput = undefined;
32
42
  jsonError = undefined;
33
43
  } catch {
44
+ releaseUnfinishedInput?.();
45
+ releaseUnfinishedInput = form.setLiveInputUnavailable(field.path);
34
46
  jsonError = runtime.i18n.t("errors:validJSON");
35
47
  }
36
48
  }
37
49
  </script>
38
50
 
39
51
  {#if field.type === "checkbox"}
40
- <div class="grid gap-2" data-field-path={field.path} data-invalid={displayIssues.length > 0}>
41
- <label
42
- class="flex min-h-6 w-fit cursor-pointer items-center gap-2 text-[13px] text-foreground-muted hover:text-foreground has-disabled:cursor-not-allowed has-disabled:text-foreground-faint"
43
- for={field.id}
44
- >
45
- <Checkbox
46
- id={field.id}
47
- required={field.required}
48
- aria-invalid={displayIssues.length > 0}
49
- aria-describedby={hasMessage ? `${field.id}-message` : undefined}
50
- aria-errormessage={displayIssues.length > 0 ? `${field.id}-message` : undefined}
51
- disabled={field.admin.readOnly}
52
- bind:checked={() => Boolean(value), (next) => form.set(field.path, next)}
53
- />
54
- <span>{field.admin.label}</span>
55
- {#if field.required}<span class="ridu-field-required" aria-hidden="true">*</span>{/if}
56
- {#if field.admin.readOnly}<span class="ridu-field-status">
57
- {runtime.i18n.t("fields:readOnly")}
58
- </span>{/if}
59
- </label>
52
+ <div data-field-path={field.path} data-invalid={displayIssues.length > 0}>
60
53
  <FieldMessages
61
- id="{field.id}-message"
54
+ controlID={field.id}
62
55
  issues={displayIssues}
63
56
  description={field.admin.description}
64
- />
57
+ >
58
+ <label
59
+ class="flex min-h-6 w-fit cursor-pointer items-center gap-2 text-[13px] text-foreground-muted hover:text-foreground has-disabled:cursor-not-allowed has-disabled:text-foreground-faint"
60
+ for={field.id}
61
+ >
62
+ <Checkbox
63
+ id={field.id}
64
+ required={field.required && (!field.dynamicDefault || form.get(field.path) !== undefined)}
65
+ {...controlARIA}
66
+ disabled={editingBlocked}
67
+ bind:checked={() => Boolean(value), (next) => form.set(field.path, next)}
68
+ />
69
+ <span>{field.admin.label}</span>
70
+ {#if field.required}<span class="ridu-field-required" aria-hidden="true">*</span>{/if}
71
+ {#if field.admin.readOnly}<span class="ridu-field-status">
72
+ {runtime.i18n.t("fields:readOnly")}
73
+ </span>{/if}
74
+ </label>
75
+ </FieldMessages>
65
76
  </div>
66
77
  {:else}
67
78
  <FieldShell {field} issues={displayIssues}>
@@ -70,15 +81,13 @@
70
81
  id={field.id}
71
82
  name={field.path}
72
83
  rows={1}
73
- required={field.required}
84
+ required={field.required && (!field.dynamicDefault || form.get(field.path) !== undefined)}
74
85
  minlength={stringConstraints?.minLength}
75
86
  maxlength={stringConstraints?.maxLength}
76
87
  placeholder={field.admin.placeholder}
77
- aria-invalid={displayIssues.length > 0}
78
- aria-describedby={hasMessage ? `${field.id}-message` : undefined}
79
- aria-errormessage={displayIssues.length > 0 ? `${field.id}-message` : undefined}
88
+ {...controlARIA}
80
89
  value={String(value ?? "")}
81
- readonly={field.admin.readOnly}
90
+ readonly={editingBlocked}
82
91
  class={field.type === "code"
83
92
  ? "min-h-14 resize-y field-sizing-fixed font-mono text-[12.5px] leading-5"
84
93
  : undefined}
@@ -89,13 +98,11 @@
89
98
  <Textarea
90
99
  id={field.id}
91
100
  name={field.path}
92
- required={field.required}
93
- aria-invalid={displayIssues.length > 0}
94
- aria-describedby={hasMessage ? `${field.id}-message` : undefined}
95
- aria-errormessage={displayIssues.length > 0 ? `${field.id}-message` : undefined}
101
+ required={field.required && (!field.dynamicDefault || form.get(field.path) !== undefined)}
102
+ {...controlARIA}
96
103
  class="min-h-40 font-mono"
97
104
  value={JSON.stringify(value ?? {}, null, 2)}
98
- readonly={field.admin.readOnly}
105
+ readonly={editingBlocked}
99
106
  oninput={(event) => setJSON(event.currentTarget.value)}
100
107
  />
101
108
  {:else if field.type === "date"}
@@ -104,28 +111,27 @@
104
111
  name={field.path}
105
112
  appearance={dateAppearance}
106
113
  value={String(value ?? "")}
114
+ disabled={form.editingBlocked}
107
115
  readonly={field.admin.readOnly}
108
- required={field.required}
116
+ required={field.required && (!field.dynamicDefault || form.get(field.path) !== undefined)}
109
117
  invalid={displayIssues.length > 0}
118
+ hasDescription={field.admin.description !== undefined}
110
119
  label={field.admin.label}
111
- describedBy={hasMessage ? `${field.id}-message` : undefined}
112
120
  onValueChange={(next) => form.set(field.path, next)}
113
121
  />
114
122
  {:else}
115
123
  <Input
116
124
  id={field.id}
117
125
  name={field.path}
118
- required={field.required}
119
- aria-invalid={displayIssues.length > 0}
120
- aria-describedby={hasMessage ? `${field.id}-message` : undefined}
121
- aria-errormessage={displayIssues.length > 0 ? `${field.id}-message` : undefined}
126
+ required={field.required && (!field.dynamicDefault || form.get(field.path) !== undefined)}
127
+ {...controlARIA}
122
128
  type={field.type === "number" ? "number" : field.type === "email" ? "email" : "text"}
123
129
  min={field.type === "number" ? field.number?.min : undefined}
124
130
  max={field.type === "number" ? field.number?.max : undefined}
125
131
  step={field.type === "number" ? field.number?.step : undefined}
126
132
  placeholder={field.admin.placeholder}
127
133
  value={String(value ?? "")}
128
- readonly={field.admin.readOnly}
134
+ readonly={editingBlocked}
129
135
  oninput={(event) =>
130
136
  form.set(
131
137
  field.path,
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { SchemaField } from "@riducms/protocol";
3
+ import { fieldControlARIA } from "@riducms/ui";
3
4
  import XIcon from "~icons/lucide/x";
4
5
 
5
6
  import { Button } from "@admin/components/ui/button";
@@ -9,7 +10,7 @@
9
10
  import FieldShell from "@admin/fields/field-shell.svelte";
10
11
  import {
11
12
  removeSelectValue,
12
- selectChoiceLabel,
13
+ selectOptionLabel,
13
14
  selectManyValues,
14
15
  } from "@admin/fields/select/select-value";
15
16
 
@@ -27,40 +28,38 @@
27
28
  field.admin.placeholder ?? runtime.i18n.t("fields:select", { label: field.admin.label })
28
29
  );
29
30
  const selectedLabel = $derived(
30
- field.select?.choices.find((choice) => choice.value === value)?.label ?? placeholder
31
+ field.select?.options.find((option) => option.value === value)?.label ?? placeholder
31
32
  );
32
33
  const selectedLabels = $derived(
33
- values.map((selected) => selectChoiceLabel(field.select?.choices ?? [], selected))
34
+ values.map((selected) => selectOptionLabel(field.select?.options ?? [], selected))
34
35
  );
35
36
  const issues = $derived(form.issuesFor(field.path));
36
- const hasMessage = $derived(issues.length > 0 || field.admin.description !== undefined);
37
+ const editingBlocked = $derived(field.admin.readOnly === true || form.editingBlocked);
38
+ const controlARIA = $derived(
39
+ fieldControlARIA(field.id, field.admin.description !== undefined, issues.length > 0)
40
+ );
37
41
 
38
42
  $effect(() => form.register(field.path));
39
43
 
40
44
  function setValues(next: string[]) {
45
+ if (editingBlocked) return;
41
46
  form.set(field.path, [...next]);
42
47
  }
43
48
 
44
49
  function removeValue(selected: string) {
50
+ if (editingBlocked) return;
45
51
  form.set(field.path, removeSelectValue(values, selected));
46
52
  }
47
53
  </script>
48
54
 
49
55
  <FieldShell {field} {issues}>
50
56
  {#if hasMany}
51
- <Select
52
- type="multiple"
53
- value={values}
54
- onValueChange={setValues}
55
- disabled={field.admin.readOnly}
56
- >
57
+ <Select type="multiple" value={values} onValueChange={setValues} disabled={editingBlocked}>
57
58
  <SelectTrigger
58
59
  id={field.id}
59
60
  class="w-full"
60
- aria-invalid={issues.length > 0}
61
+ {...controlARIA}
61
62
  aria-required={field.required}
62
- aria-describedby={hasMessage ? `${field.id}-message` : undefined}
63
- aria-errormessage={issues.length > 0 ? `${field.id}-message` : undefined}
64
63
  aria-label={field.admin.label}
65
64
  >
66
65
  <span class={values.length === 0 ? "text-foreground-placeholder" : "truncate"}>
@@ -68,8 +67,8 @@
68
67
  </span>
69
68
  </SelectTrigger>
70
69
  <SelectContent>
71
- {#each field.select?.choices ?? [] as choice (choice.value)}
72
- <SelectItem value={choice.value} label={choice.label} />
70
+ {#each field.select?.options ?? [] as option (option.value)}
71
+ <SelectItem value={option.value} label={option.label} />
73
72
  {/each}
74
73
  </SelectContent>
75
74
  </Select>
@@ -83,15 +82,16 @@
83
82
  class="inline-flex max-w-full items-center gap-1 rounded-[3px] border border-control-border bg-background py-0.5 pe-0.5 ps-2 text-[12px] text-foreground-muted"
84
83
  >
85
84
  <span class="max-w-64 truncate">
86
- {selectChoiceLabel(field.select?.choices ?? [], selected)}
85
+ {selectOptionLabel(field.select?.options ?? [], selected)}
87
86
  </span>
88
87
  {#if !field.admin.readOnly}
89
88
  <Button
90
89
  variant="ghost"
91
90
  size="icon-xs"
91
+ disabled={editingBlocked}
92
92
  onclick={() => removeValue(selected)}
93
93
  aria-label={runtime.i18n.t("fields:remove", {
94
- label: selectChoiceLabel(field.select?.choices ?? [], selected),
94
+ label: selectOptionLabel(field.select?.options ?? [], selected),
95
95
  })}
96
96
  >
97
97
  <XIcon class="size-3" aria-hidden="true" />
@@ -106,16 +106,16 @@
106
106
  type="single"
107
107
  {value}
108
108
  allowDeselect={!field.required}
109
- onValueChange={(next) => form.set(field.path, next)}
110
- disabled={field.admin.readOnly}
109
+ onValueChange={(next) => {
110
+ if (!editingBlocked) form.set(field.path, next);
111
+ }}
112
+ disabled={editingBlocked}
111
113
  >
112
114
  <SelectTrigger
113
115
  id={field.id}
114
116
  class="w-full"
115
- aria-invalid={issues.length > 0}
117
+ {...controlARIA}
116
118
  aria-required={field.required}
117
- aria-describedby={hasMessage ? `${field.id}-message` : undefined}
118
- aria-errormessage={issues.length > 0 ? `${field.id}-message` : undefined}
119
119
  aria-label={field.admin.label}
120
120
  >
121
121
  <span class={value === "" ? "text-foreground-placeholder" : undefined}>
@@ -123,8 +123,8 @@
123
123
  </span>
124
124
  </SelectTrigger>
125
125
  <SelectContent>
126
- {#each field.select?.choices ?? [] as choice (choice.value)}
127
- <SelectItem value={choice.value} label={choice.label} />
126
+ {#each field.select?.options ?? [] as option (option.value)}
127
+ <SelectItem value={option.value} label={option.label} />
128
128
  {/each}
129
129
  </SelectContent>
130
130
  </Select>
@@ -1,12 +1,12 @@
1
- import type { SchemaSelectChoice } from "@riducms/protocol";
1
+ import type { SchemaSelectOption } from "@riducms/protocol";
2
2
 
3
3
  export function selectManyValues(value: unknown): string[] {
4
4
  if (!Array.isArray(value)) return [];
5
5
  return value.filter((candidate): candidate is string => typeof candidate === "string");
6
6
  }
7
7
 
8
- export function selectChoiceLabel(choices: readonly SchemaSelectChoice[], value: string) {
9
- return choices.find((choice) => choice.value === value)?.label ?? value;
8
+ export function selectOptionLabel(options: readonly SchemaSelectOption[], value: string) {
9
+ return options.find((option) => option.value === value)?.label ?? value;
10
10
  }
11
11
 
12
12
  export function removeSelectValue(values: readonly string[], value: string) {
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { SchemaField } from "@riducms/protocol";
3
+ import { fieldControlARIA } from "@riducms/ui";
3
4
  import { untrack } from "svelte";
4
5
 
5
6
  import { Button } from "@admin/components/ui/button";
@@ -21,7 +22,10 @@
21
22
  const runtime = getAdminRuntime();
22
23
  const value = $derived(String(form.get(field.path) ?? ""));
23
24
  const issues = $derived(form.issuesFor(field.path));
24
- const hasMessage = $derived(issues.length > 0 || field.admin.description !== undefined);
25
+ const editingBlocked = $derived(field.admin.readOnly === true || form.editingBlocked);
26
+ const controlARIA = $derived(
27
+ fieldControlARIA(field.id, field.admin.description !== undefined, issues.length > 0)
28
+ );
25
29
  let locked = $state(true);
26
30
  let binding = $state<DerivedTextBinding>();
27
31
 
@@ -41,14 +45,16 @@
41
45
  });
42
46
 
43
47
  function generate() {
48
+ if (editingBlocked) return;
44
49
  binding?.follow();
45
50
  }
46
51
 
47
52
  function normalizeManualValue() {
48
- if (!locked) binding?.setManual(normalizeSlug(value));
53
+ if (!editingBlocked && !locked) binding?.setManual(normalizeSlug(value));
49
54
  }
50
55
 
51
56
  function setManualValue(value: string) {
57
+ if (editingBlocked) return;
52
58
  binding?.setManual(value);
53
59
  }
54
60
  </script>
@@ -62,10 +68,8 @@
62
68
  minlength={field.text?.minLength}
63
69
  maxlength={field.text?.maxLength}
64
70
  placeholder={field.admin.placeholder}
65
- aria-invalid={issues.length > 0}
66
- aria-describedby={hasMessage ? `${field.id}-message` : undefined}
67
- aria-errormessage={issues.length > 0 ? `${field.id}-message` : undefined}
68
- readonly={field.admin.readOnly || locked}
71
+ {...controlARIA}
72
+ readonly={editingBlocked || locked}
69
73
  class="min-w-0 flex-1 font-mono"
70
74
  {value}
71
75
  oninput={(event) => setManualValue(event.currentTarget.value)}
@@ -74,11 +78,23 @@
74
78
  {#if !field.admin.readOnly}
75
79
  <div class="flex shrink-0 items-center gap-1">
76
80
  {#if !locked}
77
- <Button type="button" size="xs" variant="ghost" onclick={generate}>
81
+ <Button
82
+ type="button"
83
+ size="xs"
84
+ variant="ghost"
85
+ disabled={editingBlocked}
86
+ onclick={generate}
87
+ >
78
88
  {runtime.i18n.t("fields:generateSlug")}
79
89
  </Button>
80
90
  {/if}
81
- <Button type="button" size="xs" variant="ghost" onclick={() => (locked = !locked)}>
91
+ <Button
92
+ type="button"
93
+ size="xs"
94
+ variant="ghost"
95
+ disabled={editingBlocked}
96
+ onclick={() => (locked = !locked)}
97
+ >
82
98
  {runtime.i18n.t(locked ? "fields:unlockSlug" : "fields:lockSlug")}
83
99
  </Button>
84
100
  </div>
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { SchemaField } from "@riducms/protocol";
3
+ import { fieldControlARIA } from "@riducms/ui";
3
4
 
4
5
  import { Input } from "@admin/components/ui/input";
5
6
  import type { FormController } from "@admin/core/forms/form-controller.svelte";
@@ -14,7 +15,10 @@
14
15
  let { field, form }: Props = $props();
15
16
  const value = $derived(String(form.get(field.path) ?? ""));
16
17
  const issues = $derived(form.issuesFor(field.path));
17
- const hasMessage = $derived(issues.length > 0 || field.admin.description !== undefined);
18
+ const editingBlocked = $derived(field.admin.readOnly === true || form.editingBlocked);
19
+ const controlARIA = $derived(
20
+ fieldControlARIA(field.id, field.admin.description !== undefined, issues.length > 0)
21
+ );
18
22
 
19
23
  $effect(() => form.register(field.path));
20
24
  </script>
@@ -26,14 +30,12 @@
26
30
  <Input
27
31
  id={field.id}
28
32
  name={field.path}
29
- required={field.required}
33
+ required={field.required && (!field.dynamicDefault || form.get(field.path) !== undefined)}
30
34
  minlength={field.text?.minLength}
31
35
  maxlength={field.text?.maxLength}
32
36
  placeholder={field.admin.placeholder}
33
- aria-invalid={issues.length > 0}
34
- aria-describedby={hasMessage ? `${field.id}-message` : undefined}
35
- aria-errormessage={issues.length > 0 ? `${field.id}-message` : undefined}
36
- readonly={field.admin.readOnly}
37
+ {...controlARIA}
38
+ readonly={editingBlocked}
37
39
  {value}
38
40
  oninput={(event) => form.set(field.path, event.currentTarget.value)}
39
41
  />
package/src/index.ts CHANGED
@@ -5,26 +5,20 @@ import "@admin/app.css";
5
5
 
6
6
  import { mount } from "svelte";
7
7
 
8
- import type { AdminPlugin, FieldPlugin } from "@riducms/plugin";
9
8
  import type { RiduClient, RiduConfigShape } from "@riducms/sdk";
10
- import type { TranslationLanguage } from "@riducms/translations";
11
9
 
12
10
  import App from "@admin/app.svelte";
13
11
  import type { AdminClient } from "@admin/core/api/admin-client";
14
12
 
15
- export interface MountAdminOptions<Config extends RiduConfigShape> {
13
+ import type { AdminConfig } from "@riducms/plugin/admin";
14
+
15
+ export interface MountAdminOptions<Config extends RiduConfigShape> extends AdminConfig {
16
16
  /** Existing DOM element that receives the Ridu admin application. */
17
17
  target: Element;
18
18
  /** Creates the generated, application-specific Fetch client. */
19
19
  clientFactory: () => RiduClient<Config>;
20
20
  /** Route prefix used when the admin is not mounted at /admin. */
21
21
  adminBasePath?: string;
22
- /** Statically paired packaged plugins, including fields and routes. */
23
- plugins?: readonly AdminPlugin[];
24
- /** Application-local field renderers that have no packaged backend pair. */
25
- fieldPlugins?: readonly FieldPlugin[];
26
- /** Statically imported catalogs available to the Go-configured admin languages. */
27
- languages?: readonly TranslationLanguage[];
28
22
  }
29
23
 
30
24
  export function mountAdmin<Config extends RiduConfigShape>(
@@ -38,10 +32,8 @@ export function mountAdmin<Config extends RiduConfigShape>(
38
32
  target: options.target,
39
33
  props: {
40
34
  clientFactory,
35
+ adminConfig: options,
41
36
  ...(options.adminBasePath === undefined ? {} : { adminBasePath: options.adminBasePath }),
42
- ...(options.plugins === undefined ? {} : { plugins: options.plugins }),
43
- ...(options.fieldPlugins === undefined ? {} : { fieldPlugins: options.fieldPlugins }),
44
- ...(options.languages === undefined ? {} : { languages: options.languages }),
45
37
  },
46
38
  });
47
39
  }