@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
+ <script lang="ts">
2
+ import type { SchemaField } from "@riducms/protocol";
3
+ import XIcon from "~icons/lucide/x";
4
+
5
+ import { Button } from "@admin/components/ui/button";
6
+ import { Select, SelectContent, SelectItem, SelectTrigger } from "@admin/components/ui/select";
7
+ import type { FormController } from "@admin/core/forms/form-controller.svelte";
8
+ import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
9
+ import FieldShell from "@admin/fields/field-shell.svelte";
10
+ import {
11
+ removeSelectValue,
12
+ selectChoiceLabel,
13
+ selectManyValues,
14
+ } from "@admin/fields/select/select-value";
15
+
16
+ interface Props {
17
+ field: SchemaField;
18
+ form: FormController;
19
+ }
20
+
21
+ let { field, form }: Props = $props();
22
+ const runtime = getAdminRuntime();
23
+ const hasMany = $derived(field.type === "select" && field.select?.hasMany === true);
24
+ const value = $derived(String(form.get(field.path) ?? ""));
25
+ const values = $derived(selectManyValues(form.get(field.path)));
26
+ const placeholder = $derived(
27
+ field.admin.placeholder ?? runtime.i18n.t("fields:select", { label: field.admin.label })
28
+ );
29
+ const selectedLabel = $derived(
30
+ field.select?.choices.find((choice) => choice.value === value)?.label ?? placeholder
31
+ );
32
+ const selectedLabels = $derived(
33
+ values.map((selected) => selectChoiceLabel(field.select?.choices ?? [], selected))
34
+ );
35
+ const issues = $derived(form.issuesFor(field.path));
36
+ const hasMessage = $derived(issues.length > 0 || field.admin.description !== undefined);
37
+
38
+ $effect(() => form.register(field.path));
39
+
40
+ function setValues(next: string[]) {
41
+ form.set(field.path, [...next]);
42
+ }
43
+
44
+ function removeValue(selected: string) {
45
+ form.set(field.path, removeSelectValue(values, selected));
46
+ }
47
+ </script>
48
+
49
+ <FieldShell {field} {issues}>
50
+ {#if hasMany}
51
+ <Select
52
+ type="multiple"
53
+ value={values}
54
+ onValueChange={setValues}
55
+ disabled={field.admin.readOnly}
56
+ >
57
+ <SelectTrigger
58
+ id={field.id}
59
+ class="w-full"
60
+ aria-invalid={issues.length > 0}
61
+ aria-required={field.required}
62
+ aria-describedby={hasMessage ? `${field.id}-message` : undefined}
63
+ aria-errormessage={issues.length > 0 ? `${field.id}-message` : undefined}
64
+ aria-label={field.admin.label}
65
+ >
66
+ <span class={values.length === 0 ? "text-foreground-placeholder" : "truncate"}>
67
+ {values.length === 0 ? placeholder : runtime.i18n.formatList(selectedLabels)}
68
+ </span>
69
+ </SelectTrigger>
70
+ <SelectContent>
71
+ {#each field.select?.choices ?? [] as choice (choice.value)}
72
+ <SelectItem value={choice.value} label={choice.label} />
73
+ {/each}
74
+ </SelectContent>
75
+ </Select>
76
+ {#if values.length > 0}
77
+ <ul
78
+ class="mt-2 flex flex-wrap gap-1.5"
79
+ aria-label={runtime.i18n.t("general:selected", { count: values.length })}
80
+ >
81
+ {#each values as selected, index (`${selected}:${index}`)}
82
+ <li
83
+ 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
+ >
85
+ <span class="max-w-64 truncate">
86
+ {selectChoiceLabel(field.select?.choices ?? [], selected)}
87
+ </span>
88
+ {#if !field.admin.readOnly}
89
+ <Button
90
+ variant="ghost"
91
+ size="icon-xs"
92
+ onclick={() => removeValue(selected)}
93
+ aria-label={runtime.i18n.t("fields:remove", {
94
+ label: selectChoiceLabel(field.select?.choices ?? [], selected),
95
+ })}
96
+ >
97
+ <XIcon class="size-3" aria-hidden="true" />
98
+ </Button>
99
+ {/if}
100
+ </li>
101
+ {/each}
102
+ </ul>
103
+ {/if}
104
+ {:else}
105
+ <Select
106
+ type="single"
107
+ {value}
108
+ allowDeselect={!field.required}
109
+ onValueChange={(next) => form.set(field.path, next)}
110
+ disabled={field.admin.readOnly}
111
+ >
112
+ <SelectTrigger
113
+ id={field.id}
114
+ class="w-full"
115
+ aria-invalid={issues.length > 0}
116
+ aria-required={field.required}
117
+ aria-describedby={hasMessage ? `${field.id}-message` : undefined}
118
+ aria-errormessage={issues.length > 0 ? `${field.id}-message` : undefined}
119
+ aria-label={field.admin.label}
120
+ >
121
+ <span class={value === "" ? "text-foreground-placeholder" : undefined}>
122
+ {selectedLabel}
123
+ </span>
124
+ </SelectTrigger>
125
+ <SelectContent>
126
+ {#each field.select?.choices ?? [] as choice (choice.value)}
127
+ <SelectItem value={choice.value} label={choice.label} />
128
+ {/each}
129
+ </SelectContent>
130
+ </Select>
131
+ {/if}
132
+ </FieldShell>
@@ -0,0 +1,14 @@
1
+ import type { SchemaSelectChoice } from "@riducms/protocol";
2
+
3
+ export function selectManyValues(value: unknown): string[] {
4
+ if (!Array.isArray(value)) return [];
5
+ return value.filter((candidate): candidate is string => typeof candidate === "string");
6
+ }
7
+
8
+ export function selectChoiceLabel(choices: readonly SchemaSelectChoice[], value: string) {
9
+ return choices.find((choice) => choice.value === value)?.label ?? value;
10
+ }
11
+
12
+ export function removeSelectValue(values: readonly string[], value: string) {
13
+ return values.filter((candidate) => candidate !== value);
14
+ }
@@ -0,0 +1,87 @@
1
+ <script lang="ts">
2
+ import type { SchemaField } from "@riducms/protocol";
3
+ import { untrack } from "svelte";
4
+
5
+ import { Button } from "@admin/components/ui/button";
6
+ import { Input } from "@admin/components/ui/input";
7
+ import type {
8
+ DerivedTextBinding,
9
+ FormController,
10
+ } from "@admin/core/forms/form-controller.svelte";
11
+ import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
12
+ import FieldShell from "@admin/fields/field-shell.svelte";
13
+ import { normalizeSlug, slugFollowsSource } from "@admin/fields/text/slug";
14
+
15
+ interface Props {
16
+ field: SchemaField;
17
+ form: FormController;
18
+ }
19
+
20
+ let { field, form }: Props = $props();
21
+ const runtime = getAdminRuntime();
22
+ const value = $derived(String(form.get(field.path) ?? ""));
23
+ const issues = $derived(form.issuesFor(field.path));
24
+ const hasMessage = $derived(issues.length > 0 || field.admin.description !== undefined);
25
+ let locked = $state(true);
26
+ let binding = $state<DerivedTextBinding>();
27
+
28
+ $effect(() => {
29
+ form.revision;
30
+ const sourcePath = field.text?.slug?.sourcePath;
31
+ const fieldPath = field.path;
32
+ if (sourcePath === undefined) return;
33
+ return untrack(() => {
34
+ binding = form.bindDerivedText(
35
+ fieldPath,
36
+ sourcePath,
37
+ (source) => normalizeSlug(String(source ?? "")),
38
+ (current, source) => String(current ?? "") === "" || slugFollowsSource(current, source)
39
+ );
40
+ });
41
+ });
42
+
43
+ function generate() {
44
+ binding?.follow();
45
+ }
46
+
47
+ function normalizeManualValue() {
48
+ if (!locked) binding?.setManual(normalizeSlug(value));
49
+ }
50
+
51
+ function setManualValue(value: string) {
52
+ binding?.setManual(value);
53
+ }
54
+ </script>
55
+
56
+ <FieldShell {field} {issues}>
57
+ <div class="flex items-center gap-2">
58
+ <Input
59
+ id={field.id}
60
+ name={field.path}
61
+ required={field.required}
62
+ minlength={field.text?.minLength}
63
+ maxlength={field.text?.maxLength}
64
+ 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}
69
+ class="min-w-0 flex-1 font-mono"
70
+ {value}
71
+ oninput={(event) => setManualValue(event.currentTarget.value)}
72
+ onblur={normalizeManualValue}
73
+ />
74
+ {#if !field.admin.readOnly}
75
+ <div class="flex shrink-0 items-center gap-1">
76
+ {#if !locked}
77
+ <Button type="button" size="xs" variant="ghost" onclick={generate}>
78
+ {runtime.i18n.t("fields:generateSlug")}
79
+ </Button>
80
+ {/if}
81
+ <Button type="button" size="xs" variant="ghost" onclick={() => (locked = !locked)}>
82
+ {runtime.i18n.t(locked ? "fields:unlockSlug" : "fields:lockSlug")}
83
+ </Button>
84
+ </div>
85
+ {/if}
86
+ </div>
87
+ </FieldShell>
@@ -0,0 +1,38 @@
1
+ export function normalizeSlug(value: string) {
2
+ let normalized = "";
3
+ let pendingSeparator = false;
4
+ for (const character of value.trim()) {
5
+ const code = character.charCodeAt(0);
6
+ if (code >= 65 && code <= 90) {
7
+ if (pendingSeparator && normalized.length > 0) normalized += "-";
8
+ normalized += String.fromCharCode(code + 32);
9
+ pendingSeparator = false;
10
+ continue;
11
+ }
12
+ if ((code >= 97 && code <= 122) || (code >= 48 && code <= 57) || character === "_") {
13
+ if (pendingSeparator && normalized.length > 0) normalized += "-";
14
+ normalized += character;
15
+ pendingSeparator = false;
16
+ continue;
17
+ }
18
+ if (character === "-" || isASCIISpace(character)) {
19
+ pendingSeparator = normalized.length > 0;
20
+ }
21
+ }
22
+ return normalized;
23
+ }
24
+
25
+ export function slugFollowsSource(slug: unknown, source: unknown) {
26
+ return String(slug ?? "") === normalizeSlug(String(source ?? ""));
27
+ }
28
+
29
+ function isASCIISpace(character: string) {
30
+ return (
31
+ character === " " ||
32
+ character === "\t" ||
33
+ character === "\n" ||
34
+ character === "\r" ||
35
+ character === "\f" ||
36
+ character === "\v"
37
+ );
38
+ }
@@ -0,0 +1,41 @@
1
+ <script lang="ts">
2
+ import type { SchemaField } from "@riducms/protocol";
3
+
4
+ import { Input } from "@admin/components/ui/input";
5
+ import type { FormController } from "@admin/core/forms/form-controller.svelte";
6
+ import FieldShell from "@admin/fields/field-shell.svelte";
7
+ import SlugField from "@admin/fields/text/slug-field.svelte";
8
+
9
+ interface Props {
10
+ field: SchemaField;
11
+ form: FormController;
12
+ }
13
+
14
+ let { field, form }: Props = $props();
15
+ const value = $derived(String(form.get(field.path) ?? ""));
16
+ const issues = $derived(form.issuesFor(field.path));
17
+ const hasMessage = $derived(issues.length > 0 || field.admin.description !== undefined);
18
+
19
+ $effect(() => form.register(field.path));
20
+ </script>
21
+
22
+ {#if field.text?.slug !== undefined}
23
+ <SlugField {field} {form} />
24
+ {:else}
25
+ <FieldShell {field} {issues}>
26
+ <Input
27
+ id={field.id}
28
+ name={field.path}
29
+ required={field.required}
30
+ minlength={field.text?.minLength}
31
+ maxlength={field.text?.maxLength}
32
+ 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
+ {value}
38
+ oninput={(event) => form.set(field.path, event.currentTarget.value)}
39
+ />
40
+ </FieldShell>
41
+ {/if}
@@ -0,0 +1,12 @@
1
+ <script lang="ts">
2
+ import type { SchemaField } from "@riducms/protocol";
3
+
4
+ let { field }: { field: SchemaField } = $props();
5
+ </script>
6
+
7
+ <aside class="border-s-2 border-control-border py-0.5 ps-3" data-ui-field={field.name}>
8
+ <p class="text-[13px] font-medium text-foreground">{field.admin.label}</p>
9
+ {#if field.admin.description}<p class="mt-1 text-[12.5px] leading-5 text-foreground-muted">
10
+ {field.admin.description}
11
+ </p>{/if}
12
+ </aside>
@@ -0,0 +1,28 @@
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 FieldShell from "@admin/fields/field-shell.svelte";
7
+
8
+ let { field, form }: { field: SchemaField; form: FormController } = $props();
9
+ const runtime = getAdminRuntime();
10
+ const value = $derived(form.get(field.path));
11
+ const formatted = $derived(
12
+ value === undefined || value === null
13
+ ? runtime.i18n.t("general:none")
14
+ : typeof value === "object"
15
+ ? JSON.stringify(value, null, 2)
16
+ : String(value)
17
+ );
18
+ </script>
19
+
20
+ <FieldShell {field} issues={[]}>
21
+ <output
22
+ id={field.id}
23
+ class="block min-h-10 whitespace-pre-wrap rounded-[3px] border border-control-border-disabled bg-control-disabled px-3 py-2.5 text-[13px] leading-5 text-foreground-label"
24
+ data-virtual-field={field.name}
25
+ >
26
+ {formatted}
27
+ </output>
28
+ </FieldShell>
package/src/index.ts ADDED
@@ -0,0 +1,49 @@
1
+ import "@fontsource-variable/geist/wght.css";
2
+ import "@fontsource-variable/spline-sans-mono/wght.css";
3
+ import "uno.css";
4
+ import "@admin/app.css";
5
+
6
+ import { mount } from "svelte";
7
+
8
+ import type { AdminPlugin, FieldPlugin } from "@riducms/plugin";
9
+ import type { RiduClient, RiduConfigShape } from "@riducms/sdk";
10
+ import type { TranslationLanguage } from "@riducms/translations";
11
+
12
+ import App from "@admin/app.svelte";
13
+ import type { AdminClient } from "@admin/core/api/admin-client";
14
+
15
+ export interface MountAdminOptions<Config extends RiduConfigShape> {
16
+ /** Existing DOM element that receives the Ridu admin application. */
17
+ target: Element;
18
+ /** Creates the generated, application-specific Fetch client. */
19
+ clientFactory: () => RiduClient<Config>;
20
+ /** Route prefix used when the admin is not mounted at /admin. */
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
+ }
29
+
30
+ export function mountAdmin<Config extends RiduConfigShape>(
31
+ options: MountAdminOptions<Config>
32
+ ): ReturnType<typeof mount> {
33
+ // AdminClient is the admin's capability-wide erased view. Generated clients keep exact
34
+ // collection capabilities while sharing invariant runtime methods and envelopes; the manifest
35
+ // gates which of those methods the admin exposes for each resource.
36
+ const clientFactory: () => AdminClient = () => options.clientFactory() as unknown as AdminClient;
37
+ return mount(App, {
38
+ target: options.target,
39
+ props: {
40
+ clientFactory,
41
+ ...(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
+ },
46
+ });
47
+ }
48
+
49
+ export type { AdminClient } from "@admin/core/api/admin-client";
package/src/main.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { createAdminClient } from "@admin/core/api/admin-client";
2
+ import { mountAdmin } from "@admin/index";
3
+
4
+ const target = document.getElementById("app");
5
+
6
+ if (target === null) {
7
+ throw new Error('Ridu admin could not find its required "#app" mount element.');
8
+ }
9
+
10
+ const app = mountAdmin({ target, clientFactory: createAdminClient });
11
+
12
+ export default app;
@@ -0,0 +1,9 @@
1
+ /// <reference types="svelte" />
2
+ /// <reference types="vite/client" />
3
+
4
+ declare module "~icons/*" {
5
+ import type { Component } from "svelte";
6
+ import type { SvelteHTMLElements } from "svelte/elements";
7
+ const icon: Component<SvelteHTMLElements["svg"]>;
8
+ export default icon;
9
+ }