@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,457 @@
1
+ <script lang="ts">
2
+ import { Link, NavLink, Outlet, useLocation } from "@hvniel/svelte-router";
3
+ import type { AdminNavigationComponent } from "@riducms/plugin";
4
+ import { tick } from "svelte";
5
+ import { MediaQuery } from "svelte/reactivity";
6
+ import ChevronDownIcon from "~icons/lucide/chevron-down";
7
+ import ChevronLeftIcon from "~icons/lucide/chevron-left";
8
+ import MenuIcon from "~icons/lucide/menu";
9
+ import SearchIcon from "~icons/lucide/search";
10
+
11
+ import { Button, buttonVariants } from "@admin/components/ui/button";
12
+ import {
13
+ Sheet,
14
+ SheetClose,
15
+ SheetContent,
16
+ SheetDescription,
17
+ SheetTitle,
18
+ SheetTrigger,
19
+ } from "@admin/components/ui/sheet";
20
+ import {
21
+ getPreferenceWriteQueue,
22
+ preferenceOwnerID,
23
+ } from "@admin/core/preferences/preference-write-queue";
24
+ import { collectionPath, globalPath } from "@admin/core/routing/admin-paths";
25
+ import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
26
+ import AccountMenu from "@admin/features/auth/account-menu.svelte";
27
+ import AdminBreadcrumbs from "@admin/features/navigation/admin-breadcrumbs.svelte";
28
+ import { groupAdminResources } from "@admin/features/navigation/admin-resource-groups";
29
+ import AdminCommandMenu from "@admin/features/navigation/admin-command-menu.svelte";
30
+ import ContentLocaleSwitcher from "@admin/features/localization/content-locale-switcher.svelte";
31
+ import { cn } from "@riducms/ui";
32
+
33
+ const navigationPreferenceKey = "navigation";
34
+ const runtime = getAdminRuntime();
35
+ const location = $derived(useLocation());
36
+ const routeSegments = $derived(location.pathname.split("/").filter(Boolean));
37
+ const routeOwnsViewport = $derived(
38
+ (routeSegments[0] === "collections" &&
39
+ routeSegments.length > 2 &&
40
+ routeSegments[2] !== "trash" &&
41
+ routeSegments[2] !== "upload" &&
42
+ routeSegments[3] !== "versions") ||
43
+ (routeSegments[0] === "globals" &&
44
+ routeSegments.length > 1 &&
45
+ routeSegments[2] !== "versions")
46
+ );
47
+ const narrowNavigation = new MediaQuery("(max-width: 1023px)");
48
+ const applicationName = $derived(
49
+ runtime.manifest === undefined
50
+ ? runtime.i18n.t("general:riduApplication")
51
+ : runtime.i18n.text(
52
+ runtime.manifest.application.name,
53
+ runtime.manifest.application.nameTranslations
54
+ )
55
+ );
56
+ const resourceGroups = $derived(
57
+ groupAdminResources(runtime.visibleCollections, runtime.visibleGlobals, {
58
+ collections: runtime.i18n.t("navigation:collections"),
59
+ globals: runtime.i18n.t("navigation:globals"),
60
+ })
61
+ );
62
+ const navigationPluginRoutes = $derived(
63
+ runtime.pluginRoutes.filter((route) => route.navigation !== undefined)
64
+ );
65
+ let commandOpen = $state(false);
66
+ let desktopNavigationOpen = $state(true);
67
+ let mobileNavigationOpen = $state(false);
68
+ let desktopToggle = $state<HTMLButtonElement | null>(null);
69
+ let preferenceGeneration = 0;
70
+ const navigationReplacement = $derived(
71
+ runtime.navigationComponents.find((component) => component.position === "replace")
72
+ );
73
+ const navigationBefore = $derived(
74
+ runtime.navigationComponents.filter((component) => component.position === "before")
75
+ );
76
+ const navigationBeforeLinks = $derived(
77
+ runtime.navigationComponents.filter((component) => component.position === "beforeLinks")
78
+ );
79
+ const navigationAfterLinks = $derived(
80
+ runtime.navigationComponents.filter((component) => component.position === "afterLinks")
81
+ );
82
+ const navigationAfter = $derived(
83
+ runtime.navigationComponents.filter((component) => component.position === "after")
84
+ );
85
+ const navigationLogo = $derived(
86
+ runtime.brandComponents.find((component) => component.surface === "navigationLogo")
87
+ );
88
+ const shellHeaders = $derived(
89
+ runtime.shellComponents.filter((component) => component.position === "header")
90
+ );
91
+ const shellActions = $derived(
92
+ runtime.shellComponents.filter((component) => component.position === "actions")
93
+ );
94
+ const hasShellHeader = $derived(runtime.manifest !== undefined && shellHeaders.length > 0);
95
+
96
+ $effect(() => {
97
+ const owner = preferenceOwnerID(runtime.session);
98
+ if (owner === "") return;
99
+ const generation = ++preferenceGeneration;
100
+ const request = new AbortController();
101
+ void runtime.client
102
+ .preference<unknown>(navigationPreferenceKey, { signal: request.signal })
103
+ .then((preference) => {
104
+ if (
105
+ request.signal.aborted ||
106
+ generation !== preferenceGeneration ||
107
+ typeof preference !== "object" ||
108
+ preference === null ||
109
+ typeof (preference as { open?: unknown }).open !== "boolean"
110
+ ) {
111
+ return;
112
+ }
113
+ desktopNavigationOpen = (preference as { open: boolean }).open;
114
+ })
115
+ .catch(() => undefined);
116
+ return () => request.abort();
117
+ });
118
+
119
+ async function toggleDesktopNavigation() {
120
+ preferenceGeneration += 1;
121
+ const next = !desktopNavigationOpen;
122
+ desktopNavigationOpen = next;
123
+ const owner = preferenceOwnerID(runtime.session);
124
+ if (owner !== "") {
125
+ void getPreferenceWriteQueue()
126
+ .enqueue(
127
+ navigationPreferenceKey,
128
+ owner,
129
+ () => preferenceOwnerID(runtime.session) === owner,
130
+ () => runtime.client.setPreference(navigationPreferenceKey, { open: next })
131
+ )
132
+ .catch(() => undefined);
133
+ }
134
+ await tick();
135
+ desktopToggle?.focus();
136
+ }
137
+
138
+ function closeMobileNavigation() {
139
+ if (narrowNavigation.current) mobileNavigationOpen = false;
140
+ }
141
+
142
+ async function openCommandMenu() {
143
+ if (narrowNavigation.current) {
144
+ mobileNavigationOpen = false;
145
+ await tick();
146
+ }
147
+ commandOpen = true;
148
+ }
149
+ </script>
150
+
151
+ {#snippet navigationContributions(components: readonly AdminNavigationComponent[])}
152
+ {#if runtime.manifest !== undefined}
153
+ {#each components as component (component.key)}
154
+ <component.component
155
+ manifest={runtime.manifest}
156
+ user={runtime.session?.user}
157
+ defaultView={defaultNavigation}
158
+ i18n={runtime.i18n}
159
+ />
160
+ {/each}
161
+ {/if}
162
+ {/snippet}
163
+
164
+ {#snippet defaultNavigation()}
165
+ {@render navigationContributions(navigationBefore)}
166
+
167
+ <nav
168
+ aria-label={runtime.i18n.t("navigation:adminNavigation")}
169
+ class="grid flex-1 content-start overflow-y-auto px-5 pb-5"
170
+ >
171
+ {@render navigationContributions(navigationBeforeLinks)}
172
+
173
+ {#each resourceGroups as group (group.key)}
174
+ <details open class="group/resource mb-3">
175
+ <summary
176
+ class="flex cursor-pointer list-none items-center justify-between gap-3 py-1 text-[12.5px] text-foreground-faint transition-colors hover:text-foreground-strong [&::-webkit-details-marker]:hidden"
177
+ >
178
+ <span>{runtime.i18n.text(group.label, group.translations)}</span>
179
+ <ChevronDownIcon
180
+ class="size-2.5 rotate-[-90deg] text-foreground-soft transition-transform group-open/resource:rotate-0"
181
+ aria-hidden="true"
182
+ />
183
+ </summary>
184
+ <div class="grid">
185
+ {#each group.collections as collection (collection.id)}
186
+ <NavLink
187
+ to={collectionPath(collection.slug)}
188
+ class={({ isActive, isPending }) =>
189
+ cn(
190
+ "relative flex min-h-7 items-center text-[13px] transition-colors after:absolute after:top-1 after:bottom-1 after:-start-5 after:w-0.5",
191
+ isActive
192
+ ? "font-medium text-foreground-strong after:bg-foreground-strong"
193
+ : "text-sidebar-foreground hover:text-foreground",
194
+ isPending && "opacity-60"
195
+ )}
196
+ onclick={closeMobileNavigation}
197
+ >
198
+ <span class="truncate">
199
+ {runtime.i18n.text(collection.labels.plural, collection.labels.pluralTranslations)}
200
+ </span>
201
+ </NavLink>
202
+ {/each}
203
+ {#each group.globals as global (global.id)}
204
+ <NavLink
205
+ to={globalPath(global.slug)}
206
+ class={({ isActive, isPending }) =>
207
+ cn(
208
+ "relative flex min-h-7 items-center text-[13px] transition-colors after:absolute after:top-1 after:bottom-1 after:-start-5 after:w-0.5",
209
+ isActive
210
+ ? "font-medium text-foreground-strong after:bg-foreground-strong"
211
+ : "text-sidebar-foreground hover:text-foreground",
212
+ isPending && "opacity-60"
213
+ )}
214
+ onclick={closeMobileNavigation}
215
+ >
216
+ <span class="truncate">
217
+ {runtime.i18n.text(global.labels.singular, global.labels.singularTranslations)}
218
+ </span>
219
+ </NavLink>
220
+ {/each}
221
+ </div>
222
+ </details>
223
+ {/each}
224
+
225
+ {#if navigationPluginRoutes.length > 0}
226
+ <details open class="group/resource mb-3">
227
+ <summary
228
+ class="flex cursor-pointer list-none items-center justify-between gap-3 py-1 text-[12.5px] text-foreground-faint transition-colors hover:text-foreground-strong [&::-webkit-details-marker]:hidden"
229
+ >
230
+ <span>{runtime.i18n.t("navigation:plugins")}</span>
231
+ <ChevronDownIcon
232
+ class="size-2.5 rotate-[-90deg] text-foreground-soft transition-transform group-open/resource:rotate-0"
233
+ aria-hidden="true"
234
+ />
235
+ </summary>
236
+ <div class="grid">
237
+ {#each navigationPluginRoutes as pluginRoute (pluginRoute.path)}
238
+ <NavLink
239
+ to={`/${pluginRoute.path}`}
240
+ class={({ isActive }) =>
241
+ cn(
242
+ "relative flex min-h-7 items-center text-[13px] transition-colors after:absolute after:top-1 after:bottom-1 after:-start-5 after:w-0.5",
243
+ isActive
244
+ ? "font-medium text-foreground-strong after:bg-foreground-strong"
245
+ : "text-sidebar-foreground hover:text-foreground"
246
+ )}
247
+ onclick={closeMobileNavigation}
248
+ >
249
+ {pluginRoute.navigation?.labelKey === undefined
250
+ ? pluginRoute.navigation?.label
251
+ : runtime.i18n.t(pluginRoute.navigation.labelKey)}
252
+ </NavLink>
253
+ {/each}
254
+ </div>
255
+ </details>
256
+ {/if}
257
+
258
+ {@render navigationContributions(navigationAfterLinks)}
259
+ </nav>
260
+
261
+ {@render navigationContributions(navigationAfter)}
262
+
263
+ {#if runtime.session !== undefined}
264
+ <div class="mt-auto px-5 py-4">
265
+ <AccountMenu compact />
266
+ </div>
267
+ {/if}
268
+ {/snippet}
269
+
270
+ {#snippet navigationSurface(mobile = false)}
271
+ <div class={["flex h-14 shrink-0 items-center gap-2 pe-4", mobile ? "ps-4" : "ps-16"]}>
272
+ {#if mobile}
273
+ <SheetClose
274
+ class={buttonVariants({ variant: "outline", size: "icon-sm" })}
275
+ aria-label={runtime.i18n.t("navigation:closeMenu")}
276
+ >
277
+ <ChevronLeftIcon class="size-4 rtl:rotate-180" aria-hidden="true" />
278
+ </SheetClose>
279
+ {/if}
280
+
281
+ <Link
282
+ class="ms-1 min-w-0 flex-1 truncate text-[13px] font-medium text-foreground"
283
+ to="/"
284
+ title={applicationName}
285
+ aria-label={applicationName}
286
+ onclick={closeMobileNavigation}
287
+ >
288
+ {#if navigationLogo !== undefined && runtime.manifest !== undefined}
289
+ <navigationLogo.component
290
+ manifest={runtime.manifest}
291
+ user={runtime.session?.user}
292
+ surface="navigationLogo"
293
+ i18n={runtime.i18n}
294
+ />
295
+ {/if}
296
+ </Link>
297
+
298
+ <Button
299
+ variant="ghost"
300
+ size="icon-sm"
301
+ onclick={openCommandMenu}
302
+ aria-label={runtime.i18n.t("navigation:searchAndNavigate")}
303
+ tooltip={runtime.i18n.t("navigation:searchAndNavigateShortcut")}
304
+ >
305
+ <SearchIcon class="size-3.5" aria-hidden="true" />
306
+ </Button>
307
+ </div>
308
+
309
+ {#if runtime.manifest !== undefined && navigationReplacement !== undefined}
310
+ <navigationReplacement.component
311
+ manifest={runtime.manifest}
312
+ user={runtime.session?.user}
313
+ defaultView={defaultNavigation}
314
+ i18n={runtime.i18n}
315
+ />
316
+ {:else}
317
+ {@render defaultNavigation()}
318
+ {/if}
319
+ {/snippet}
320
+
321
+ {#snippet adminHeaderActions()}
322
+ {#if runtime.manifest !== undefined}
323
+ {#each shellActions as component (component.key)}
324
+ <component.component
325
+ manifest={runtime.manifest}
326
+ user={runtime.session?.user}
327
+ position="actions"
328
+ i18n={runtime.i18n}
329
+ />
330
+ {/each}
331
+ {/if}
332
+ <ContentLocaleSwitcher />
333
+ {#if runtime.session !== undefined}
334
+ <div class="lg:hidden">
335
+ <AccountMenu compact />
336
+ </div>
337
+ {/if}
338
+ {/snippet}
339
+
340
+ {#if hasShellHeader && runtime.manifest !== undefined}
341
+ <header
342
+ class="flex min-h-5 items-center gap-3 bg-background-surface px-5 text-[12px] sm:px-8 lg:px-15"
343
+ aria-label={runtime.i18n.t("navigation:pluginShellHeader")}
344
+ >
345
+ {#each shellHeaders as component (component.key)}
346
+ <component.component
347
+ manifest={runtime.manifest}
348
+ user={runtime.session?.user}
349
+ position="header"
350
+ i18n={runtime.i18n}
351
+ />
352
+ {/each}
353
+ </header>
354
+ {/if}
355
+
356
+ <div
357
+ class={[
358
+ "transition-[grid-template-columns] duration-300 ease-out motion-reduce:transition-none md:grid md:overflow-hidden",
359
+ hasShellHeader
360
+ ? "min-h-[calc(100vh-1.25rem)] md:h-[calc(100vh-1.25rem)]"
361
+ : "min-h-screen md:h-screen",
362
+ desktopNavigationOpen ? "lg:grid-cols-[275px_minmax(0,1fr)]" : "lg:grid-cols-[0_minmax(0,1fr)]",
363
+ ]}
364
+ >
365
+ {#if narrowNavigation.current}
366
+ <Sheet bind:open={mobileNavigationOpen}>
367
+ <SheetTrigger
368
+ class={buttonVariants({
369
+ variant: "outline",
370
+ size: "icon-sm",
371
+ class: hasShellHeader ? "fixed top-7 start-2 z-30" : "fixed top-2.5 start-2 z-30",
372
+ })}
373
+ aria-label={runtime.i18n.t("navigation:openMenu")}
374
+ title={runtime.i18n.t("navigation:openMenu")}
375
+ >
376
+ <MenuIcon class="size-4" aria-hidden="true" />
377
+ </SheetTrigger>
378
+ <SheetContent
379
+ side={runtime.i18n.direction === "rtl" ? "right" : "left"}
380
+ showCloseButton={false}
381
+ class="w-[min(275px,calc(100vw-40px))] border-sidebar-border bg-sidebar p-0 text-sidebar-foreground sm:max-w-none"
382
+ >
383
+ <SheetTitle class="sr-only">
384
+ {runtime.i18n.t("navigation:adminNavigation")}
385
+ </SheetTitle>
386
+ <SheetDescription class="sr-only">
387
+ {runtime.i18n.t("navigation:adminNavigationDescription")}
388
+ </SheetDescription>
389
+ <aside class="flex h-full min-h-0 flex-col">
390
+ {@render navigationSurface(true)}
391
+ </aside>
392
+ </SheetContent>
393
+ </Sheet>
394
+ {:else}
395
+ <div class="relative h-full min-h-0 bg-sidebar text-sidebar-foreground">
396
+ <aside
397
+ id="ridu-admin-navigation"
398
+ inert={!desktopNavigationOpen}
399
+ aria-hidden={!desktopNavigationOpen}
400
+ class={[
401
+ "absolute inset-y-0 start-0 flex w-[275px] min-h-0 flex-col border-e border-sidebar-border bg-sidebar transition-[transform,opacity] duration-300 ease-out motion-reduce:transition-none",
402
+ desktopNavigationOpen
403
+ ? "translate-x-0 opacity-100"
404
+ : runtime.i18n.direction === "rtl"
405
+ ? "pointer-events-none translate-x-full opacity-0"
406
+ : "pointer-events-none -translate-x-full opacity-0",
407
+ ]}
408
+ >
409
+ {@render navigationSurface()}
410
+ </aside>
411
+ </div>
412
+
413
+ <Button
414
+ bind:ref={desktopToggle}
415
+ variant="outline"
416
+ size="icon-sm"
417
+ class={cn(
418
+ "fixed z-40",
419
+ hasShellHeader ? "top-[34px]" : "top-3.5",
420
+ desktopNavigationOpen ? "start-4" : "start-2"
421
+ )}
422
+ onclick={toggleDesktopNavigation}
423
+ aria-label={runtime.i18n.t(
424
+ desktopNavigationOpen ? "navigation:closeMenu" : "navigation:openMenu"
425
+ )}
426
+ aria-controls="ridu-admin-navigation"
427
+ aria-expanded={desktopNavigationOpen}
428
+ title={runtime.i18n.t(desktopNavigationOpen ? "navigation:closeMenu" : "navigation:openMenu")}
429
+ >
430
+ {#if desktopNavigationOpen}
431
+ <ChevronLeftIcon class="size-4 rtl:rotate-180" aria-hidden="true" />
432
+ {:else}
433
+ <MenuIcon class="size-4" aria-hidden="true" />
434
+ {/if}
435
+ </Button>
436
+ {/if}
437
+
438
+ <main
439
+ class={[
440
+ "relative min-w-0 bg-background max-lg:ps-11 md:flex md:h-full md:flex-col",
441
+ routeOwnsViewport ? "md:overflow-hidden" : "md:overflow-y-auto",
442
+ ]}
443
+ >
444
+ <header
445
+ class="flex h-14 shrink-0 items-center justify-between gap-4 bg-background px-5 sm:px-8 lg:px-15"
446
+ aria-label={runtime.i18n.t("navigation:adminHeader")}
447
+ >
448
+ <AdminBreadcrumbs />
449
+ <div class="flex shrink-0 items-center gap-2">
450
+ {@render adminHeaderActions()}
451
+ </div>
452
+ </header>
453
+ <Outlet />
454
+ </main>
455
+ </div>
456
+
457
+ <AdminCommandMenu bind:open={commandOpen} collections={runtime.visibleCollections} />
@@ -0,0 +1,204 @@
1
+ <script lang="ts">
2
+ import { useLocation } from "@hvniel/svelte-router";
3
+ import { onMount, tick } from "svelte";
4
+
5
+ import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
6
+
7
+ const revealDelayMilliseconds = 160;
8
+ const minimumVisibleMilliseconds = 180;
9
+ const completionMilliseconds = 150;
10
+ const fallbackIntentMilliseconds = 140;
11
+ const runtime = getAdminRuntime();
12
+ const routeLocation = $derived(useLocation());
13
+ const routeKey = $derived(
14
+ `${routeLocation.pathname}${routeLocation.search}${routeLocation.hash}`
15
+ );
16
+
17
+ type ProgressPhase = "idle" | "pending" | "running" | "finishing";
18
+
19
+ let phase = $state<ProgressPhase>("idle");
20
+ let progress = $state(0);
21
+ let observedRouteKey = "";
22
+ let startedAt = 0;
23
+ let generation = 0;
24
+ let revealTimer: number | undefined;
25
+ let trickleTimer: number | undefined;
26
+ let finishTimer: number | undefined;
27
+ let hideTimer: number | undefined;
28
+ let resetTimer: number | undefined;
29
+ let intentTimer: number | undefined;
30
+
31
+ function clearTimer(timer: number | undefined) {
32
+ if (timer !== undefined) window.clearTimeout(timer);
33
+ }
34
+
35
+ function clearCompletionTimers() {
36
+ clearTimer(finishTimer);
37
+ clearTimer(hideTimer);
38
+ clearTimer(resetTimer);
39
+ finishTimer = undefined;
40
+ hideTimer = undefined;
41
+ resetTimer = undefined;
42
+ }
43
+
44
+ function scheduleTrickle() {
45
+ clearTimer(trickleTimer);
46
+ trickleTimer = window.setTimeout(() => {
47
+ if (phase !== "running") return;
48
+ progress += (0.92 - progress) * 0.16;
49
+ scheduleTrickle();
50
+ }, 150);
51
+ }
52
+
53
+ function start() {
54
+ clearCompletionTimers();
55
+ clearTimer(intentTimer);
56
+ intentTimer = undefined;
57
+
58
+ if (phase === "pending") return;
59
+ if (phase === "running") {
60
+ generation += 1;
61
+ return;
62
+ }
63
+
64
+ generation += 1;
65
+ const activeGeneration = generation;
66
+ phase = "pending";
67
+ progress = 0;
68
+ clearTimer(revealTimer);
69
+ revealTimer = window.setTimeout(() => {
70
+ if (activeGeneration !== generation || phase !== "pending") return;
71
+ revealTimer = undefined;
72
+ startedAt = performance.now();
73
+ progress = 0.08;
74
+ phase = "running";
75
+ scheduleTrickle();
76
+ }, revealDelayMilliseconds);
77
+ }
78
+
79
+ function afterDestinationPaint() {
80
+ return new Promise<void>((resolve) => {
81
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
82
+ });
83
+ }
84
+
85
+ async function finish() {
86
+ const activeGeneration = generation;
87
+ await tick();
88
+ await afterDestinationPaint();
89
+ if (activeGeneration !== generation || phase === "idle" || phase === "finishing") return;
90
+ if (phase === "pending") {
91
+ clearTimer(revealTimer);
92
+ revealTimer = undefined;
93
+ phase = "idle";
94
+ progress = 0;
95
+ return;
96
+ }
97
+
98
+ const remaining = Math.max(0, minimumVisibleMilliseconds - (performance.now() - startedAt));
99
+ finishTimer = window.setTimeout(() => {
100
+ if (activeGeneration !== generation || phase !== "running") return;
101
+ clearTimer(trickleTimer);
102
+ trickleTimer = undefined;
103
+ progress = 1;
104
+ phase = "finishing";
105
+ hideTimer = window.setTimeout(() => {
106
+ if (activeGeneration !== generation) return;
107
+ phase = "idle";
108
+ resetTimer = window.setTimeout(() => {
109
+ if (activeGeneration === generation) progress = 0;
110
+ }, completionMilliseconds);
111
+ }, completionMilliseconds);
112
+ }, remaining);
113
+ }
114
+
115
+ function startIntent() {
116
+ const routeBeforeIntent = routeKey;
117
+ start();
118
+ intentTimer = window.setTimeout(() => {
119
+ intentTimer = undefined;
120
+ if (routeKey === routeBeforeIntent) void finish();
121
+ }, fallbackIntentMilliseconds);
122
+ }
123
+
124
+ function handleLinkIntent(event: MouseEvent) {
125
+ if (
126
+ event.defaultPrevented ||
127
+ event.button !== 0 ||
128
+ event.metaKey ||
129
+ event.ctrlKey ||
130
+ event.shiftKey ||
131
+ event.altKey ||
132
+ !(event.target instanceof Element)
133
+ ) {
134
+ return;
135
+ }
136
+
137
+ const anchor = event.target.closest<HTMLAnchorElement>("a[href]");
138
+ if (
139
+ anchor === null ||
140
+ anchor.hasAttribute("download") ||
141
+ (anchor.target !== "" && anchor.target !== "_self")
142
+ ) {
143
+ return;
144
+ }
145
+
146
+ const destination = new URL(anchor.href, window.location.href);
147
+ if (
148
+ destination.origin !== window.location.origin ||
149
+ `${destination.pathname}${destination.search}${destination.hash}` ===
150
+ `${window.location.pathname}${window.location.search}${window.location.hash}`
151
+ ) {
152
+ return;
153
+ }
154
+
155
+ startIntent();
156
+ }
157
+
158
+ $effect(() => {
159
+ const nextRouteKey = routeKey;
160
+ if (observedRouteKey === "") {
161
+ observedRouteKey = nextRouteKey;
162
+ return;
163
+ }
164
+ if (nextRouteKey === observedRouteKey) return;
165
+
166
+ observedRouteKey = nextRouteKey;
167
+ start();
168
+ void finish();
169
+ });
170
+
171
+ onMount(() => {
172
+ const handleHistoryIntent = () => startIntent();
173
+ document.addEventListener("click", handleLinkIntent, true);
174
+ window.addEventListener("popstate", handleHistoryIntent);
175
+
176
+ return () => {
177
+ generation += 1;
178
+ document.removeEventListener("click", handleLinkIntent, true);
179
+ window.removeEventListener("popstate", handleHistoryIntent);
180
+ clearTimer(revealTimer);
181
+ clearTimer(trickleTimer);
182
+ clearTimer(intentTimer);
183
+ clearCompletionTimers();
184
+ };
185
+ });
186
+ </script>
187
+
188
+ <div
189
+ class="pointer-events-none fixed inset-x-0 top-0 z-[100] h-0.5 overflow-hidden"
190
+ aria-hidden={phase === "idle" || phase === "pending"}
191
+ >
192
+ <div
193
+ class={[
194
+ "h-full origin-left bg-foreground-strong shadow-[0_0_10px_color-mix(in_srgb,currentColor_55%,transparent)] transition-[transform,opacity] duration-180 ease-out motion-reduce:transition-none rtl:origin-right",
195
+ phase === "idle" || phase === "pending" ? "opacity-0" : "opacity-100",
196
+ ]}
197
+ style:transform={`scaleX(${progress})`}
198
+ role="progressbar"
199
+ aria-label={runtime.i18n.t("navigation:loadingPage")}
200
+ aria-valuemin="0"
201
+ aria-valuemax="100"
202
+ aria-valuenow={Math.round(progress * 100)}
203
+ ></div>
204
+ </div>