@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,390 @@
1
+ <script lang="ts">
2
+ import type { PreviewToken, SchemaLivePreview } from "@riducms/protocol";
3
+ import {
4
+ RIDU_LIVE_PREVIEW_MESSAGE,
5
+ type RiduLivePreviewReadyMessage,
6
+ type RiduLivePreviewUpdateMessage,
7
+ } from "@riducms/sdk";
8
+ import ExternalLinkIcon from "~icons/lucide/external-link";
9
+ import XIcon from "~icons/lucide/x";
10
+ import { TooltipContent, TooltipRoot, TooltipTrigger } from "@riducms/ui";
11
+
12
+ import { Button, buttonVariants } from "@admin/components/ui/button";
13
+ import { Banner } from "@admin/components/ui/banner";
14
+ import { Input } from "@admin/components/ui/input";
15
+ import { Select, SelectContent, SelectItem, SelectTrigger } from "@admin/components/ui/select";
16
+ import type { FormValues } from "@admin/core/forms/form-schema";
17
+ import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
18
+ import {
19
+ addLivePreviewChannel,
20
+ addLivePreviewToken,
21
+ livePreviewTargetOrigin,
22
+ resolveLivePreviewURL,
23
+ } from "@admin/features/documents/live-preview";
24
+
25
+ let {
26
+ preview,
27
+ collection,
28
+ documentID,
29
+ resource,
30
+ values,
31
+ onclose,
32
+ }: {
33
+ preview: SchemaLivePreview;
34
+ collection: string;
35
+ documentID: string;
36
+ resource: "collection" | "global";
37
+ values: FormValues;
38
+ onclose: () => void;
39
+ } = $props();
40
+
41
+ const runtime = getAdminRuntime();
42
+ let iframe = $state<HTMLIFrameElement>();
43
+ let viewport = $state("responsive");
44
+ let responsiveWidth = $state(768);
45
+ let responsiveHeight = $state(720);
46
+ let zoom = $state(100);
47
+ let iframeReady = $state(false);
48
+ let popupReady = false;
49
+ let popupWindow: Window | null = null;
50
+ let popupCapabilityToken: string | undefined;
51
+ let sequence = 0;
52
+ let capability = $state<PreviewToken>();
53
+ let capabilityError = $state<string>();
54
+ let capabilityRevision = $state(0);
55
+ const channel = crypto.randomUUID();
56
+ const selectedBreakpoint = $derived(
57
+ preview.breakpoints?.find((breakpoint) => breakpoint.name === viewport)
58
+ );
59
+ const frameWidth = $derived(selectedBreakpoint?.width ?? responsiveWidth);
60
+ const frameHeight = $derived(selectedBreakpoint?.height ?? responsiveHeight);
61
+ const previewURL = $derived(
62
+ capability === undefined
63
+ ? undefined
64
+ : addLivePreviewToken(
65
+ addLivePreviewChannel(
66
+ resolveLivePreviewURL(
67
+ preview,
68
+ { collection, documentID, values },
69
+ window.location.href
70
+ ),
71
+ channel
72
+ ),
73
+ capability.token
74
+ )
75
+ );
76
+ const serializedValues = $derived(JSON.stringify(values));
77
+
78
+ function sendPreview(target: Window) {
79
+ if (previewURL === undefined) return;
80
+ const message: RiduLivePreviewUpdateMessage<FormValues> = {
81
+ type: RIDU_LIVE_PREVIEW_MESSAGE,
82
+ channel,
83
+ sequence: sequence++,
84
+ resource,
85
+ slug: collection,
86
+ collection,
87
+ id: documentID,
88
+ data: JSON.parse(serializedValues) as FormValues,
89
+ };
90
+ target.postMessage(message, livePreviewTargetOrigin(previewURL));
91
+ }
92
+
93
+ function handleLoad() {
94
+ iframeReady = false;
95
+ const target = iframe?.contentWindow;
96
+ if (target !== null && target !== undefined) sendPreview(target);
97
+ }
98
+
99
+ function openPreviewWindow(event: MouseEvent) {
100
+ event.preventDefault();
101
+ if (previewURL === undefined) return;
102
+ popupReady = false;
103
+ popupWindow = window.open(
104
+ previewURL,
105
+ `ridu-live-preview-${channel}`,
106
+ "popup=yes,width=1200,height=800,resizable=yes,scrollbars=yes"
107
+ );
108
+ popupCapabilityToken = capability?.token;
109
+ }
110
+
111
+ $effect(() => {
112
+ if (previewURL === undefined) return;
113
+ const origin = livePreviewTargetOrigin(previewURL);
114
+ const handleMessage = (event: MessageEvent) => {
115
+ if (event.origin !== origin || !isReadyMessage(event.data, channel)) return;
116
+ const iframeTarget = iframe?.contentWindow;
117
+ if (event.source === iframeTarget && iframeTarget !== null && iframeTarget !== undefined) {
118
+ iframeReady = true;
119
+ return;
120
+ }
121
+ if (event.source === popupWindow && popupWindow !== null && !popupWindow.closed) {
122
+ popupReady = true;
123
+ sendPreview(popupWindow);
124
+ }
125
+ };
126
+ window.addEventListener("message", handleMessage);
127
+ return () => window.removeEventListener("message", handleMessage);
128
+ });
129
+
130
+ function isReadyMessage(
131
+ value: unknown,
132
+ expectedChannel: string
133
+ ): value is RiduLivePreviewReadyMessage {
134
+ if (value === null || typeof value !== "object") return false;
135
+ const candidate = value as Partial<RiduLivePreviewReadyMessage>;
136
+ return (
137
+ candidate.type === RIDU_LIVE_PREVIEW_MESSAGE &&
138
+ candidate.ready === true &&
139
+ candidate.channel === expectedChannel
140
+ );
141
+ }
142
+
143
+ $effect(() => {
144
+ previewURL;
145
+ serializedValues;
146
+ const iframeTarget = iframe?.contentWindow;
147
+ if (iframeReady && iframeTarget !== null && iframeTarget !== undefined) {
148
+ sendPreview(iframeTarget);
149
+ }
150
+ if (popupReady && popupWindow !== null && !popupWindow.closed) sendPreview(popupWindow);
151
+ });
152
+
153
+ $effect(() => {
154
+ const nextURL = previewURL;
155
+ const nextToken = capability?.token;
156
+ if (
157
+ nextURL === undefined ||
158
+ nextToken === undefined ||
159
+ popupWindow === null ||
160
+ popupWindow.closed ||
161
+ popupCapabilityToken === nextToken
162
+ )
163
+ return;
164
+ popupReady = false;
165
+ popupCapabilityToken = nextToken;
166
+ popupWindow.location.replace(nextURL);
167
+ });
168
+
169
+ $effect(() => {
170
+ capabilityRevision;
171
+ const controller = new AbortController();
172
+ void authorizePreview(controller.signal);
173
+ return () => controller.abort();
174
+ });
175
+
176
+ $effect(() => {
177
+ const token = capability?.token;
178
+ if (token === undefined) return;
179
+ return () => {
180
+ void runtime.client.revokePreviewToken(token, { keepalive: true }).catch(() => undefined);
181
+ };
182
+ });
183
+
184
+ $effect(() => {
185
+ return () => {
186
+ if (popupWindow !== null && !popupWindow.closed) popupWindow.close();
187
+ popupWindow = null;
188
+ popupReady = false;
189
+ popupCapabilityToken = undefined;
190
+ };
191
+ });
192
+
193
+ $effect(() => {
194
+ if (capability === undefined) return;
195
+ const refreshIn = Math.max(1_000, Date.parse(capability.expiresAt) - Date.now() - 30_000);
196
+ const timeout = window.setTimeout(() => capabilityRevision++, refreshIn);
197
+ return () => window.clearTimeout(timeout);
198
+ });
199
+
200
+ async function authorizePreview(signal: AbortSignal) {
201
+ capabilityError = undefined;
202
+ try {
203
+ capability =
204
+ resource === "global"
205
+ ? await runtime.client.createGlobalPreviewToken(collection, { signal })
206
+ : await runtime.client.createPreviewToken(collection, documentID, { signal });
207
+ } catch (cause) {
208
+ if (signal.aborted) return;
209
+ capability = undefined;
210
+ capabilityError =
211
+ cause instanceof Error
212
+ ? cause.message
213
+ : runtime.i18n.t("documents:previewAuthorizationFailed");
214
+ }
215
+ }
216
+ </script>
217
+
218
+ <section
219
+ class="flex min-h-[540px] min-w-0 flex-1 flex-col border-t border-control-border bg-background-layer min-[1240px]:min-h-0 min-[1240px]:border-t-0 min-[1240px]:border-s"
220
+ aria-label={runtime.i18n.t("documents:livePreview")}
221
+ >
222
+ <div
223
+ class="flex min-h-12 flex-wrap items-center gap-1.5 border-b border-control-border px-3 py-2"
224
+ >
225
+ <span
226
+ class="font-mono me-1 text-[9px] tracking-[0.08em] text-foreground-faint uppercase"
227
+ role="status"
228
+ >
229
+ {capabilityError !== undefined
230
+ ? runtime.i18n.t("documents:previewUnavailable")
231
+ : capability === undefined
232
+ ? runtime.i18n.t("documents:previewAuthorizing")
233
+ : iframeReady
234
+ ? runtime.i18n.t("documents:previewConnected")
235
+ : runtime.i18n.t("documents:previewConnecting")}
236
+ </span>
237
+ <div class="flex items-center gap-1" aria-label={runtime.i18n.t("documents:previewViewport")}>
238
+ <Button
239
+ variant="ghost"
240
+ size="sm"
241
+ class={[
242
+ "h-7 px-2.5 text-[11px]",
243
+ viewport === "responsive" && "bg-control-hover text-foreground",
244
+ ]}
245
+ onclick={() => (viewport = "responsive")}
246
+ >
247
+ {runtime.i18n.t("documents:responsive")}
248
+ </Button>
249
+ {#each preview.breakpoints ?? [] as breakpoint (breakpoint.name)}
250
+ <Button
251
+ variant="ghost"
252
+ size="sm"
253
+ class={[
254
+ "h-7 px-2.5 text-[11px]",
255
+ viewport === breakpoint.name && "bg-control-hover text-foreground",
256
+ ]}
257
+ onclick={() => (viewport = breakpoint.name)}
258
+ >
259
+ {runtime.i18n.text(breakpoint.label, breakpoint.labelTranslations)}
260
+ </Button>
261
+ {/each}
262
+ </div>
263
+ <div class="ms-auto flex items-center gap-1.5">
264
+ <div class="font-mono flex items-center gap-1 text-[9.5px] text-foreground-faint">
265
+ <label>
266
+ <span class="sr-only">{runtime.i18n.t("documents:previewWidth")}</span>
267
+ {#if selectedBreakpoint !== undefined}
268
+ <Input
269
+ class="h-7 w-15 px-1.5 text-center text-[10px]"
270
+ type="number"
271
+ value={frameWidth}
272
+ disabled
273
+ />
274
+ {:else}
275
+ <Input
276
+ class="h-7 w-15 px-1.5 text-center text-[10px]"
277
+ type="number"
278
+ min="240"
279
+ max="3840"
280
+ bind:value={responsiveWidth}
281
+ />
282
+ {/if}
283
+ </label>
284
+ ×
285
+ <label>
286
+ <span class="sr-only">{runtime.i18n.t("documents:previewHeight")}</span>
287
+ {#if selectedBreakpoint !== undefined}
288
+ <Input
289
+ class="h-7 w-15 px-1.5 text-center text-[10px]"
290
+ type="number"
291
+ value={frameHeight}
292
+ disabled
293
+ />
294
+ {:else}
295
+ <Input
296
+ class="h-7 w-15 px-1.5 text-center text-[10px]"
297
+ type="number"
298
+ min="240"
299
+ max="3840"
300
+ bind:value={responsiveHeight}
301
+ />
302
+ {/if}
303
+ </label>
304
+ </div>
305
+ <label>
306
+ <span class="sr-only">{runtime.i18n.t("documents:previewZoom")}</span>
307
+ <Select type="single" value={String(zoom)} onValueChange={(next) => (zoom = Number(next))}>
308
+ <SelectTrigger
309
+ size="toolbar"
310
+ class="w-18 text-[10px]"
311
+ aria-label={runtime.i18n.t("documents:previewZoom")}
312
+ >
313
+ {runtime.i18n.formatNumber(zoom / 100, { style: "percent" })}
314
+ </SelectTrigger>
315
+ <SelectContent>
316
+ {#each [50, 75, 100] as percentage}
317
+ <SelectItem
318
+ value={String(percentage)}
319
+ label={runtime.i18n.formatNumber(percentage / 100, { style: "percent" })}
320
+ />
321
+ {/each}
322
+ </SelectContent>
323
+ </Select>
324
+ </label>
325
+ {#if previewURL !== undefined}
326
+ <TooltipRoot>
327
+ <TooltipTrigger>
328
+ {#snippet child({ props })}
329
+ <a
330
+ {...props}
331
+ class={buttonVariants({ variant: "ghost", size: "icon-sm" })}
332
+ href={previewURL}
333
+ target="_blank"
334
+ rel="opener"
335
+ aria-label={runtime.i18n.t("documents:openPreviewWindow")}
336
+ onclick={openPreviewWindow}
337
+ >
338
+ <ExternalLinkIcon class="size-3.5" />
339
+ </a>
340
+ {/snippet}
341
+ </TooltipTrigger>
342
+ <TooltipContent>{runtime.i18n.t("documents:openPreviewWindow")}</TooltipContent>
343
+ </TooltipRoot>
344
+ {/if}
345
+ <Button
346
+ variant="ghost"
347
+ size="icon-sm"
348
+ onclick={onclose}
349
+ aria-label={runtime.i18n.t("documents:closeLivePreview")}
350
+ tooltip={runtime.i18n.t("documents:closeLivePreview")}
351
+ >
352
+ <XIcon class="size-3.5" />
353
+ </Button>
354
+ </div>
355
+ </div>
356
+ <div class="min-h-0 flex-1 overflow-auto p-4" data-preview-viewport={viewport}>
357
+ {#if capabilityError !== undefined}
358
+ <Banner class="mx-auto mt-8 max-w-lg" tone="destructive">
359
+ <div class="flex flex-1 items-center justify-between gap-4">
360
+ <span>{capabilityError}</span>
361
+ <Button variant="outline" size="sm" onclick={() => capabilityRevision++}>
362
+ {runtime.i18n.t("documents:retryPreview")}
363
+ </Button>
364
+ </div>
365
+ </Banner>
366
+ {:else if previewURL === undefined}
367
+ <div class="grid min-h-72 place-items-center text-[12px] text-foreground-faint">
368
+ {runtime.i18n.t("documents:authorizingDraftPreview")}
369
+ </div>
370
+ {:else}
371
+ <div
372
+ class="mx-auto overflow-hidden rounded-[4px] border border-control-border bg-preview-canvas shadow-lg"
373
+ style:width="{frameWidth * (zoom / 100)}px"
374
+ style:height="{frameHeight * (zoom / 100)}px"
375
+ >
376
+ <iframe
377
+ bind:this={iframe}
378
+ class="block border-0 bg-preview-canvas"
379
+ src={previewURL}
380
+ title={runtime.i18n.t("documents:livePreview")}
381
+ width={frameWidth}
382
+ height={frameHeight}
383
+ style:transform="scale({zoom / 100})"
384
+ style:transform-origin="top left"
385
+ onload={handleLoad}
386
+ ></iframe>
387
+ </div>
388
+ {/if}
389
+ </div>
390
+ </section>
@@ -0,0 +1,61 @@
1
+ import type { SchemaLivePreview } from "@riducms/protocol";
2
+ import { RIDU_LIVE_PREVIEW_CHANNEL_PARAM, RIDU_PREVIEW_TOKEN_PARAM } from "@riducms/sdk";
3
+
4
+ import type { FormValues } from "@admin/core/forms/form-schema";
5
+
6
+ export interface LivePreviewContext {
7
+ collection: string;
8
+ documentID: string;
9
+ values: FormValues;
10
+ }
11
+
12
+ const placeholder = /\{(id|collection|field:([^{}]+))\}/g;
13
+
14
+ export function resolveLivePreviewURL(
15
+ preview: SchemaLivePreview,
16
+ context: LivePreviewContext,
17
+ baseURL: string
18
+ ) {
19
+ const path = preview.url.replace(placeholder, (_match, token: string, fieldPath?: string) => {
20
+ const value =
21
+ token === "id"
22
+ ? context.documentID
23
+ : token === "collection"
24
+ ? context.collection
25
+ : readValue(context.values, fieldPath ?? "");
26
+ return encodeURIComponent(stringValue(value));
27
+ });
28
+ return new URL(path, baseURL).toString();
29
+ }
30
+
31
+ export function livePreviewTargetOrigin(url: string) {
32
+ return new URL(url).origin;
33
+ }
34
+
35
+ export function addLivePreviewChannel(url: string, channel: string) {
36
+ const target = new URL(url);
37
+ target.searchParams.set(RIDU_LIVE_PREVIEW_CHANNEL_PARAM, channel);
38
+ return target.toString();
39
+ }
40
+
41
+ export function addLivePreviewToken(url: string, token: string) {
42
+ const target = new URL(url);
43
+ target.searchParams.set(RIDU_PREVIEW_TOKEN_PARAM, token);
44
+ return target.toString();
45
+ }
46
+
47
+ function readValue(values: FormValues, path: string) {
48
+ let current: unknown = values;
49
+ for (const segment of path.split(".")) {
50
+ if (current === null || typeof current !== "object" || Array.isArray(current)) return undefined;
51
+ current = (current as Record<string, unknown>)[segment];
52
+ }
53
+ return current;
54
+ }
55
+
56
+ function stringValue(value: unknown) {
57
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
58
+ return String(value);
59
+ }
60
+ return "";
61
+ }
@@ -0,0 +1,40 @@
1
+ <script lang="ts">
2
+ import { getAdminI18n } from "@riducms/plugin";
3
+ import type { AdminDocument } from "@admin/core/api/admin-client";
4
+ import type { Component } from "svelte";
5
+
6
+ type ImageEditInput = {
7
+ focalX: number;
8
+ focalY: number;
9
+ cropX: number;
10
+ cropY: number;
11
+ cropWidth: number;
12
+ cropHeight: number;
13
+ };
14
+ type UploadDocumentPreviewProps = {
15
+ document: AdminDocument;
16
+ editable?: boolean;
17
+ updating?: boolean;
18
+ onUpdate?: (input: ImageEditInput) => Promise<boolean>;
19
+ };
20
+
21
+ let props: UploadDocumentPreviewProps = $props();
22
+ const i18n = getAdminI18n();
23
+ let Preview = $state<Component<UploadDocumentPreviewProps> | undefined>(undefined);
24
+
25
+ void import("@admin/features/documents/upload-document-preview.svelte").then(
26
+ ({ default: component }) => (Preview = component)
27
+ );
28
+ </script>
29
+
30
+ {#if Preview !== undefined}
31
+ <Preview {...props} />
32
+ {:else}
33
+ <section
34
+ class="grid min-h-48 place-items-center"
35
+ aria-busy="true"
36
+ aria-label={i18n.t("uploads:loadingAssetPreview")}
37
+ >
38
+ <p class="text-[12px] text-foreground-faint">{i18n.t("uploads:loadingAssetPreview")}</p>
39
+ </section>
40
+ {/if}