@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,265 @@
1
+ import type { SchemaCollection, SchemaField } from "@riducms/protocol";
2
+ import type { AdminI18n } from "@riducms/translations";
3
+
4
+ export const listPageSizes = [10, 25, 50, 100] as const;
5
+
6
+ export type ListPageSize = (typeof listPageSizes)[number];
7
+ export type ListFilterOperator =
8
+ | "equals"
9
+ | "notEquals"
10
+ | "like"
11
+ | "contains"
12
+ | "greaterThan"
13
+ | "greaterThanEqual"
14
+ | "lessThan"
15
+ | "lessThanEqual"
16
+ | "exists";
17
+
18
+ export interface ListFilter {
19
+ field: string;
20
+ operator: ListFilterOperator;
21
+ value: string;
22
+ }
23
+
24
+ export interface ListWorkspacePreference {
25
+ columns: string[];
26
+ showStatus: boolean;
27
+ showID: boolean;
28
+ showCreated: boolean;
29
+ showUpdated: boolean;
30
+ limit: ListPageSize;
31
+ }
32
+
33
+ const textOperators: readonly ListFilterOperator[] = [
34
+ "like",
35
+ "equals",
36
+ "notEquals",
37
+ "contains",
38
+ "exists",
39
+ ];
40
+ const orderedOperators: readonly ListFilterOperator[] = [
41
+ "equals",
42
+ "notEquals",
43
+ "greaterThan",
44
+ "greaterThanEqual",
45
+ "lessThan",
46
+ "lessThanEqual",
47
+ "exists",
48
+ ];
49
+ const identityOperators: readonly ListFilterOperator[] = ["equals", "notEquals", "exists"];
50
+
51
+ export function listColumnFields(collection: SchemaCollection | undefined, titleName?: string) {
52
+ return (collection?.fields ?? []).flatMap((field) => {
53
+ if (field.name === titleName || field.type === "ui") return [];
54
+ if (field.type === "group") return nestedFields(field, "columns");
55
+ return [field];
56
+ });
57
+ }
58
+
59
+ export function filterableFields(fields: readonly SchemaField[]) {
60
+ return fields.flatMap((field) => {
61
+ if (field.type === "group" || field.type === "array" || field.type === "blocks") {
62
+ return nestedFields(field, "filters");
63
+ }
64
+ return filterableLeaf(field) ? [field] : [];
65
+ });
66
+ }
67
+
68
+ export function sortableField(field: SchemaField) {
69
+ return (
70
+ field.virtual === undefined &&
71
+ field.join === undefined &&
72
+ field.type !== "array" &&
73
+ field.type !== "blocks" &&
74
+ field.type !== "group" &&
75
+ field.type !== "json" &&
76
+ field.plugin === undefined &&
77
+ field.type !== "point" &&
78
+ field.type !== "code"
79
+ );
80
+ }
81
+
82
+ export function defaultListColumns(
83
+ collection: SchemaCollection | undefined,
84
+ fields: readonly SchemaField[]
85
+ ) {
86
+ const byName = new Map(fields.map((field) => [field.path, field.path]));
87
+ for (const field of fields) {
88
+ if (field.path === field.name && !byName.has(field.name)) byName.set(field.name, field.path);
89
+ }
90
+ const configured = (collection?.admin.defaultColumns ?? []).flatMap((name) => {
91
+ const path = byName.get(name);
92
+ return path === undefined ? [] : [path];
93
+ });
94
+ if (configured.length > 0) return configured;
95
+ return fields.slice(0, 2).map((field) => field.path);
96
+ }
97
+
98
+ export function filterOperatorsFor(field: SchemaField): readonly ListFilterOperator[] {
99
+ if (field.type === "number" || field.type === "date") return orderedOperators;
100
+ if (field.type === "text" || field.type === "textarea" || field.type === "email") {
101
+ return textOperators;
102
+ }
103
+ return identityOperators;
104
+ }
105
+
106
+ export function filterOperatorLabel(operator: ListFilterOperator, i18n: AdminI18n) {
107
+ const labels: Record<ListFilterOperator, Parameters<AdminI18n["t"]>[0]> = {
108
+ equals: "collections:operatorEquals",
109
+ notEquals: "collections:operatorNotEquals",
110
+ like: "collections:operatorLike",
111
+ contains: "collections:operatorContains",
112
+ greaterThan: "collections:operatorGreaterThan",
113
+ greaterThanEqual: "collections:operatorGreaterThanEqual",
114
+ lessThan: "collections:operatorLessThan",
115
+ lessThanEqual: "collections:operatorLessThanEqual",
116
+ exists: "collections:operatorExists",
117
+ };
118
+ return i18n.t(labels[operator]);
119
+ }
120
+
121
+ export function normalizeListFilters(value: unknown, fields: readonly SchemaField[]): ListFilter[] {
122
+ if (!Array.isArray(value)) return [];
123
+ const byName = new Map(fields.map((field) => [field.path, field]));
124
+ return value.flatMap((candidate) => {
125
+ if (typeof candidate !== "object" || candidate === null) return [];
126
+ const fieldName = Reflect.get(candidate, "field");
127
+ const operator = Reflect.get(candidate, "operator");
128
+ const rawValue = Reflect.get(candidate, "value");
129
+ if (typeof fieldName !== "string" || typeof operator !== "string") return [];
130
+ const field = byName.get(fieldName);
131
+ if (
132
+ field === undefined ||
133
+ !filterOperatorsFor(field).includes(operator as ListFilterOperator)
134
+ ) {
135
+ return [];
136
+ }
137
+ return [
138
+ {
139
+ field: fieldName,
140
+ operator: operator as ListFilterOperator,
141
+ value:
142
+ typeof rawValue === "string" ||
143
+ typeof rawValue === "number" ||
144
+ typeof rawValue === "boolean"
145
+ ? String(rawValue)
146
+ : "",
147
+ },
148
+ ];
149
+ });
150
+ }
151
+
152
+ export function parseListFilters(encoded: string | null, fields: readonly SchemaField[]) {
153
+ if (encoded === null || encoded === "") return [];
154
+ try {
155
+ return normalizeListFilters(JSON.parse(encoded), fields);
156
+ } catch {
157
+ return [];
158
+ }
159
+ }
160
+
161
+ export function buildListFilterWhere(
162
+ filters: readonly ListFilter[],
163
+ fields: readonly SchemaField[]
164
+ ) {
165
+ const byName = new Map(fields.map((field) => [field.path, field]));
166
+ return filters.flatMap((filter): Record<string, unknown>[] => {
167
+ const field = byName.get(filter.field);
168
+ if (field === undefined) return [];
169
+ if (filter.operator === "exists") {
170
+ return [{ [filter.field]: { exists: filter.value !== "false" } }];
171
+ }
172
+ if (filter.value === "") return [];
173
+ let value: string | number | boolean = filter.value;
174
+ if (field.type === "number") {
175
+ value = Number(filter.value);
176
+ if (!Number.isFinite(value)) return [];
177
+ } else if (field.type === "checkbox") {
178
+ value = filter.value === "true";
179
+ }
180
+ return [{ [filter.field]: { [filter.operator]: value } }];
181
+ });
182
+ }
183
+
184
+ export function parseListPageSize(value: string | null, fallback: ListPageSize = 25): ListPageSize {
185
+ const parsed = Number(value);
186
+ return listPageSizes.includes(parsed as ListPageSize) ? (parsed as ListPageSize) : fallback;
187
+ }
188
+
189
+ export function normalizeWorkspacePreference(
190
+ value: unknown,
191
+ fields: readonly SchemaField[],
192
+ fallback: ListWorkspacePreference
193
+ ): ListWorkspacePreference {
194
+ if (typeof value !== "object" || value === null) return fallback;
195
+ const eligible = new Set(fields.map((field) => field.path));
196
+ const rawColumns = Reflect.get(value, "columns");
197
+ const columns = Array.isArray(rawColumns)
198
+ ? rawColumns.filter((name): name is string => typeof name === "string" && eligible.has(name))
199
+ : fallback.columns;
200
+ const showStatus = Reflect.get(value, "showStatus");
201
+ const showID = Reflect.get(value, "showID");
202
+ const showCreated = Reflect.get(value, "showCreated");
203
+ const showUpdated = Reflect.get(value, "showUpdated");
204
+ const rawLimit = Reflect.get(value, "limit");
205
+ return {
206
+ columns,
207
+ showStatus: typeof showStatus === "boolean" ? showStatus : fallback.showStatus,
208
+ showID: typeof showID === "boolean" ? showID : fallback.showID,
209
+ showCreated: typeof showCreated === "boolean" ? showCreated : fallback.showCreated,
210
+ showUpdated: typeof showUpdated === "boolean" ? showUpdated : fallback.showUpdated,
211
+ limit: parseListPageSize(
212
+ typeof rawLimit === "number" ? String(rawLimit) : null,
213
+ fallback.limit
214
+ ),
215
+ };
216
+ }
217
+
218
+ export function visibleColumnNames(encoded: string | null, fallback: readonly string[]) {
219
+ if (encoded === null) return [...fallback];
220
+ const names = encoded.split(",").filter(Boolean);
221
+ return [...new Set(names)];
222
+ }
223
+
224
+ function nestedFields(field: SchemaField, mode: "columns" | "filters"): SchemaField[] {
225
+ if (field.type === "blocks") {
226
+ if (mode === "columns") return [];
227
+ return (field.blocks?.types ?? []).flatMap((block) =>
228
+ block.fields.flatMap((child) =>
229
+ withListLabel(child, `${field.admin.label} > ${block.label}`, mode)
230
+ )
231
+ );
232
+ }
233
+ return (field.nested?.fields ?? []).flatMap((child) =>
234
+ withListLabel(child, field.admin.label, mode)
235
+ );
236
+ }
237
+
238
+ function withListLabel(
239
+ field: SchemaField,
240
+ prefix: string,
241
+ mode: "columns" | "filters"
242
+ ): SchemaField[] {
243
+ const labelled = {
244
+ ...field,
245
+ admin: { ...field.admin, label: `${prefix} > ${field.admin.label}` },
246
+ };
247
+ if (field.type === "group" || (mode === "filters" && field.type === "array")) {
248
+ return nestedFields(labelled, mode);
249
+ }
250
+ if (field.type === "blocks") return mode === "filters" ? nestedFields(labelled, mode) : [];
251
+ if (mode === "filters" && !filterableLeaf(field)) return [];
252
+ return [labelled];
253
+ }
254
+
255
+ function filterableLeaf(field: SchemaField) {
256
+ return (
257
+ field.virtual === undefined &&
258
+ field.join === undefined &&
259
+ field.plugin === undefined &&
260
+ field.type !== "ui" &&
261
+ field.type !== "json" &&
262
+ field.type !== "point" &&
263
+ field.type !== "code"
264
+ );
265
+ }
@@ -0,0 +1,141 @@
1
+ <script lang="ts">
2
+ import { Link } from "@hvniel/svelte-router";
3
+ import ArrowRightIcon from "~icons/lucide/arrow-right";
4
+ import PlusIcon from "~icons/lucide/plus";
5
+ import { TooltipContent, TooltipRoot, TooltipTrigger } from "@riducms/ui";
6
+
7
+ import { collectionPath, createDocumentPath, globalPath } from "@admin/core/routing/admin-paths";
8
+ import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
9
+ import { groupAdminResources } from "@admin/features/navigation/admin-resource-groups";
10
+
11
+ const runtime = getAdminRuntime();
12
+ const manifest = $derived(runtime.manifest);
13
+ const resourceGroups = $derived(
14
+ groupAdminResources(runtime.visibleCollections, runtime.visibleGlobals, {
15
+ collections: runtime.i18n.t("navigation:collections"),
16
+ globals: runtime.i18n.t("navigation:globals"),
17
+ })
18
+ );
19
+ const replacement = $derived(
20
+ runtime.dashboardPanels.find((panel) => panel.position === "replace")
21
+ );
22
+ const beforePanels = $derived(
23
+ runtime.dashboardPanels.filter((panel) => panel.position === "before")
24
+ );
25
+ const afterPanels = $derived(
26
+ runtime.dashboardPanels.filter(
27
+ (panel) => panel.position === undefined || panel.position === "after"
28
+ )
29
+ );
30
+ </script>
31
+
32
+ {#if manifest !== undefined && replacement !== undefined}
33
+ <replacement.component {manifest} user={runtime.session?.user} i18n={runtime.i18n} />
34
+ {:else}<section class="w-full px-5 pb-7 sm:px-8 sm:pb-9 lg:px-15">
35
+ {#if manifest !== undefined && beforePanels.length > 0}
36
+ <div class="grid gap-3" aria-label={runtime.i18n.t("dashboard:extensions")}>
37
+ {#each beforePanels as panel (panel.key)}
38
+ <panel.component {manifest} user={runtime.session?.user} i18n={runtime.i18n} />
39
+ {/each}
40
+ </div>
41
+ {/if}
42
+
43
+ <div class="mt-9 grid gap-9">
44
+ {#each resourceGroups as group (group.key)}
45
+ {const groupLabel = $derived(runtime.i18n.text(group.label, group.translations))}
46
+ <section aria-label={groupLabel}>
47
+ <div class="mb-3 flex items-end justify-between gap-4">
48
+ <h2 class="text-[18px] font-medium text-foreground-strong">
49
+ {groupLabel}
50
+ </h2>
51
+ </div>
52
+ <div class="grid grid-cols-[repeat(auto-fill,minmax(min(260px,100%),260px))] gap-2.5">
53
+ {#each group.collections as collection (collection.id)}
54
+ {const pluralLabel = $derived(
55
+ runtime.i18n.text(collection.labels.plural, collection.labels.pluralTranslations)
56
+ )}
57
+ {const singularLabel = $derived(
58
+ runtime.i18n.text(
59
+ collection.labels.singular,
60
+ collection.labels.singularTranslations
61
+ )
62
+ )}
63
+ <article
64
+ class="group relative flex min-h-20 items-center gap-3 rounded-[3px] bg-control px-4 py-3.5 transition-colors hover:bg-control-hover"
65
+ >
66
+ <Link
67
+ class="absolute inset-0 rounded-[3px] focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2"
68
+ to={collectionPath(collection.slug)}
69
+ aria-label={runtime.i18n.t("dashboard:open", {
70
+ label: pluralLabel,
71
+ })}
72
+ />
73
+ <h3 class="min-w-0 flex-1 text-[14px] font-medium text-foreground-strong">
74
+ {pluralLabel}
75
+ </h3>
76
+ {#if runtime.collectionOperations[collection.slug]?.create}
77
+ <TooltipRoot>
78
+ <TooltipTrigger>
79
+ {#snippet child({ props })}
80
+ <Link
81
+ {...props}
82
+ class="relative z-1 grid size-8 shrink-0 place-items-center rounded-[3px] text-foreground-muted hover:bg-background-layer hover:text-foreground-strong"
83
+ to={createDocumentPath(collection.slug)}
84
+ aria-label={runtime.i18n.t("dashboard:create", {
85
+ label: singularLabel.toLocaleLowerCase(runtime.i18n.language),
86
+ })}
87
+ >
88
+ <PlusIcon class="size-4" />
89
+ </Link>
90
+ {/snippet}
91
+ </TooltipTrigger>
92
+ <TooltipContent>
93
+ {runtime.i18n.t("dashboard:create", {
94
+ label: singularLabel,
95
+ })}
96
+ </TooltipContent>
97
+ </TooltipRoot>
98
+ {:else}
99
+ <ArrowRightIcon
100
+ class="size-4 text-foreground-faint rtl:rotate-180"
101
+ aria-hidden="true"
102
+ />
103
+ {/if}
104
+ </article>
105
+ {/each}
106
+ {#each group.globals as global (global.id)}
107
+ {const globalLabel = $derived(
108
+ runtime.i18n.text(global.labels.singular, global.labels.singularTranslations)
109
+ )}
110
+ <article
111
+ class="group relative flex min-h-20 items-center gap-3 rounded-[3px] bg-control px-4 py-3.5 transition-colors hover:bg-control-hover"
112
+ >
113
+ <Link
114
+ class="absolute inset-0 rounded-[3px] focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2"
115
+ to={globalPath(global.slug)}
116
+ aria-label={runtime.i18n.t("dashboard:open", {
117
+ label: globalLabel,
118
+ })}
119
+ />
120
+ <h3 class="min-w-0 flex-1 text-[14px] font-medium text-foreground-strong">
121
+ {globalLabel}
122
+ </h3>
123
+ <ArrowRightIcon
124
+ class="size-4 text-foreground-faint rtl:rotate-180"
125
+ aria-hidden="true"
126
+ />
127
+ </article>
128
+ {/each}
129
+ </div>
130
+ </section>
131
+ {/each}
132
+ </div>
133
+
134
+ {#if manifest !== undefined && afterPanels.length > 0}
135
+ <div class="mt-8 grid gap-3" aria-label={runtime.i18n.t("dashboard:extensions")}>
136
+ {#each afterPanels as panel (panel.key)}
137
+ <panel.component {manifest} user={runtime.session?.user} i18n={runtime.i18n} />
138
+ {/each}
139
+ </div>
140
+ {/if}
141
+ </section>{/if}
@@ -0,0 +1,217 @@
1
+ <script lang="ts">
2
+ import BookOpenIcon from "~icons/lucide/book-open";
3
+ import ExternalLinkIcon from "~icons/lucide/external-link";
4
+ import LoaderCircleIcon from "~icons/lucide/loader-circle";
5
+ import { onDestroy } from "svelte";
6
+
7
+ import RiduLogo from "@admin/components/brand/ridu-logo.svelte";
8
+ import { Button } from "@admin/components/ui/button";
9
+ import { Popover, PopoverContent, PopoverTrigger } from "@admin/components/ui/popover";
10
+ import { StatusIndicator } from "@admin/components/ui/status-indicator";
11
+ import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
12
+ import { cn } from "@riducms/ui";
13
+
14
+ type SchemaUpdateNotice = {
15
+ tone: "success" | "error";
16
+ label: string;
17
+ };
18
+
19
+ const runtime = getAdminRuntime();
20
+ let updateApplied = $state(false);
21
+ let notice = $state<SchemaUpdateNotice>();
22
+ let popoverOpen = $state(false);
23
+ let recoveryAvailable = $state(runtime.manifestRefreshError !== undefined);
24
+ let refreshObserved = false;
25
+ let noticeTimer: number | undefined;
26
+ const status = $derived(
27
+ runtime.refreshingManifest
28
+ ? {
29
+ label: runtime.i18n.t("navigation:applyingSchemaUpdate"),
30
+ tone: "live" as const,
31
+ }
32
+ : runtime.manifestRefreshError !== undefined
33
+ ? {
34
+ label: runtime.i18n.t("development:schemaUpdateFailed"),
35
+ tone: "destructive" as const,
36
+ }
37
+ : updateApplied
38
+ ? {
39
+ label: runtime.i18n.t("development:upToDate"),
40
+ tone: "success" as const,
41
+ }
42
+ : {
43
+ label: runtime.i18n.t("development:ready"),
44
+ tone: "live" as const,
45
+ }
46
+ );
47
+
48
+ function hideNotice() {
49
+ if (noticeTimer !== undefined) window.clearTimeout(noticeTimer);
50
+ notice = undefined;
51
+ noticeTimer = undefined;
52
+ }
53
+
54
+ function showNotice(next: SchemaUpdateNotice, duration: number) {
55
+ hideNotice();
56
+ notice = next;
57
+ noticeTimer = window.setTimeout(() => {
58
+ notice = undefined;
59
+ noticeTimer = undefined;
60
+ }, duration);
61
+ }
62
+
63
+ onDestroy(() => {
64
+ if (noticeTimer !== undefined) window.clearTimeout(noticeTimer);
65
+ });
66
+
67
+ function retrySchema() {
68
+ if (runtime.loading || runtime.refreshingManifest) return;
69
+ void runtime.refreshManifest();
70
+ }
71
+
72
+ $effect(() => {
73
+ const refreshing = runtime.refreshingManifest;
74
+ const error = runtime.manifestRefreshError;
75
+ if (refreshing) {
76
+ refreshObserved = true;
77
+ hideNotice();
78
+ updateApplied = false;
79
+ return;
80
+ }
81
+ if (!refreshObserved) return;
82
+ refreshObserved = false;
83
+
84
+ if (error !== undefined) {
85
+ recoveryAvailable = true;
86
+ showNotice({ tone: "error", label: runtime.i18n.t("development:schemaUpdateFailed") }, 7_000);
87
+ } else {
88
+ showNotice(
89
+ { tone: "success", label: runtime.i18n.t("development:schemaUpdateApplied") },
90
+ 4_500
91
+ );
92
+ updateApplied = true;
93
+ if (recoveryAvailable) popoverOpen = false;
94
+ recoveryAvailable = false;
95
+ }
96
+ });
97
+ </script>
98
+
99
+ <div class="fixed end-3 bottom-3 z-30" data-ridu-development-tools>
100
+ {#if notice !== undefined}
101
+ <div
102
+ class={cn(
103
+ "ridu-popover-enter pointer-events-none absolute end-full bottom-0 me-2 flex h-9 max-w-[calc(100vw-4.5rem)] items-center gap-2 whitespace-nowrap rounded-[8px] border bg-popover px-3 text-[12.5px] font-medium shadow-[var(--shadow-popover)]",
104
+ notice.tone === "error"
105
+ ? "border-destructive/30 text-destructive"
106
+ : "border-success/30 text-success"
107
+ )}
108
+ role={notice.tone === "error" ? "alert" : "status"}
109
+ data-ridu-development-toast
110
+ data-tone={notice.tone}
111
+ >
112
+ <span class="size-1.75 shrink-0 rounded-full bg-current" aria-hidden="true"></span>
113
+ <span class="truncate">{notice.label}</span>
114
+ </div>
115
+ {/if}
116
+
117
+ <Popover bind:open={popoverOpen}>
118
+ <PopoverTrigger
119
+ class={cn(
120
+ "relative inline-grid size-9 cursor-pointer place-items-center rounded-full border bg-popover text-popover-foreground shadow-[var(--shadow-popover)] transition-[background-color,border-color,color] duration-150 outline-none hover:bg-control-hover focus-visible:outline-2 focus-visible:outline-ring/70 focus-visible:outline-offset-2",
121
+ runtime.manifestRefreshError !== undefined
122
+ ? "border-destructive/55"
123
+ : updateApplied
124
+ ? "border-success/55"
125
+ : "border-control-border"
126
+ )}
127
+ aria-label={runtime.i18n.t("development:toolsStatus", { status: status.label })}
128
+ title={runtime.i18n.t("development:tools")}
129
+ data-ridu-development-trigger
130
+ >
131
+ {#if runtime.refreshingManifest}
132
+ <LoaderCircleIcon
133
+ class="size-4 animate-spin text-primary motion-reduce:animate-none"
134
+ aria-hidden="true"
135
+ />
136
+ {:else}
137
+ <RiduLogo variant="arch" class="h-4 text-foreground" />
138
+ {/if}
139
+ <span
140
+ class={cn(
141
+ "absolute top-0 end-0 size-2.5 rounded-full border-2 border-popover",
142
+ runtime.manifestRefreshError !== undefined
143
+ ? "bg-destructive"
144
+ : updateApplied
145
+ ? "bg-success"
146
+ : "bg-primary"
147
+ )}
148
+ aria-hidden="true"
149
+ ></span>
150
+ </PopoverTrigger>
151
+
152
+ <PopoverContent
153
+ class="w-[min(18rem,calc(100vw-1.5rem))] gap-0 overflow-hidden p-0"
154
+ side="top"
155
+ align="end"
156
+ sideOffset={8}
157
+ role="dialog"
158
+ aria-label={runtime.i18n.t("development:tools")}
159
+ data-ridu-development-panel
160
+ >
161
+ <div class="flex items-center gap-2.5 border-b border-control-border px-3.5 py-3">
162
+ <RiduLogo variant="arch" class="h-4 text-foreground" />
163
+ <p class="text-[13px] font-semibold text-foreground">
164
+ {runtime.i18n.t("development:tools")}
165
+ </p>
166
+ </div>
167
+
168
+ <div class="grid gap-2.5 px-3.5 py-3">
169
+ <div class="flex items-center justify-between gap-4">
170
+ <span class="font-mono text-[10px] tracking-[0.12em] text-foreground-faint uppercase">
171
+ {runtime.i18n.t("development:schema")}
172
+ </span>
173
+ <StatusIndicator tone={status.tone} pulse={runtime.refreshingManifest}>
174
+ {status.label}
175
+ </StatusIndicator>
176
+ </div>
177
+ {#if runtime.manifestRefreshError !== undefined}
178
+ <p class="text-[12px] leading-5 text-destructive">
179
+ {runtime.manifestRefreshError}
180
+ </p>
181
+ {/if}
182
+ {#if runtime.manifestRefreshError !== undefined || recoveryAvailable}
183
+ <Button
184
+ class="w-fit"
185
+ variant="outline"
186
+ size="sm"
187
+ aria-disabled={runtime.refreshingManifest}
188
+ aria-busy={runtime.refreshingManifest}
189
+ onclick={retrySchema}
190
+ >
191
+ {runtime.refreshingManifest
192
+ ? runtime.i18n.t("navigation:applyingSchemaUpdate")
193
+ : runtime.i18n.t("development:retrySchemaUpdate")}
194
+ </Button>
195
+ {/if}
196
+ </div>
197
+
198
+ <a
199
+ class="flex items-center gap-2.5 border-t border-control-border px-3.5 py-3 text-foreground-muted outline-none transition-colors hover:bg-control-hover hover:text-foreground focus-visible:bg-control-hover focus-visible:text-foreground"
200
+ href="https://riducms.com"
201
+ target="_blank"
202
+ rel="noreferrer"
203
+ >
204
+ <BookOpenIcon class="size-3.5 shrink-0" aria-hidden="true" />
205
+ <span class="min-w-0 flex-1">
206
+ <span class="block text-[12.5px] font-medium">
207
+ {runtime.i18n.t("development:documentation")}
208
+ </span>
209
+ <span class="block text-[11px] text-foreground-faint">
210
+ {runtime.i18n.t("development:documentationDescription")}
211
+ </span>
212
+ </span>
213
+ <ExternalLinkIcon class="size-3.5 shrink-0" aria-hidden="true" />
214
+ </a>
215
+ </PopoverContent>
216
+ </Popover>
217
+ </div>
@@ -0,0 +1,31 @@
1
+ <script lang="ts">
2
+ import { getAdminI18n } from "@riducms/plugin";
3
+
4
+ type DocumentAPIViewProps = {
5
+ resourceSlug: string;
6
+ resourceLabel: string;
7
+ documentID?: string;
8
+ globalResource?: boolean;
9
+ fallbackValue: unknown;
10
+ };
11
+
12
+ let props: DocumentAPIViewProps = $props();
13
+ const i18n = getAdminI18n();
14
+ const view = import("@admin/features/documents/document-api-view.svelte");
15
+ </script>
16
+
17
+ {#await view}
18
+ <section
19
+ class="grid min-h-72 place-items-center"
20
+ aria-busy="true"
21
+ aria-label={i18n.t("documents:loadingAPIView")}
22
+ >
23
+ <p class="text-[12px] text-foreground-faint">{i18n.t("documents:loadingAPIView")}</p>
24
+ </section>
25
+ {:then { default: DocumentAPIView }}
26
+ <DocumentAPIView {...props} />
27
+ {:catch}
28
+ <section class="grid min-h-72 place-items-center" role="alert">
29
+ <p class="text-[12px] text-destructive">{i18n.t("documents:apiViewLoadFailed")}</p>
30
+ </section>
31
+ {/await}