@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,547 @@
1
+ <script lang="ts">
2
+ import { getAdminI18n } from "@riducms/plugin";
3
+ import { onDestroy } from "svelte";
4
+ import CheckIcon from "~icons/lucide/check";
5
+ import CopyIcon from "~icons/lucide/copy";
6
+ import ExternalLinkIcon from "~icons/lucide/external-link";
7
+ import RefreshCwIcon from "~icons/lucide/refresh-cw";
8
+
9
+ import CodeBlock from "@admin/components/code-block/code-block.svelte";
10
+ import JsonViewer from "@admin/components/json-tree/json-viewer.svelte";
11
+ import { Banner } from "@admin/components/ui/banner";
12
+ import { Button } from "@admin/components/ui/button";
13
+ import { Checkbox } from "@admin/components/ui/checkbox";
14
+ import { Input } from "@admin/components/ui/input";
15
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@admin/components/ui/tabs";
16
+
17
+ type APIAction = "list" | "find" | "create" | "update" | "duplicate" | "delete";
18
+ const collectionActions: readonly {
19
+ key: APIAction;
20
+ method: "GET" | "POST" | "PATCH" | "DELETE";
21
+ }[] = [
22
+ { key: "list", method: "GET" },
23
+ { key: "find", method: "GET" },
24
+ { key: "create", method: "POST" },
25
+ { key: "update", method: "PATCH" },
26
+ { key: "duplicate", method: "POST" },
27
+ { key: "delete", method: "DELETE" },
28
+ ];
29
+ const i18n = getAdminI18n();
30
+
31
+ let {
32
+ resourceSlug,
33
+ resourceLabel,
34
+ documentID,
35
+ globalResource = false,
36
+ fallbackValue,
37
+ }: {
38
+ resourceSlug: string;
39
+ resourceLabel: string;
40
+ documentID?: string;
41
+ globalResource?: boolean;
42
+ fallbackValue: unknown;
43
+ } = $props();
44
+
45
+ let depth = $state(2);
46
+ let authenticated = $state(true);
47
+ let requestRevision = $state(0);
48
+ let loading = $state(false);
49
+ // Begin with a non-reactive form snapshot until a live request becomes available.
50
+ // svelte-ignore state_referenced_locally
51
+ let responseValue = $state.raw<unknown>($state.snapshot(fallbackValue));
52
+ let responseStatus = $state<number>();
53
+ let responseStatusText = $state("");
54
+ let responseDuration = $state<number>();
55
+ let responseSize = $state<number>();
56
+ let requestError = $state<string>();
57
+ let example = $state<"fetch" | "sdk" | "curl">("fetch");
58
+ let apiAction = $state<APIAction>("find");
59
+ let copied = $state<"url" | "example">();
60
+ let copyResetTimer: ReturnType<typeof setTimeout> | undefined;
61
+
62
+ onDestroy(() => clearTimeout(copyResetTimer));
63
+
64
+ const availableActions = $derived(
65
+ globalResource
66
+ ? collectionActions.filter((action) => action.key === "find" || action.key === "update")
67
+ : collectionActions
68
+ );
69
+ const selectedAction = $derived(
70
+ availableActions.find((action) => action.key === apiAction) ?? availableActions[0]!
71
+ );
72
+ const runnableAction = $derived(apiAction === "list" || apiAction === "find");
73
+ const liveRequest = $derived(
74
+ runnableAction && (apiAction === "list" || globalResource || documentID !== undefined)
75
+ );
76
+ const collectionEndpoint = $derived(`/api/collections/${encodeURIComponent(resourceSlug)}`);
77
+ const documentEndpoint = $derived(
78
+ `${collectionEndpoint}/${documentID === undefined ? ":id" : encodeURIComponent(documentID)}`
79
+ );
80
+ const endpointPath = $derived.by(() => {
81
+ if (globalResource) return `/api/globals/${encodeURIComponent(resourceSlug)}`;
82
+ if (apiAction === "list" || apiAction === "create") return collectionEndpoint;
83
+ if (apiAction === "duplicate") return `${documentEndpoint}/duplicate`;
84
+ return documentEndpoint;
85
+ });
86
+ const requestURL = $derived.by(() => {
87
+ if (endpointPath.includes(":id")) return "";
88
+ const url = new URL(endpointPath, window.location.origin);
89
+ if (runnableAction) url.searchParams.set("depth", String(depth));
90
+ return url.href;
91
+ });
92
+ const requestExample = $derived(requestExampleFor(example));
93
+
94
+ function requestExampleFor(exampleKind: typeof example) {
95
+ if (apiAction !== "find") return actionExample(exampleKind);
96
+ if (exampleKind === "sdk") {
97
+ if (globalResource)
98
+ return `const settings = await client.global(${JSON.stringify(resourceSlug)}, { depth: ${depth} });`;
99
+ return `const document = await client.find(\n ${JSON.stringify(resourceSlug)},\n ${JSON.stringify(documentID ?? "document-id")},\n { depth: ${depth} },\n);`;
100
+ }
101
+ if (exampleKind === "curl") {
102
+ return `curl ${authenticated ? "-H 'Authorization: Bearer <api-key>' \\\n " : ""}${JSON.stringify(requestURL || endpointPath)}`;
103
+ }
104
+ return `const response = await fetch(${JSON.stringify(requestURL || endpointPath)}, {\n headers: { Accept: "application/json" },\n credentials: ${JSON.stringify(authenticated ? "include" : "omit")},\n});\n\nif (!response.ok) throw new Error(\`Request failed: \${response.status}\`);\nconst { doc } = await response.json();`;
105
+ }
106
+
107
+ $effect(() => {
108
+ requestRevision;
109
+ const url = requestURL;
110
+ const includeSession = authenticated;
111
+ if (!liveRequest || url === "") {
112
+ responseValue = $state.snapshot(fallbackValue);
113
+ responseStatus = undefined;
114
+ responseStatusText = "";
115
+ responseDuration = undefined;
116
+ responseSize = undefined;
117
+ requestError = undefined;
118
+ return;
119
+ }
120
+ const abort = new AbortController();
121
+ execute(url, includeSession, abort.signal);
122
+ return () => abort.abort();
123
+ });
124
+
125
+ async function execute(url: string, includeSession: boolean, signal: AbortSignal) {
126
+ loading = true;
127
+ requestError = undefined;
128
+ const startedAt = performance.now();
129
+ try {
130
+ const response = await fetch(url, {
131
+ headers: { Accept: "application/json" },
132
+ credentials: includeSession ? "include" : "omit",
133
+ signal,
134
+ });
135
+ const encoded = await response.text();
136
+ if (signal.aborted) return;
137
+ responseStatus = response.status;
138
+ responseStatusText = response.statusText;
139
+ responseDuration = Math.round(performance.now() - startedAt);
140
+ responseSize = new TextEncoder().encode(encoded).byteLength;
141
+ let body: unknown = encoded;
142
+ try {
143
+ body = encoded === "" ? null : JSON.parse(encoded);
144
+ } catch {
145
+ // Non-JSON failures remain inspectable in the response viewer.
146
+ }
147
+ responseValue = documentFromEnvelope(body);
148
+ if (!response.ok) requestError = errorMessage(body, response);
149
+ } catch (cause) {
150
+ if (signal.aborted) return;
151
+ responseDuration = Math.round(performance.now() - startedAt);
152
+ responseStatus = undefined;
153
+ responseStatusText = "";
154
+ responseSize = undefined;
155
+ requestError = cause instanceof Error ? cause.message : i18n.t("documents:apiRequestFailed");
156
+ responseValue = { error: requestError };
157
+ } finally {
158
+ if (!signal.aborted) loading = false;
159
+ }
160
+ }
161
+
162
+ function documentFromEnvelope(value: unknown) {
163
+ return isRecord(value) && "doc" in value ? value.doc : value;
164
+ }
165
+
166
+ function errorMessage(value: unknown, response: Response) {
167
+ if (isRecord(value) && typeof value.message === "string") return value.message;
168
+ if (isRecord(value) && isRecord(value.error) && typeof value.error.message === "string") {
169
+ return value.error.message;
170
+ }
171
+ return `${response.status} ${response.statusText}`.trim();
172
+ }
173
+
174
+ function isRecord(value: unknown): value is Record<string, unknown> {
175
+ return value !== null && typeof value === "object" && !Array.isArray(value);
176
+ }
177
+
178
+ function actionExample(language: "fetch" | "sdk" | "curl") {
179
+ if (language === "sdk") return sdkActionExample();
180
+ if (language === "curl") return curlActionExample();
181
+ return fetchActionExample();
182
+ }
183
+
184
+ function sdkActionExample() {
185
+ if (globalResource) {
186
+ return [
187
+ `const settings = await client.updateGlobal(${JSON.stringify(resourceSlug)}, {`,
188
+ ' siteName: "Updated value",',
189
+ "});",
190
+ ].join("\n");
191
+ }
192
+ const slug = JSON.stringify(resourceSlug);
193
+ const id = JSON.stringify(documentID ?? "document-id");
194
+ switch (apiAction) {
195
+ case "list":
196
+ return [
197
+ `const page = await client.list(${slug}, {`,
198
+ " page: 1,",
199
+ " limit: 10,",
200
+ ` depth: ${depth},`,
201
+ "});",
202
+ ].join("\n");
203
+ case "create":
204
+ return `const document = await client.create(${slug}, {\n title: "New ${resourceLabel}",\n});`;
205
+ case "update":
206
+ return `const document = await client.update(${slug}, ${id}, {\n title: "Updated ${resourceLabel}",\n});`;
207
+ case "duplicate":
208
+ return `const copy = await client.duplicate(${slug}, ${id}, {\n title: "Copy of ${resourceLabel}",\n});`;
209
+ case "delete":
210
+ return `const result = await client.delete(${slug}, ${id});`;
211
+ default:
212
+ return "";
213
+ }
214
+ }
215
+
216
+ function fetchActionExample() {
217
+ const hasBody = apiAction === "create" || apiAction === "update" || apiAction === "duplicate";
218
+ const resultName = apiAction === "list" ? "page" : apiAction === "delete" ? "result" : "doc";
219
+ const lines = [
220
+ `const response = await fetch(${JSON.stringify(requestURL || endpointPath)}, {`,
221
+ ` method: ${JSON.stringify(selectedAction.method)},`,
222
+ " headers: {",
223
+ ' Accept: "application/json",',
224
+ ];
225
+ if (hasBody) lines.push(' "Content-Type": "application/json",');
226
+ lines.push(" },", ` credentials: ${JSON.stringify(authenticated ? "include" : "omit")},`);
227
+ if (hasBody) lines.push(' body: JSON.stringify({ title: "Example value" }),');
228
+ lines.push(
229
+ "});",
230
+ "",
231
+ "if (!response.ok) throw new Error(`Request failed: ${response.status}`);"
232
+ );
233
+ lines.push(
234
+ apiAction === "list" || apiAction === "delete"
235
+ ? `const ${resultName} = await response.json();`
236
+ : `const { ${resultName} } = await response.json();`
237
+ );
238
+ return lines.join("\n");
239
+ }
240
+
241
+ function curlActionExample() {
242
+ const hasBody = apiAction === "create" || apiAction === "update" || apiAction === "duplicate";
243
+ const lines = [`curl -X ${selectedAction.method} \\`, " -H 'Accept: application/json' \\"];
244
+ if (authenticated) lines.push(" -H 'Authorization: Bearer <api-key>' \\");
245
+ if (hasBody) {
246
+ lines.push(
247
+ " -H 'Content-Type: application/json' \\",
248
+ ' -d \'{"title":"Example value"}\' \\'
249
+ );
250
+ }
251
+ lines.push(` ${JSON.stringify(requestURL || endpointPath)}`);
252
+ return lines.join("\n");
253
+ }
254
+
255
+ function setDepth(event: Event) {
256
+ const next = Number((event.currentTarget as HTMLInputElement).value);
257
+ depth = Number.isFinite(next) ? Math.min(5, Math.max(0, Math.round(next))) : 0;
258
+ }
259
+
260
+ async function copy(kind: "url" | "example", value: string) {
261
+ await navigator.clipboard.writeText(value);
262
+ copied = kind;
263
+ clearTimeout(copyResetTimer);
264
+ copyResetTimer = window.setTimeout(() => {
265
+ if (copied === kind) copied = undefined;
266
+ }, 1_500);
267
+ }
268
+
269
+ function formatBytes(value: number | undefined) {
270
+ if (value === undefined) return "";
271
+ return value < 1_024
272
+ ? i18n.formatNumber(value, { style: "unit", unit: "byte" })
273
+ : i18n.formatNumber(value / 1_024, {
274
+ maximumFractionDigits: 1,
275
+ style: "unit",
276
+ unit: "kilobyte",
277
+ });
278
+ }
279
+
280
+ function actionLabel(action: APIAction) {
281
+ switch (action) {
282
+ case "list":
283
+ return i18n.t("documents:apiListSearch");
284
+ case "find":
285
+ return i18n.t("documents:apiView");
286
+ case "create":
287
+ return i18n.t("documents:apiCreate");
288
+ case "update":
289
+ return i18n.t("documents:apiUpdate");
290
+ case "duplicate":
291
+ return i18n.t("documents:duplicate");
292
+ case "delete":
293
+ return i18n.t("documents:delete");
294
+ }
295
+ }
296
+ </script>
297
+
298
+ <section class="grid gap-6" aria-labelledby="api-heading">
299
+ <header class="grid gap-2">
300
+ <p class="font-mono text-[9.5px] tracking-[0.15em] text-foreground-faint uppercase">
301
+ {i18n.t("documents:liveRESTExplorer")}
302
+ </p>
303
+ <h2
304
+ id="api-heading"
305
+ class="text-[28px] font-semibold leading-tight tracking-[-0.02em] text-foreground-strong"
306
+ >
307
+ {i18n.t("documents:api")}
308
+ </h2>
309
+ <p class="max-w-2xl text-[13px] leading-5 text-foreground-muted">
310
+ {i18n.t("documents:apiDescription", {
311
+ label: resourceLabel.toLocaleLowerCase(i18n.language),
312
+ })}
313
+ </p>
314
+ </header>
315
+
316
+ <div class="grid gap-5 xl:grid-cols-[minmax(0,1.65fr)_minmax(340px,0.85fr)] xl:items-start">
317
+ <div class="grid min-w-0 gap-4">
318
+ <div class="overflow-hidden rounded-[4px] border border-control-border bg-background">
319
+ <div
320
+ class="flex min-w-0 flex-wrap items-center gap-2 border-b border-control-border bg-control px-3 py-2.5"
321
+ >
322
+ <span
323
+ class={[
324
+ "rounded-[3px] px-2 py-1 font-mono text-[10px] font-semibold",
325
+ selectedAction.method === "GET" && "bg-success/12 text-success",
326
+ selectedAction.method === "POST" && "bg-primary/12 text-primary",
327
+ selectedAction.method === "PATCH" && "bg-warning/12 text-warning",
328
+ selectedAction.method === "DELETE" && "bg-destructive/12 text-destructive",
329
+ ]}
330
+ >
331
+ {selectedAction.method}
332
+ </span>
333
+ <code class="min-w-0 flex-1 truncate text-[11.5px] text-foreground-muted">
334
+ {endpointPath || i18n.t("documents:apiAvailableAfterSave")}
335
+ </code>
336
+ {#if requestURL !== ""}
337
+ <Button variant="ghost" size="xs" onclick={() => copy("url", requestURL)}>
338
+ {#if copied === "url"}<CheckIcon class="size-3" />
339
+ {i18n.t("documents:copied")}{:else}<CopyIcon class="size-3" />
340
+ {i18n.t("documents:copyURL")}{/if}
341
+ </Button>
342
+ <Button
343
+ variant="ghost"
344
+ size="icon-xs"
345
+ href={requestURL}
346
+ target="_blank"
347
+ aria-label={i18n.t("documents:openAPIURL")}
348
+ tooltip={i18n.t("documents:openAPIURL")}
349
+ >
350
+ <ExternalLinkIcon class="size-3.5" />
351
+ </Button>
352
+ {/if}
353
+ </div>
354
+
355
+ <div
356
+ class="grid gap-4 border-b border-control-border px-4 py-4 md:grid-cols-[minmax(0,1fr)_auto] md:items-end"
357
+ >
358
+ <div class="flex flex-wrap items-end gap-5">
359
+ {#if runnableAction && !globalResource}
360
+ <label class="grid gap-1.5" for="ridu-api-depth">
361
+ <span class="text-[11.5px] font-medium text-foreground-muted">
362
+ {i18n.t("documents:apiDepth")}
363
+ </span>
364
+ <Input
365
+ id="ridu-api-depth"
366
+ class="w-20 font-mono"
367
+ type="number"
368
+ min="0"
369
+ max="5"
370
+ step="1"
371
+ value={depth}
372
+ oninput={setDepth}
373
+ />
374
+ </label>
375
+ {/if}
376
+ <label
377
+ class="flex min-h-10 cursor-pointer items-center gap-2 pb-0.5"
378
+ for="ridu-api-authenticated"
379
+ >
380
+ <Checkbox id="ridu-api-authenticated" bind:checked={authenticated} />
381
+ <span class="text-[12.5px] text-foreground-muted">
382
+ {i18n.t("documents:apiAuthenticated")}
383
+ </span>
384
+ </label>
385
+ </div>
386
+ <Button
387
+ variant="outline"
388
+ size="sm"
389
+ disabled={!liveRequest || loading}
390
+ onclick={() => (requestRevision += 1)}
391
+ >
392
+ <RefreshCwIcon class={["size-3.5", loading && "animate-spin"]} />
393
+ {loading
394
+ ? i18n.t("documents:apiRequesting")
395
+ : runnableAction
396
+ ? i18n.t("documents:apiRunRequest")
397
+ : i18n.t("documents:apiExampleOnly")}
398
+ </Button>
399
+ </div>
400
+
401
+ <div
402
+ class="flex min-h-10 flex-wrap items-center gap-x-4 gap-y-1 px-4 py-2 text-[10.5px] text-foreground-faint"
403
+ >
404
+ {#if responseStatus !== undefined}
405
+ <strong class={responseStatus < 400 ? "text-success" : "text-destructive"}>
406
+ {i18n.formatNumber(responseStatus, { useGrouping: false })}
407
+ {responseStatusText}
408
+ </strong>
409
+ {/if}
410
+ {#if responseDuration !== undefined}<span>
411
+ {i18n.formatNumber(responseDuration, {
412
+ style: "unit",
413
+ unit: "millisecond",
414
+ })}
415
+ </span>{/if}
416
+ {#if responseSize !== undefined}<span>{formatBytes(responseSize)}</span>{/if}
417
+ <span>
418
+ {authenticated
419
+ ? i18n.t("documents:apiCurrentSession")
420
+ : i18n.t("documents:apiPublicRequest")}
421
+ </span>
422
+ </div>
423
+ </div>
424
+
425
+ {const requestBanner = $derived(
426
+ !runnableAction
427
+ ? {
428
+ message: i18n.t("documents:apiMutationDescription"),
429
+ }
430
+ : !liveRequest
431
+ ? { message: i18n.t("documents:apiRunnableAfterSave") }
432
+ : requestError === undefined
433
+ ? undefined
434
+ : { message: requestError, tone: "destructive" as const }
435
+ )}
436
+
437
+ {#if requestBanner !== undefined}
438
+ <Banner tone={requestBanner.tone}>{requestBanner.message}</Banner>
439
+ {/if}
440
+
441
+ <JsonViewer class="xl:h-[calc(100vh-9rem)] xl:min-h-[620px]" value={responseValue} />
442
+ </div>
443
+
444
+ <aside class="grid min-w-0 gap-4 xl:sticky xl:top-4">
445
+ <nav
446
+ class="overflow-hidden rounded-[4px] border border-control-border bg-background"
447
+ aria-label={i18n.t("documents:apiActions")}
448
+ >
449
+ <p
450
+ class="border-b border-control-border bg-control px-3 py-2 font-mono text-[9.5px] tracking-[0.12em] text-foreground-faint uppercase"
451
+ >
452
+ {i18n.t("documents:apiActions")}
453
+ </p>
454
+ <div class="grid grid-cols-2 gap-px bg-control-border p-px xl:grid-cols-1">
455
+ {#each availableActions as action (action.key)}
456
+ <button
457
+ type="button"
458
+ class={[
459
+ "flex min-h-10 items-center gap-2.5 bg-background px-3 text-start text-[12.5px] outline-none transition-colors hover:bg-control focus-visible:bg-control",
460
+ apiAction === action.key && "bg-control text-foreground-strong",
461
+ ]}
462
+ onclick={() => (apiAction = action.key)}
463
+ aria-current={apiAction === action.key ? "page" : undefined}
464
+ >
465
+ <span
466
+ class={[
467
+ "w-12 font-mono text-[9px] font-semibold",
468
+ action.method === "GET" && "text-success",
469
+ action.method === "POST" && "text-primary",
470
+ action.method === "PATCH" && "text-warning",
471
+ action.method === "DELETE" && "text-destructive",
472
+ ]}
473
+ >
474
+ {action.method}
475
+ </span>
476
+ <span>{actionLabel(action.key)}</span>
477
+ </button>
478
+ {/each}
479
+ </div>
480
+ </nav>
481
+ <Tabs
482
+ bind:value={example}
483
+ class="overflow-hidden rounded-[4px] border border-control-border"
484
+ aria-labelledby="api-example-heading"
485
+ >
486
+ <div
487
+ class="flex flex-wrap items-center justify-between gap-3 border-b border-control-border bg-control px-3 py-2"
488
+ >
489
+ <TabsList
490
+ variant="line"
491
+ class="h-auto gap-1 rounded-none border-0 bg-transparent p-0"
492
+ aria-label={i18n.t("documents:apiExampleLanguage")}
493
+ >
494
+ {#each ["fetch", "sdk", "curl"] as option}
495
+ <TabsTrigger
496
+ value={option}
497
+ class={[
498
+ "h-auto flex-none rounded-[3px] px-2.5 py-1.5 text-[11.5px] capitalize",
499
+ "data-[state=active]:bg-background data-[state=active]:shadow-sm",
500
+ ]}
501
+ >
502
+ {option === "sdk" ? "Ridu SDK" : option}
503
+ </TabsTrigger>
504
+ {/each}
505
+ </TabsList>
506
+ <Button variant="ghost" size="xs" onclick={() => copy("example", requestExample)}>
507
+ {#if copied === "example"}<CheckIcon class="size-3" />
508
+ {i18n.t("documents:copied")}{:else}<CopyIcon class="size-3" />
509
+ {i18n.t("documents:copy")}{/if}
510
+ </Button>
511
+ </div>
512
+ <h3 id="api-example-heading" class="sr-only">
513
+ {i18n.t("documents:apiRequestExample")}
514
+ </h3>
515
+ <TabsContent value="fetch" class="mt-0">
516
+ <CodeBlock value={requestExampleFor("fetch")} language="javascript" />
517
+ </TabsContent>
518
+ <TabsContent value="sdk" class="mt-0">
519
+ <CodeBlock value={requestExampleFor("sdk")} language="javascript" />
520
+ </TabsContent>
521
+ <TabsContent value="curl" class="mt-0">
522
+ <CodeBlock value={requestExampleFor("curl")} language="bash" />
523
+ </TabsContent>
524
+ </Tabs>
525
+
526
+ <details class="group rounded-[4px] border border-control-border px-4 py-3">
527
+ <summary class="cursor-pointer text-[12.5px] font-medium text-foreground outline-none">
528
+ {i18n.t("documents:apiRequestParameters")}
529
+ </summary>
530
+ <div
531
+ class="mt-3 grid gap-3 border-t border-control-border pt-3 text-[12px] leading-5 text-foreground-muted sm:grid-cols-2"
532
+ >
533
+ {#if runnableAction}
534
+ <p>
535
+ <code class="text-foreground">depth</code>
536
+ {i18n.t("documents:apiDepthDescription")}
537
+ </p>
538
+ {/if}
539
+ <p>
540
+ <code class="text-foreground">authenticated</code>
541
+ {i18n.t("documents:apiAuthenticatedDescription")}
542
+ </p>
543
+ </div>
544
+ </details>
545
+ </aside>
546
+ </div>
547
+ </section>