@riducms/admin 0.1.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.
Files changed (226) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/THIRD_PARTY_NOTICES.md +217 -0
  4. package/package.json +62 -0
  5. package/src/app/admin-core-view-route.svelte +89 -0
  6. package/src/app/admin-layout.svelte +457 -0
  7. package/src/app/admin-navigation-progress.svelte +204 -0
  8. package/src/app/admin-page-title.svelte +94 -0
  9. package/src/app/admin-provider-layer.svelte +26 -0
  10. package/src/app/admin-router-root.svelte +9 -0
  11. package/src/app/admin-routes.svelte +116 -0
  12. package/src/app/deferred-redirect.svelte +23 -0
  13. package/src/app/loading-boundary.svelte +70 -0
  14. package/src/app/not-found-route.svelte +29 -0
  15. package/src/app-meta.ts +2 -0
  16. package/src/app.css +467 -0
  17. package/src/app.svelte +88 -0
  18. package/src/assets/ridu-logo-arch.svg +3 -0
  19. package/src/assets/ridu-word-logo.svg +7 -0
  20. package/src/components/brand/ridu-logo.svelte +24 -0
  21. package/src/components/code-block/code-block.svelte +107 -0
  22. package/src/components/json-tree/json-tree-node.svelte +77 -0
  23. package/src/components/json-tree/json-viewer.svelte +113 -0
  24. package/src/components/ui/banner/banner.svelte +41 -0
  25. package/src/components/ui/banner/index.ts +5 -0
  26. package/src/components/ui/bulk-action-bar/bulk-action-bar.svelte +50 -0
  27. package/src/components/ui/bulk-action-bar/index.ts +1 -0
  28. package/src/components/ui/button/index.ts +8 -0
  29. package/src/components/ui/calendar/calendar-caption.svelte +72 -0
  30. package/src/components/ui/calendar/calendar-cell.svelte +20 -0
  31. package/src/components/ui/calendar/calendar-day.svelte +27 -0
  32. package/src/components/ui/calendar/calendar-grid-row.svelte +13 -0
  33. package/src/components/ui/calendar/calendar-grid.svelte +17 -0
  34. package/src/components/ui/calendar/calendar-head-cell.svelte +20 -0
  35. package/src/components/ui/calendar/calendar-header.svelte +20 -0
  36. package/src/components/ui/calendar/calendar-heading.svelte +17 -0
  37. package/src/components/ui/calendar/calendar-month-select.svelte +45 -0
  38. package/src/components/ui/calendar/calendar-month.svelte +16 -0
  39. package/src/components/ui/calendar/calendar-months.svelte +20 -0
  40. package/src/components/ui/calendar/calendar-nav.svelte +20 -0
  41. package/src/components/ui/calendar/calendar-next-button.svelte +30 -0
  42. package/src/components/ui/calendar/calendar-prev-button.svelte +30 -0
  43. package/src/components/ui/calendar/calendar-year-select.svelte +44 -0
  44. package/src/components/ui/calendar/calendar.svelte +118 -0
  45. package/src/components/ui/calendar/index.ts +1 -0
  46. package/src/components/ui/checkbox/checkbox.svelte +35 -0
  47. package/src/components/ui/checkbox/index.ts +1 -0
  48. package/src/components/ui/confirmation-dialog/confirmation-dialog.svelte +37 -0
  49. package/src/components/ui/confirmation-dialog/index.ts +1 -0
  50. package/src/components/ui/date-value-control/date-value-calendar.svelte +37 -0
  51. package/src/components/ui/date-value-control/date-value-control-loader.svelte +36 -0
  52. package/src/components/ui/date-value-control/date-value-control.svelte +177 -0
  53. package/src/components/ui/date-value-control/index.ts +3 -0
  54. package/src/components/ui/dialog/dialog-close.svelte +11 -0
  55. package/src/components/ui/dialog/dialog-content.svelte +52 -0
  56. package/src/components/ui/dialog/dialog-description.svelte +20 -0
  57. package/src/components/ui/dialog/dialog-footer.svelte +20 -0
  58. package/src/components/ui/dialog/dialog-header.svelte +20 -0
  59. package/src/components/ui/dialog/dialog-overlay.svelte +20 -0
  60. package/src/components/ui/dialog/dialog-title.svelte +17 -0
  61. package/src/components/ui/dialog/dialog-trigger.svelte +11 -0
  62. package/src/components/ui/dialog/dialog.svelte +7 -0
  63. package/src/components/ui/dialog/index.ts +8 -0
  64. package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +27 -0
  65. package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +20 -0
  66. package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
  67. package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +16 -0
  68. package/src/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
  69. package/src/components/ui/dropdown-menu/index.ts +5 -0
  70. package/src/components/ui/empty-state/empty-state.svelte +34 -0
  71. package/src/components/ui/empty-state/index.ts +1 -0
  72. package/src/components/ui/input/index.ts +1 -0
  73. package/src/components/ui/kbd/index.ts +1 -0
  74. package/src/components/ui/kbd/kbd.svelte +24 -0
  75. package/src/components/ui/pagination/index.ts +1 -0
  76. package/src/components/ui/pagination/pagination.svelte +88 -0
  77. package/src/components/ui/popover/index.ts +3 -0
  78. package/src/components/ui/popover/popover-content.svelte +29 -0
  79. package/src/components/ui/popover/popover-trigger.svelte +11 -0
  80. package/src/components/ui/popover/popover.svelte +7 -0
  81. package/src/components/ui/radio-group/index.ts +3 -0
  82. package/src/components/ui/radio-group/radio-card-item.svelte +38 -0
  83. package/src/components/ui/radio-group/radio-group-item.svelte +17 -0
  84. package/src/components/ui/radio-group/radio-group.svelte +19 -0
  85. package/src/components/ui/select/index.ts +4 -0
  86. package/src/components/ui/select/select-content.svelte +38 -0
  87. package/src/components/ui/select/select-item.svelte +40 -0
  88. package/src/components/ui/select/select-scroll-down-button.svelte +23 -0
  89. package/src/components/ui/select/select-scroll-up-button.svelte +23 -0
  90. package/src/components/ui/select/select-trigger.svelte +29 -0
  91. package/src/components/ui/select/select.svelte +11 -0
  92. package/src/components/ui/sheet/index.ts +6 -0
  93. package/src/components/ui/sheet/sheet-close.svelte +7 -0
  94. package/src/components/ui/sheet/sheet-content.svelte +77 -0
  95. package/src/components/ui/sheet/sheet-description.svelte +17 -0
  96. package/src/components/ui/sheet/sheet-overlay.svelte +36 -0
  97. package/src/components/ui/sheet/sheet-title.svelte +17 -0
  98. package/src/components/ui/sheet/sheet-trigger.svelte +7 -0
  99. package/src/components/ui/sheet/sheet.svelte +7 -0
  100. package/src/components/ui/skeleton/index.ts +1 -0
  101. package/src/components/ui/skeleton/skeleton.svelte +31 -0
  102. package/src/components/ui/slider/index.ts +3 -0
  103. package/src/components/ui/slider/slider-content.svelte +59 -0
  104. package/src/components/ui/slider/slider.svelte +35 -0
  105. package/src/components/ui/sonner/index.ts +1 -0
  106. package/src/components/ui/sonner/sonner.svelte +55 -0
  107. package/src/components/ui/status-indicator/index.ts +5 -0
  108. package/src/components/ui/status-indicator/status-indicator.svelte +54 -0
  109. package/src/components/ui/table/index.ts +6 -0
  110. package/src/components/ui/table/table-body.svelte +20 -0
  111. package/src/components/ui/table/table-cell.svelte +23 -0
  112. package/src/components/ui/table/table-head.svelte +23 -0
  113. package/src/components/ui/table/table-header.svelte +20 -0
  114. package/src/components/ui/table/table-row.svelte +23 -0
  115. package/src/components/ui/table/table.svelte +22 -0
  116. package/src/components/ui/tabs/index.ts +8 -0
  117. package/src/components/ui/tabs/tabs-content.svelte +17 -0
  118. package/src/components/ui/tabs/tabs-list.svelte +40 -0
  119. package/src/components/ui/tabs/tabs-trigger.svelte +23 -0
  120. package/src/components/ui/tabs/tabs.svelte +19 -0
  121. package/src/components/ui/textarea/index.ts +1 -0
  122. package/src/components/ui/tooltip/index.ts +4 -0
  123. package/src/components/ui/tooltip/tooltip-content.svelte +50 -0
  124. package/src/components/ui/tooltip/tooltip-provider.svelte +7 -0
  125. package/src/components/ui/tooltip/tooltip-trigger.svelte +7 -0
  126. package/src/components/ui/tooltip/tooltip.svelte +7 -0
  127. package/src/core/api/admin-client.ts +45 -0
  128. package/src/core/forms/field-condition.ts +63 -0
  129. package/src/core/forms/form-controller.svelte.ts +369 -0
  130. package/src/core/forms/form-draft-recovery.ts +80 -0
  131. package/src/core/forms/form-schema.ts +609 -0
  132. package/src/core/forms/form-validation.ts +750 -0
  133. package/src/core/i18n/admin-i18n.svelte.ts +364 -0
  134. package/src/core/i18n/localized-schema.ts +111 -0
  135. package/src/core/notifications/notification-center.svelte.ts +69 -0
  136. package/src/core/notifications/validation-toast.svelte +18 -0
  137. package/src/core/plugins/field-registry.ts +60 -0
  138. package/src/core/plugins/row-label-registry.ts +62 -0
  139. package/src/core/preferences/preference-write-queue.ts +107 -0
  140. package/src/core/routing/admin-paths.ts +137 -0
  141. package/src/core/runtime/admin-runtime.svelte.ts +625 -0
  142. package/src/features/auth/account-menu.svelte +159 -0
  143. package/src/features/auth/account-navigation.svelte +44 -0
  144. package/src/features/auth/account-route.svelte +395 -0
  145. package/src/features/auth/account-security-controller.svelte.ts +119 -0
  146. package/src/features/auth/account-security-route.svelte +460 -0
  147. package/src/features/auth/auth-brand.svelte +17 -0
  148. package/src/features/auth/auth-frame.svelte +18 -0
  149. package/src/features/auth/create-first-user-controller.svelte.ts +220 -0
  150. package/src/features/auth/create-first-user-route.svelte +174 -0
  151. package/src/features/auth/forgot-password-route.svelte +78 -0
  152. package/src/features/auth/login-route.svelte +221 -0
  153. package/src/features/auth/request-verification-route.svelte +73 -0
  154. package/src/features/auth/reset-password-route.svelte +107 -0
  155. package/src/features/auth/verify-email-route.svelte +60 -0
  156. package/src/features/collections/collection-list-bulk-actions.svelte +143 -0
  157. package/src/features/collections/collection-list-bulk-editor.svelte +157 -0
  158. package/src/features/collections/collection-list-controller.svelte.ts +717 -0
  159. package/src/features/collections/collection-list-filter-popover.svelte +238 -0
  160. package/src/features/collections/collection-list-preferences-controller.svelte.ts +393 -0
  161. package/src/features/collections/collection-list-results.svelte +410 -0
  162. package/src/features/collections/collection-list-route.svelte +951 -0
  163. package/src/features/collections/collection-list-workspace-toolbar.svelte +409 -0
  164. package/src/features/collections/collection-table-skeleton.svelte +44 -0
  165. package/src/features/collections/list-workspace.ts +265 -0
  166. package/src/features/dashboard/dashboard-route.svelte +141 -0
  167. package/src/features/development/development-tools.svelte +217 -0
  168. package/src/features/documents/document-api-view-loader.svelte +31 -0
  169. package/src/features/documents/document-api-view.svelte +547 -0
  170. package/src/features/documents/document-controller.svelte.ts +1252 -0
  171. package/src/features/documents/document-field-partition.ts +18 -0
  172. package/src/features/documents/document-field-sections.svelte +46 -0
  173. package/src/features/documents/document-heading.ts +20 -0
  174. package/src/features/documents/document-route-dirty.ts +43 -0
  175. package/src/features/documents/document-route-loader.svelte +22 -0
  176. package/src/features/documents/document-route.svelte +1124 -0
  177. package/src/features/documents/live-preview-panel.svelte +390 -0
  178. package/src/features/documents/live-preview.ts +61 -0
  179. package/src/features/documents/upload-document-preview-loader.svelte +40 -0
  180. package/src/features/documents/upload-document-preview.svelte +525 -0
  181. package/src/features/localization/content-locale-switcher.svelte +93 -0
  182. package/src/features/navigation/admin-breadcrumbs.svelte +124 -0
  183. package/src/features/navigation/admin-command-menu.svelte +245 -0
  184. package/src/features/navigation/admin-resource-groups.ts +72 -0
  185. package/src/features/reference-browser/reference-browser-loader.svelte +15 -0
  186. package/src/features/reference-browser/reference-browser-workflow.svelte.ts +691 -0
  187. package/src/features/reference-browser/reference-browser.svelte +844 -0
  188. package/src/features/uploads/bulk-upload-route.svelte +624 -0
  189. package/src/features/uploads/bulk-upload.ts +39 -0
  190. package/src/features/uploads/upload-metadata-fields.svelte +112 -0
  191. package/src/features/versions/version-diff.ts +72 -0
  192. package/src/features/versions/version-history-route.svelte +581 -0
  193. package/src/fields/field-clipboard.ts +132 -0
  194. package/src/fields/field-layout.svelte +60 -0
  195. package/src/fields/field-layout.ts +57 -0
  196. package/src/fields/field-messages.svelte +29 -0
  197. package/src/fields/field-renderer.svelte +152 -0
  198. package/src/fields/field-shell.svelte +30 -0
  199. package/src/fields/field-tab-layout.svelte +99 -0
  200. package/src/fields/join/join-field.svelte +358 -0
  201. package/src/fields/localized-field-presentation.ts +17 -0
  202. package/src/fields/nested/nested-field.svelte +602 -0
  203. package/src/fields/nested/scoped-field.ts +78 -0
  204. package/src/fields/nested/sortable-row.svelte +32 -0
  205. package/src/fields/point/point-field.svelte +52 -0
  206. package/src/fields/radio/radio-field.svelte +34 -0
  207. package/src/fields/relationship/relationship-field-controller.svelte.ts +245 -0
  208. package/src/fields/relationship/relationship-field.svelte +258 -0
  209. package/src/fields/relationship/relationship-lookup-controller.svelte.ts +129 -0
  210. package/src/fields/relationship/relationship-query.ts +88 -0
  211. package/src/fields/relationship/relationship-quick-picker-controller.svelte.ts +198 -0
  212. package/src/fields/relationship/relationship-quick-picker.svelte +327 -0
  213. package/src/fields/relationship/relationship-value.ts +47 -0
  214. package/src/fields/scalar/date-control-value.ts +60 -0
  215. package/src/fields/scalar/date-value.ts +26 -0
  216. package/src/fields/scalar/scalar-field.svelte +137 -0
  217. package/src/fields/select/select-field.svelte +132 -0
  218. package/src/fields/select/select-value.ts +14 -0
  219. package/src/fields/text/slug-field.svelte +87 -0
  220. package/src/fields/text/slug.ts +38 -0
  221. package/src/fields/text/text-field.svelte +41 -0
  222. package/src/fields/ui/ui-field.svelte +12 -0
  223. package/src/fields/virtual/virtual-field.svelte +28 -0
  224. package/src/index.ts +49 -0
  225. package/src/main.ts +12 -0
  226. package/src/vite-env.d.ts +9 -0
@@ -0,0 +1,132 @@
1
+ import type { SchemaField } from "@riducms/protocol";
2
+
3
+ export type FieldClipboardKind = "field" | "row";
4
+
5
+ export interface FieldClipboardPayload {
6
+ version: 1;
7
+ kind: FieldClipboardKind;
8
+ signature: string;
9
+ value: unknown;
10
+ }
11
+
12
+ const clipboardPrefix = "ridu-field-clipboard:";
13
+ const maximumClipboardLength = 1_000_000;
14
+ let memoryClipboard: FieldClipboardPayload | undefined;
15
+
16
+ export function fieldClipboardSignature(field: SchemaField): string {
17
+ return JSON.stringify(fieldShape(field));
18
+ }
19
+
20
+ export function createFieldClipboardPayload(
21
+ field: SchemaField,
22
+ kind: FieldClipboardKind,
23
+ value: unknown
24
+ ): FieldClipboardPayload {
25
+ return {
26
+ version: 1,
27
+ kind,
28
+ signature: fieldClipboardSignature(field),
29
+ value: cloneJSON(value),
30
+ };
31
+ }
32
+
33
+ export function parseFieldClipboardPayload(encoded: string): FieldClipboardPayload | undefined {
34
+ if (!encoded.startsWith(clipboardPrefix) || encoded.length > maximumClipboardLength)
35
+ return undefined;
36
+ try {
37
+ const candidate = JSON.parse(
38
+ encoded.slice(clipboardPrefix.length)
39
+ ) as Partial<FieldClipboardPayload>;
40
+ if (
41
+ candidate.version !== 1 ||
42
+ (candidate.kind !== "field" && candidate.kind !== "row") ||
43
+ typeof candidate.signature !== "string" ||
44
+ !("value" in candidate)
45
+ )
46
+ return undefined;
47
+ return candidate as FieldClipboardPayload;
48
+ } catch {
49
+ return undefined;
50
+ }
51
+ }
52
+
53
+ export function compatibleClipboardValue(
54
+ payload: FieldClipboardPayload | undefined,
55
+ field: SchemaField,
56
+ kind: FieldClipboardKind
57
+ ) {
58
+ if (
59
+ payload === undefined ||
60
+ payload.kind !== kind ||
61
+ payload.signature !== fieldClipboardSignature(field)
62
+ )
63
+ return undefined;
64
+ return cloneForPaste(payload.value);
65
+ }
66
+
67
+ export async function writeFieldClipboard(payload: FieldClipboardPayload) {
68
+ memoryClipboard = payload;
69
+ try {
70
+ await navigator.clipboard.writeText(clipboardPrefix + JSON.stringify(payload));
71
+ } catch {
72
+ // The in-memory copy keeps same-admin workflows available when a browser
73
+ // denies clipboard permission or the app is served from an insecure origin.
74
+ }
75
+ }
76
+
77
+ export async function readFieldClipboard() {
78
+ try {
79
+ const encoded = await navigator.clipboard.readText();
80
+ const parsed = parseFieldClipboardPayload(encoded);
81
+ if (parsed !== undefined) {
82
+ memoryClipboard = parsed;
83
+ return parsed;
84
+ }
85
+ } catch {
86
+ // Fall through to the last trusted in-app copy.
87
+ }
88
+ return memoryClipboard;
89
+ }
90
+
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
+ );
100
+ }
101
+
102
+ function cloneJSON(value: unknown) {
103
+ return value === undefined ? null : JSON.parse(JSON.stringify(value));
104
+ }
105
+
106
+ function fieldShape(field: SchemaField): unknown {
107
+ return {
108
+ type: field.type,
109
+ category: field.category,
110
+ select: field.select?.choices.map((choice) => choice.value),
111
+ relationship: field.relationship && {
112
+ collection: field.relationship.collectionSlug,
113
+ targets: field.relationship.targets?.map((target) => target.collectionSlug),
114
+ hasMany: field.relationship.hasMany === true,
115
+ polymorphic: field.relationship.polymorphic === true,
116
+ },
117
+ upload: field.upload && {
118
+ collection: field.upload.collectionSlug,
119
+ hasMany: field.upload.hasMany === true,
120
+ },
121
+ nested: field.nested && {
122
+ minRows: field.nested.minRows ?? 0,
123
+ maxRows: field.nested.maxRows ?? 0,
124
+ fields: field.nested.fields.map((child) => ({ name: child.name, shape: fieldShape(child) })),
125
+ },
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 },
131
+ };
132
+ }
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ import type { SchemaField } from "@riducms/protocol";
3
+
4
+ import type { FormController } from "@admin/core/forms/form-controller.svelte";
5
+ import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
6
+ import FieldRenderer from "@admin/fields/field-renderer.svelte";
7
+ import { createFieldLayout } from "@admin/fields/field-layout";
8
+ import FieldTabLayout from "@admin/fields/field-tab-layout.svelte";
9
+
10
+ let {
11
+ fields,
12
+ form,
13
+ suppressedTabGroupID = "",
14
+ }: {
15
+ fields: readonly SchemaField[];
16
+ form: FormController;
17
+ suppressedTabGroupID?: string;
18
+ } = $props();
19
+ const runtime = getAdminRuntime();
20
+ </script>
21
+
22
+ {#snippet fieldGrid(tabFields: readonly SchemaField[])}
23
+ <div class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-12">
24
+ {#each createFieldLayout(tabFields, suppressedTabGroupID) as group (group.key)}
25
+ {#if group.tabGroup !== undefined}
26
+ <FieldTabLayout fields={group.fields} {form} tabGroup={group.tabGroup} />
27
+ {: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>
44
+ {:else if group.row !== undefined}
45
+ <div
46
+ class="col-span-1 grid grid-cols-1 gap-x-4 gap-y-6 sm:col-span-12 sm:grid-cols-12"
47
+ data-field-row={group.row.id}
48
+ >
49
+ {#each group.fields as field (field.id)}
50
+ <FieldRenderer {field} {form} />
51
+ {/each}
52
+ </div>
53
+ {:else}
54
+ <FieldRenderer field={group.fields[0]} {form} />
55
+ {/if}
56
+ {/each}
57
+ </div>
58
+ {/snippet}
59
+
60
+ {@render fieldGrid(fields)}
@@ -0,0 +1,57 @@
1
+ import type {
2
+ SchemaField,
3
+ SchemaFieldCollapsible,
4
+ SchemaFieldRow,
5
+ SchemaFieldTabGroup,
6
+ } from "@riducms/protocol";
7
+
8
+ export interface FieldLayoutGroup {
9
+ key: string;
10
+ row?: SchemaFieldRow;
11
+ collapsible?: SchemaFieldCollapsible;
12
+ tabGroup?: SchemaFieldTabGroup;
13
+ fields: [SchemaField, ...SchemaField[]];
14
+ }
15
+
16
+ // Row metadata is deliberately carried by ordinary data fields. Grouping it
17
+ // here preserves the manifest's field order without introducing a form value.
18
+ export function createFieldLayout(
19
+ fields: readonly SchemaField[],
20
+ suppressedTabGroupID = ""
21
+ ): FieldLayoutGroup[] {
22
+ const groups: FieldLayoutGroup[] = [];
23
+ for (const field of fields) {
24
+ const row = field.admin.row;
25
+ const collapsible = field.admin.collapsible;
26
+ const tabGroup =
27
+ field.admin.tabGroup?.id === suppressedTabGroupID ? undefined : field.admin.tabGroup;
28
+ const previous = groups.at(-1);
29
+ if (tabGroup !== undefined && previous?.tabGroup?.id === tabGroup.id) {
30
+ previous.fields.push(field);
31
+ continue;
32
+ }
33
+ if (collapsible !== undefined && previous?.collapsible?.id === collapsible.id) {
34
+ previous.fields.push(field);
35
+ continue;
36
+ }
37
+ if (row !== undefined && previous?.row?.id === row.id) {
38
+ previous.fields.push(field);
39
+ continue;
40
+ }
41
+ groups.push({
42
+ key:
43
+ tabGroup !== undefined
44
+ ? `tabs:${tabGroup.id}:${field.id}`
45
+ : collapsible !== undefined
46
+ ? `collapsible:${collapsible.id}`
47
+ : row === undefined
48
+ ? `field:${field.id}`
49
+ : `row:${row.id}`,
50
+ ...(row === undefined ? {} : { row }),
51
+ ...(collapsible === undefined ? {} : { collapsible }),
52
+ ...(tabGroup === undefined ? {} : { tabGroup }),
53
+ fields: [field],
54
+ });
55
+ }
56
+ return groups;
57
+ }
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ import type { ValidationIssue } from "@riducms/protocol";
3
+ import CircleAlertIcon from "~icons/lucide/circle-alert";
4
+
5
+ let {
6
+ id,
7
+ issues = [],
8
+ description,
9
+ class: className,
10
+ }: {
11
+ id: string;
12
+ issues?: readonly ValidationIssue[];
13
+ description?: string;
14
+ class?: string;
15
+ } = $props();
16
+ </script>
17
+
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}
@@ -0,0 +1,152 @@
1
+ <script lang="ts">
2
+ import type { FieldAuthoringHost } from "@riducms/plugin";
3
+ import type { SchemaField } from "@riducms/protocol";
4
+
5
+ import type { FormController } from "@admin/core/forms/form-controller.svelte";
6
+ import { evaluateFieldCondition } from "@admin/core/forms/field-condition";
7
+ import { localizeSchemaCollection, localizeSchemaField } from "@admin/core/i18n/localized-schema";
8
+ import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
9
+ import { fieldAccessPath } from "@admin/fields/nested/scoped-field";
10
+ import ReferenceBrowser from "@admin/features/reference-browser/reference-browser-loader.svelte";
11
+ import {
12
+ contentLocaleLabel,
13
+ withContentLocaleLabel,
14
+ } from "@admin/fields/localized-field-presentation";
15
+
16
+ import SelectField from "@admin/fields/select/select-field.svelte";
17
+ import RadioField from "@admin/fields/radio/radio-field.svelte";
18
+ import PointField from "@admin/fields/point/point-field.svelte";
19
+ import UIField from "@admin/fields/ui/ui-field.svelte";
20
+ import JoinField from "@admin/fields/join/join-field.svelte";
21
+ import VirtualField from "@admin/fields/virtual/virtual-field.svelte";
22
+ import ScalarField from "@admin/fields/scalar/scalar-field.svelte";
23
+ import NestedField from "@admin/fields/nested/nested-field.svelte";
24
+ import RelationshipField from "@admin/fields/relationship/relationship-field.svelte";
25
+ import TextField from "@admin/fields/text/text-field.svelte";
26
+
27
+ interface Props {
28
+ field: SchemaField;
29
+ form: FormController;
30
+ }
31
+
32
+ let { field, form }: Props = $props();
33
+ const runtime = getAdminRuntime();
34
+ const authoring: FieldAuthoringHost = {
35
+ get collections() {
36
+ return (
37
+ runtime.manifest?.collections.map((collection) =>
38
+ localizeSchemaCollection(collection, runtime.i18n)
39
+ ) ?? []
40
+ );
41
+ },
42
+ get documentRevision() {
43
+ return runtime.documentRevision;
44
+ },
45
+ get locale() {
46
+ return form.contentLocale;
47
+ },
48
+ referenceBrowser: ReferenceBrowser,
49
+ findDocument: (collection, id, signal) =>
50
+ 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
+ };
59
+ const plugin = $derived(runtime.fields.resolve(field));
60
+ const accessPath = $derived(fieldAccessPath(field));
61
+ const visible = $derived(
62
+ field.admin.hidden !== true &&
63
+ form.canRead(field.path, accessPath) &&
64
+ (field.admin.condition === undefined ||
65
+ evaluateFieldCondition(field.admin.condition, field.path, (path) => form.get(path)))
66
+ );
67
+ const renderedField = $derived(
68
+ field.type === "join" || field.admin.readOnly || form.canWrite(field.path, accessPath)
69
+ ? field
70
+ : { ...field, admin: { ...field.admin, readOnly: true } }
71
+ );
72
+ const localizedField = $derived(localizeSchemaField(renderedField, runtime.i18n));
73
+ const activeContentLocaleLabel = $derived(
74
+ contentLocaleLabel(runtime.contentLocales, form.contentLocale)
75
+ );
76
+ const presentedField = $derived(withContentLocaleLabel(localizedField, activeContentLocaleLabel));
77
+ const columnClasses: Record<number, string> = {
78
+ 1: "col-span-1 sm:col-span-1",
79
+ 2: "col-span-1 sm:col-span-2",
80
+ 3: "col-span-1 sm:col-span-3",
81
+ 4: "col-span-1 sm:col-span-4",
82
+ 5: "col-span-1 sm:col-span-5",
83
+ 6: "col-span-1 sm:col-span-6",
84
+ 7: "col-span-1 sm:col-span-7",
85
+ 8: "col-span-1 sm:col-span-8",
86
+ 9: "col-span-1 sm:col-span-9",
87
+ 10: "col-span-1 sm:col-span-10",
88
+ 11: "col-span-1 sm:col-span-11",
89
+ 12: "col-span-1 sm:col-span-12",
90
+ };
91
+ const columns = $derived(Math.min(12, Math.max(1, field.admin.columns ?? 12)));
92
+ const columnClass = $derived(columnClasses[columns] ?? columnClasses[12]);
93
+ const inheritedFrom = $derived(
94
+ form.isInherited(field.path) ? form.localizationSource(field.path) : undefined
95
+ );
96
+ const inheritedFromLabel = $derived(contentLocaleLabel(runtime.contentLocales, inheritedFrom));
97
+ const missingLocale = $derived(
98
+ field.localized === true &&
99
+ form.contentLocale !== undefined &&
100
+ form.get(field.path) === undefined
101
+ );
102
+
103
+ function renderField() {
104
+ if (plugin.component !== undefined) return plugin.component;
105
+
106
+ switch (plugin.type) {
107
+ case "text":
108
+ return TextField;
109
+ case "select":
110
+ return SelectField;
111
+ case "radio":
112
+ return RadioField;
113
+ case "point":
114
+ return PointField;
115
+ case "ui":
116
+ return UIField;
117
+ case "join":
118
+ return JoinField;
119
+ case "virtual":
120
+ return VirtualField;
121
+ case "group":
122
+ case "array":
123
+ case "blocks":
124
+ return NestedField;
125
+ case "relationship":
126
+ case "upload":
127
+ return RelationshipField;
128
+ default:
129
+ return ScalarField;
130
+ }
131
+ }
132
+ </script>
133
+
134
+ {#if visible}<div class={columnClass}>
135
+ {const RenderField = $derived(renderField())}
136
+ <RenderField field={presentedField} {form} {authoring} i18n={runtime.i18n} />
137
+ {#if inheritedFrom !== undefined}<p
138
+ class="mt-1 font-mono text-[10px] text-foreground-faint"
139
+ data-localization-source={inheritedFrom}
140
+ >
141
+ {runtime.i18n.t("documents:inheritedFrom", {
142
+ locale: inheritedFromLabel ?? inheritedFrom,
143
+ })}
144
+ </p>{:else if missingLocale}<p
145
+ class="mt-1 font-mono text-[10px] text-foreground-faint"
146
+ data-localization-missing={form.contentLocale}
147
+ >
148
+ {runtime.i18n.t("documents:missingTranslation", {
149
+ locale: activeContentLocaleLabel ?? form.contentLocale ?? "",
150
+ })}
151
+ </p>{/if}
152
+ </div>{/if}
@@ -0,0 +1,30 @@
1
+ <script lang="ts">
2
+ import type { SchemaField, ValidationIssue } from "@riducms/protocol";
3
+ import type { Snippet } from "svelte";
4
+
5
+ import { FieldFrame } from "@riducms/ui";
6
+
7
+ let {
8
+ field,
9
+ issues = [],
10
+ children,
11
+ }: {
12
+ field: SchemaField;
13
+ issues?: ValidationIssue[];
14
+ children: Snippet;
15
+ } = $props();
16
+ const errorMessages = $derived(issues.map((issue) => issue.message));
17
+ </script>
18
+
19
+ <div data-field-path={field.path}>
20
+ <FieldFrame
21
+ controlID={field.id}
22
+ label={field.admin.label}
23
+ required={field.required}
24
+ readOnly={field.admin.readOnly}
25
+ description={field.admin.description}
26
+ errors={errorMessages}
27
+ >
28
+ {@render children()}
29
+ </FieldFrame>
30
+ </div>
@@ -0,0 +1,99 @@
1
+ <script lang="ts">
2
+ import type { SchemaField, SchemaFieldTabGroup } from "@riducms/protocol";
3
+
4
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@admin/components/ui/tabs";
5
+ import type { FormController } from "@admin/core/forms/form-controller.svelte";
6
+ import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
7
+ import FieldLayout from "@admin/fields/field-layout.svelte";
8
+
9
+ let {
10
+ fields,
11
+ form,
12
+ tabGroup,
13
+ }: {
14
+ fields: readonly SchemaField[];
15
+ form: FormController;
16
+ tabGroup: SchemaFieldTabGroup;
17
+ } = $props();
18
+ const runtime = getAdminRuntime();
19
+ const fallbackTab = $derived(runtime.i18n.t("fields:content"));
20
+ const tabs = $derived.by(() => {
21
+ const entries: Array<{ key: string; label: string }> = [];
22
+ for (const field of fields) {
23
+ const key = field.admin.tab ?? "";
24
+ if (entries.some((entry) => entry.key === key)) continue;
25
+ entries.push({
26
+ key,
27
+ label:
28
+ field.admin.tab === undefined
29
+ ? fallbackTab
30
+ : runtime.i18n.text(field.admin.tab, field.admin.tabTranslations),
31
+ });
32
+ }
33
+ return entries;
34
+ });
35
+ const accessibleLabel = $derived(
36
+ runtime.i18n.t("fields:sections", {
37
+ labels: runtime.i18n.formatList(tabs.map((tab) => tab.label)),
38
+ count: tabs.length,
39
+ })
40
+ );
41
+ let selectedTab = $state("");
42
+ let revealedIssueSignature = $state("");
43
+ const activeTab = $derived(
44
+ tabs.some((tab) => tab.key === selectedTab) ? selectedTab : (tabs[0]?.key ?? "")
45
+ );
46
+
47
+ function issueCountForTab(tab: string) {
48
+ return fields.filter(
49
+ (field) => (field.admin.tab ?? "") === tab && form.issuesFor(field.path).length > 0
50
+ ).length;
51
+ }
52
+
53
+ $effect(() => {
54
+ const signature = form.issues.map((issue) => `${issue.code}:${issue.path}`).join("|");
55
+ if (signature === "" || signature === revealedIssueSignature) return;
56
+ revealedIssueSignature = signature;
57
+ const issueTab = tabs.find((tab) => issueCountForTab(tab.key) > 0);
58
+ if (issueTab !== undefined) selectedTab = issueTab.key;
59
+ });
60
+ </script>
61
+
62
+ <Tabs
63
+ class="col-span-1 mb-1 gap-0 sm:col-span-12"
64
+ value={activeTab}
65
+ onValueChange={(tab) => (selectedTab = tab)}
66
+ data-field-tab-group={tabGroup.id}
67
+ >
68
+ <TabsList
69
+ variant="line"
70
+ class="-ms-5 h-12 w-[calc(100%+2.5rem)] justify-start gap-6 overflow-x-auto overflow-y-hidden border-b border-control-border px-5 sm:-ms-8 sm:w-[calc(100%+4rem)] sm:px-8 lg:-ms-[60px] lg:w-[calc(100%+120px)] lg:px-[60px]"
71
+ aria-label={accessibleLabel}
72
+ >
73
+ {#each tabs as tab (tab.key)}
74
+ {const issueCount = $derived(issueCountForTab(tab.key))}
75
+ <TabsTrigger
76
+ class="h-12 flex-none rounded-none px-px text-[14px] font-medium focus-visible:bg-control focus-visible:outline-none data-[state=active]:border-b-foreground-strong!"
77
+ value={tab.key}
78
+ >
79
+ {tab.label}{#if issueCount > 0}<span
80
+ class="font-mono grid min-w-4 place-items-center rounded-full bg-destructive/14 px-1 text-[9px] text-destructive"
81
+ aria-label={runtime.i18n.t("fields:invalidCount", { count: issueCount })}
82
+ >
83
+ {runtime.i18n.formatNumber(issueCount)}
84
+ </span>{/if}
85
+ </TabsTrigger>
86
+ {/each}
87
+ </TabsList>
88
+ {#each tabs as tab (tab.key)}
89
+ <TabsContent value={tab.key} class="pt-6">
90
+ {#if activeTab === tab.key}
91
+ <FieldLayout
92
+ fields={fields.filter((field) => (field.admin.tab ?? "") === tab.key)}
93
+ {form}
94
+ suppressedTabGroupID={tabGroup.id}
95
+ />
96
+ {/if}
97
+ </TabsContent>
98
+ {/each}
99
+ </Tabs>