@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.
- package/LICENSE +21 -0
- package/README.md +31 -0
- package/THIRD_PARTY_NOTICES.md +217 -0
- package/package.json +62 -0
- package/src/app/admin-core-view-route.svelte +89 -0
- package/src/app/admin-layout.svelte +457 -0
- package/src/app/admin-navigation-progress.svelte +204 -0
- package/src/app/admin-page-title.svelte +94 -0
- package/src/app/admin-provider-layer.svelte +26 -0
- package/src/app/admin-router-root.svelte +9 -0
- package/src/app/admin-routes.svelte +116 -0
- package/src/app/deferred-redirect.svelte +23 -0
- package/src/app/loading-boundary.svelte +70 -0
- package/src/app/not-found-route.svelte +29 -0
- package/src/app-meta.ts +2 -0
- package/src/app.css +467 -0
- package/src/app.svelte +88 -0
- package/src/assets/ridu-logo-arch.svg +3 -0
- package/src/assets/ridu-word-logo.svg +7 -0
- package/src/components/brand/ridu-logo.svelte +24 -0
- package/src/components/code-block/code-block.svelte +107 -0
- package/src/components/json-tree/json-tree-node.svelte +77 -0
- package/src/components/json-tree/json-viewer.svelte +113 -0
- package/src/components/ui/banner/banner.svelte +41 -0
- package/src/components/ui/banner/index.ts +5 -0
- package/src/components/ui/bulk-action-bar/bulk-action-bar.svelte +50 -0
- package/src/components/ui/bulk-action-bar/index.ts +1 -0
- package/src/components/ui/button/index.ts +8 -0
- package/src/components/ui/calendar/calendar-caption.svelte +72 -0
- package/src/components/ui/calendar/calendar-cell.svelte +20 -0
- package/src/components/ui/calendar/calendar-day.svelte +27 -0
- package/src/components/ui/calendar/calendar-grid-row.svelte +13 -0
- package/src/components/ui/calendar/calendar-grid.svelte +17 -0
- package/src/components/ui/calendar/calendar-head-cell.svelte +20 -0
- package/src/components/ui/calendar/calendar-header.svelte +20 -0
- package/src/components/ui/calendar/calendar-heading.svelte +17 -0
- package/src/components/ui/calendar/calendar-month-select.svelte +45 -0
- package/src/components/ui/calendar/calendar-month.svelte +16 -0
- package/src/components/ui/calendar/calendar-months.svelte +20 -0
- package/src/components/ui/calendar/calendar-nav.svelte +20 -0
- package/src/components/ui/calendar/calendar-next-button.svelte +30 -0
- package/src/components/ui/calendar/calendar-prev-button.svelte +30 -0
- package/src/components/ui/calendar/calendar-year-select.svelte +44 -0
- package/src/components/ui/calendar/calendar.svelte +118 -0
- package/src/components/ui/calendar/index.ts +1 -0
- package/src/components/ui/checkbox/checkbox.svelte +35 -0
- package/src/components/ui/checkbox/index.ts +1 -0
- package/src/components/ui/confirmation-dialog/confirmation-dialog.svelte +37 -0
- package/src/components/ui/confirmation-dialog/index.ts +1 -0
- package/src/components/ui/date-value-control/date-value-calendar.svelte +37 -0
- package/src/components/ui/date-value-control/date-value-control-loader.svelte +36 -0
- package/src/components/ui/date-value-control/date-value-control.svelte +177 -0
- package/src/components/ui/date-value-control/index.ts +3 -0
- package/src/components/ui/dialog/dialog-close.svelte +11 -0
- package/src/components/ui/dialog/dialog-content.svelte +52 -0
- package/src/components/ui/dialog/dialog-description.svelte +20 -0
- package/src/components/ui/dialog/dialog-footer.svelte +20 -0
- package/src/components/ui/dialog/dialog-header.svelte +20 -0
- package/src/components/ui/dialog/dialog-overlay.svelte +20 -0
- package/src/components/ui/dialog/dialog-title.svelte +17 -0
- package/src/components/ui/dialog/dialog-trigger.svelte +11 -0
- package/src/components/ui/dialog/dialog.svelte +7 -0
- package/src/components/ui/dialog/index.ts +8 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +27 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +20 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +16 -0
- package/src/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
- package/src/components/ui/dropdown-menu/index.ts +5 -0
- package/src/components/ui/empty-state/empty-state.svelte +34 -0
- package/src/components/ui/empty-state/index.ts +1 -0
- package/src/components/ui/input/index.ts +1 -0
- package/src/components/ui/kbd/index.ts +1 -0
- package/src/components/ui/kbd/kbd.svelte +24 -0
- package/src/components/ui/pagination/index.ts +1 -0
- package/src/components/ui/pagination/pagination.svelte +88 -0
- package/src/components/ui/popover/index.ts +3 -0
- package/src/components/ui/popover/popover-content.svelte +29 -0
- package/src/components/ui/popover/popover-trigger.svelte +11 -0
- package/src/components/ui/popover/popover.svelte +7 -0
- package/src/components/ui/radio-group/index.ts +3 -0
- package/src/components/ui/radio-group/radio-card-item.svelte +38 -0
- package/src/components/ui/radio-group/radio-group-item.svelte +17 -0
- package/src/components/ui/radio-group/radio-group.svelte +19 -0
- package/src/components/ui/select/index.ts +4 -0
- package/src/components/ui/select/select-content.svelte +38 -0
- package/src/components/ui/select/select-item.svelte +40 -0
- package/src/components/ui/select/select-scroll-down-button.svelte +23 -0
- package/src/components/ui/select/select-scroll-up-button.svelte +23 -0
- package/src/components/ui/select/select-trigger.svelte +29 -0
- package/src/components/ui/select/select.svelte +11 -0
- package/src/components/ui/sheet/index.ts +6 -0
- package/src/components/ui/sheet/sheet-close.svelte +7 -0
- package/src/components/ui/sheet/sheet-content.svelte +77 -0
- package/src/components/ui/sheet/sheet-description.svelte +17 -0
- package/src/components/ui/sheet/sheet-overlay.svelte +36 -0
- package/src/components/ui/sheet/sheet-title.svelte +17 -0
- package/src/components/ui/sheet/sheet-trigger.svelte +7 -0
- package/src/components/ui/sheet/sheet.svelte +7 -0
- package/src/components/ui/skeleton/index.ts +1 -0
- package/src/components/ui/skeleton/skeleton.svelte +31 -0
- package/src/components/ui/slider/index.ts +3 -0
- package/src/components/ui/slider/slider-content.svelte +59 -0
- package/src/components/ui/slider/slider.svelte +35 -0
- package/src/components/ui/sonner/index.ts +1 -0
- package/src/components/ui/sonner/sonner.svelte +55 -0
- package/src/components/ui/status-indicator/index.ts +5 -0
- package/src/components/ui/status-indicator/status-indicator.svelte +54 -0
- package/src/components/ui/table/index.ts +6 -0
- package/src/components/ui/table/table-body.svelte +20 -0
- package/src/components/ui/table/table-cell.svelte +23 -0
- package/src/components/ui/table/table-head.svelte +23 -0
- package/src/components/ui/table/table-header.svelte +20 -0
- package/src/components/ui/table/table-row.svelte +23 -0
- package/src/components/ui/table/table.svelte +22 -0
- package/src/components/ui/tabs/index.ts +8 -0
- package/src/components/ui/tabs/tabs-content.svelte +17 -0
- package/src/components/ui/tabs/tabs-list.svelte +40 -0
- package/src/components/ui/tabs/tabs-trigger.svelte +23 -0
- package/src/components/ui/tabs/tabs.svelte +19 -0
- package/src/components/ui/textarea/index.ts +1 -0
- package/src/components/ui/tooltip/index.ts +4 -0
- package/src/components/ui/tooltip/tooltip-content.svelte +50 -0
- package/src/components/ui/tooltip/tooltip-provider.svelte +7 -0
- package/src/components/ui/tooltip/tooltip-trigger.svelte +7 -0
- package/src/components/ui/tooltip/tooltip.svelte +7 -0
- package/src/core/api/admin-client.ts +45 -0
- package/src/core/forms/field-condition.ts +63 -0
- package/src/core/forms/form-controller.svelte.ts +369 -0
- package/src/core/forms/form-draft-recovery.ts +80 -0
- package/src/core/forms/form-schema.ts +609 -0
- package/src/core/forms/form-validation.ts +750 -0
- package/src/core/i18n/admin-i18n.svelte.ts +364 -0
- package/src/core/i18n/localized-schema.ts +111 -0
- package/src/core/notifications/notification-center.svelte.ts +69 -0
- package/src/core/notifications/validation-toast.svelte +18 -0
- package/src/core/plugins/field-registry.ts +60 -0
- package/src/core/plugins/row-label-registry.ts +62 -0
- package/src/core/preferences/preference-write-queue.ts +107 -0
- package/src/core/routing/admin-paths.ts +137 -0
- package/src/core/runtime/admin-runtime.svelte.ts +625 -0
- package/src/features/auth/account-menu.svelte +159 -0
- package/src/features/auth/account-navigation.svelte +44 -0
- package/src/features/auth/account-route.svelte +395 -0
- package/src/features/auth/account-security-controller.svelte.ts +119 -0
- package/src/features/auth/account-security-route.svelte +460 -0
- package/src/features/auth/auth-brand.svelte +17 -0
- package/src/features/auth/auth-frame.svelte +18 -0
- package/src/features/auth/create-first-user-controller.svelte.ts +220 -0
- package/src/features/auth/create-first-user-route.svelte +174 -0
- package/src/features/auth/forgot-password-route.svelte +78 -0
- package/src/features/auth/login-route.svelte +221 -0
- package/src/features/auth/request-verification-route.svelte +73 -0
- package/src/features/auth/reset-password-route.svelte +107 -0
- package/src/features/auth/verify-email-route.svelte +60 -0
- package/src/features/collections/collection-list-bulk-actions.svelte +143 -0
- package/src/features/collections/collection-list-bulk-editor.svelte +157 -0
- package/src/features/collections/collection-list-controller.svelte.ts +717 -0
- package/src/features/collections/collection-list-filter-popover.svelte +238 -0
- package/src/features/collections/collection-list-preferences-controller.svelte.ts +393 -0
- package/src/features/collections/collection-list-results.svelte +410 -0
- package/src/features/collections/collection-list-route.svelte +951 -0
- package/src/features/collections/collection-list-workspace-toolbar.svelte +409 -0
- package/src/features/collections/collection-table-skeleton.svelte +44 -0
- package/src/features/collections/list-workspace.ts +265 -0
- package/src/features/dashboard/dashboard-route.svelte +141 -0
- package/src/features/development/development-tools.svelte +217 -0
- package/src/features/documents/document-api-view-loader.svelte +31 -0
- package/src/features/documents/document-api-view.svelte +547 -0
- package/src/features/documents/document-controller.svelte.ts +1252 -0
- package/src/features/documents/document-field-partition.ts +18 -0
- package/src/features/documents/document-field-sections.svelte +46 -0
- package/src/features/documents/document-heading.ts +20 -0
- package/src/features/documents/document-route-dirty.ts +43 -0
- package/src/features/documents/document-route-loader.svelte +22 -0
- package/src/features/documents/document-route.svelte +1124 -0
- package/src/features/documents/live-preview-panel.svelte +390 -0
- package/src/features/documents/live-preview.ts +61 -0
- package/src/features/documents/upload-document-preview-loader.svelte +40 -0
- package/src/features/documents/upload-document-preview.svelte +525 -0
- package/src/features/localization/content-locale-switcher.svelte +93 -0
- package/src/features/navigation/admin-breadcrumbs.svelte +124 -0
- package/src/features/navigation/admin-command-menu.svelte +245 -0
- package/src/features/navigation/admin-resource-groups.ts +72 -0
- package/src/features/reference-browser/reference-browser-loader.svelte +15 -0
- package/src/features/reference-browser/reference-browser-workflow.svelte.ts +691 -0
- package/src/features/reference-browser/reference-browser.svelte +844 -0
- package/src/features/uploads/bulk-upload-route.svelte +624 -0
- package/src/features/uploads/bulk-upload.ts +39 -0
- package/src/features/uploads/upload-metadata-fields.svelte +112 -0
- package/src/features/versions/version-diff.ts +72 -0
- package/src/features/versions/version-history-route.svelte +581 -0
- package/src/fields/field-clipboard.ts +132 -0
- package/src/fields/field-layout.svelte +60 -0
- package/src/fields/field-layout.ts +57 -0
- package/src/fields/field-messages.svelte +29 -0
- package/src/fields/field-renderer.svelte +152 -0
- package/src/fields/field-shell.svelte +30 -0
- package/src/fields/field-tab-layout.svelte +99 -0
- package/src/fields/join/join-field.svelte +358 -0
- package/src/fields/localized-field-presentation.ts +17 -0
- package/src/fields/nested/nested-field.svelte +602 -0
- package/src/fields/nested/scoped-field.ts +78 -0
- package/src/fields/nested/sortable-row.svelte +32 -0
- package/src/fields/point/point-field.svelte +52 -0
- package/src/fields/radio/radio-field.svelte +34 -0
- package/src/fields/relationship/relationship-field-controller.svelte.ts +245 -0
- package/src/fields/relationship/relationship-field.svelte +258 -0
- package/src/fields/relationship/relationship-lookup-controller.svelte.ts +129 -0
- package/src/fields/relationship/relationship-query.ts +88 -0
- package/src/fields/relationship/relationship-quick-picker-controller.svelte.ts +198 -0
- package/src/fields/relationship/relationship-quick-picker.svelte +327 -0
- package/src/fields/relationship/relationship-value.ts +47 -0
- package/src/fields/scalar/date-control-value.ts +60 -0
- package/src/fields/scalar/date-value.ts +26 -0
- package/src/fields/scalar/scalar-field.svelte +137 -0
- package/src/fields/select/select-field.svelte +132 -0
- package/src/fields/select/select-value.ts +14 -0
- package/src/fields/text/slug-field.svelte +87 -0
- package/src/fields/text/slug.ts +38 -0
- package/src/fields/text/text-field.svelte +41 -0
- package/src/fields/ui/ui-field.svelte +12 -0
- package/src/fields/virtual/virtual-field.svelte +28 -0
- package/src/index.ts +49 -0
- package/src/main.ts +12 -0
- 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
|
+
}
|